@@ -1309,8 +1265,9 @@ let state = {
edi: { region: 'us-east-1', partnershipId: '', key: '', secret: '', senderId: '', receiverId: '', transformerId: '', connected: false },
company: { name: '', taxId: '', terms: 'Net 30', currency: 'USD', invPrefix: 'INV-', taxRate: 0 }
},
- pendingImport: [],
- invCounter: 1
+ invCounter: 1,
+ poPage: 1,
+ poPerPage: 20
};
// Load settings from backend
@@ -1345,8 +1302,8 @@ function navigate(view, el) {
const titles = { dashboard:'Dashboard', pos:'Purchase Orders', invoices:'Invoices', billing:'Billing', edi:'EDI Console', settings:'Connections & Settings' };
const actions = {
- dashboard: '
',
- pos: '
',
+ dashboard: '
',
+ pos: '
',
invoices: '
',
billing: '
',
edi: '',
@@ -1367,10 +1324,6 @@ function navigate(view, el) {
function openModal(id) {
document.getElementById(id).classList.add('open');
if (id === 'newInvoiceModal' && !editingInvoiceId) initNewInvoiceModal();
- if (id === 'importPOModal') {
- document.getElementById('imp-region').textContent = state.config.dynamo.region || 'โ';
- document.getElementById('imp-table').textContent = state.config.dynamo.table || 'โ';
- }
}
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
document.querySelectorAll('.modal-overlay').forEach(o => {
@@ -1516,141 +1469,6 @@ function updateConnectionBadges() {
// ============================================================
// PURCHASE ORDERS
// ============================================================
-async function fetchFromDynamo() {
- if (!state.config.dynamo.table) {
- toast('Configure DynamoDB table name in Settings first', 'warn');
- return;
- }
- toast('Fetching from DynamoDB...', 'info');
- const params = new URLSearchParams({ limit: '200' });
- const data = await api(`/pos/dynamo-scan?${params}`);
- if (!data) return;
-
- // Backend returns already-mapped items using the user's field config
- state.pendingImport = (data.items || []).map(r => ({
- poNumber: r.poNumber || 'โ',
- vendor: r.vendor || 'โ',
- amount: parseFloat(r.amount || 0),
- status: (r.status || 'open').toLowerCase(),
- issueDate: r.issueDate || '',
- dueDate: r.dueDate || '',
- notes: r._raw?.notes || r._raw?.description || ''
- }));
-
- document.getElementById('dynamo-preview').style.display = 'block';
- document.getElementById('preview-count').textContent = state.pendingImport.length;
- const list = document.getElementById('dynamo-preview-list');
- if (!state.pendingImport.length) {
- list.innerHTML = '
No records found in table.
';
- return;
- }
- list.innerHTML = state.pendingImport.map((po, i) =>
- `
-
โ
-
- ${po.poNumber}
- ยท
- ${po.vendor}
-
-
${fmt(po.amount)}
-
${po.status}
-
`
- ).join('');
- toast(`${state.pendingImport.length} records fetched from DynamoDB`, 'ok');
-}
-
-// Unmarshall DynamoDB JSON ({S:"val"}, {N:"123"}, {NULL:true}, etc.) to plain values
-function unmarshallValue(v) {
- if (v === null || v === undefined) return null;
- if (typeof v !== 'object') return v;
- if ('S' in v) return v.S;
- if ('N' in v) return parseFloat(v.N);
- if ('BOOL' in v) return v.BOOL;
- if ('NULL' in v) return null;
- if ('L' in v) return v.L.map(unmarshallValue);
- if ('M' in v) return unmarshallItem(v.M);
- return v;
-}
-function unmarshallItem(item) {
- const out = {};
- for (const [k, v] of Object.entries(item)) out[k] = unmarshallValue(v);
- return out;
-}
-function isDynamoJSON(obj) {
- const vals = Object.values(obj);
- if (vals.length === 0) return false;
- return vals.some(v => v && typeof v === 'object' && !Array.isArray(v) && ('S' in v || 'N' in v || 'NULL' in v || 'BOOL' in v || 'M' in v || 'L' in v));
-}
-
-function importFromJSON() {
- const raw = document.getElementById('dynamo-json-paste').value.trim();
- if (!raw) { toast('Paste JSON data first', 'warn'); return; }
- try {
- let data = JSON.parse(raw);
- if (!Array.isArray(data)) data = [data];
- // Auto-detect and unmarshall DynamoDB JSON format
- data = data.map(item => isDynamoJSON(item) ? unmarshallItem(item) : item);
- const fields = state.config.dynamo.fields;
- state.pendingImport = data.map(r => ({
- poNumber: r[fields.poNumber] || r.po_number || r.poNumber || 'โ',
- vendor: r[fields.vendor] || r.vendor_name || r.vendor || r.supplier?.name || 'โ',
- amount: parseFloat(r[fields.amount] || r.total_amount || r.amount || 0),
- status: (r[fields.status] || r.po_status || r.status || 'open').toLowerCase(),
- issueDate: r[fields.issueDate] || r.issue_date || r.issueDate || r.order_date || '',
- dueDate: r[fields.dueDate] || r.due_date || r.dueDate || '',
- notes: r.notes || r.description || ''
- }));
-
- document.getElementById('dynamo-preview').style.display = 'block';
- document.getElementById('preview-count').textContent = state.pendingImport.length;
- const list = document.getElementById('dynamo-preview-list');
- list.innerHTML = state.pendingImport.map((po, i) =>
- `
-
โ
-
- ${po.poNumber}
- ยท
- ${po.vendor}
-
-
${fmt(po.amount)}
-
${po.status}
-
`
- ).join('');
- toast(`${state.pendingImport.length} records parsed`, 'ok');
- } catch(e) {
- toast('Invalid JSON: ' + e.message, 'err');
- }
-}
-
-function togglePendingPO(el, idx) {
- el.classList.toggle('selected');
-}
-
-async function confirmImport() {
- const selected = document.querySelectorAll('#dynamo-preview-list .po-match-bar.selected');
- if (selected.length === 0) { toast('Select at least one PO to import', 'warn'); return; }
-
- const items = [];
- selected.forEach(el => {
- const idx = parseInt(el.dataset.idx);
- items.push(state.pendingImport[idx]);
- });
-
- toast('Importing POs...', 'info');
- const data = await api('/pos/import', {
- method: 'POST',
- body: { items, source: 'dynamodb' },
- });
- if (!data) return;
-
- closeModal('importPOModal');
- await loadPOs();
- refreshPOTable();
- refreshDashboard();
- addActivity(`Imported ${data.imported} PO(s) from DynamoDB (${data.skipped} skipped)`, 'green');
- toast(`Imported ${data.imported} PO(s) successfully`, 'ok');
- state.pendingImport = [];
-}
async function addManualPO() {
const poNumber = document.getElementById('add-po-number').value.trim();
@@ -1719,14 +1537,26 @@ async function saveEditPO() {
function refreshPOTable() {
const tbody = document.getElementById('po-table-body');
+ const paginationEl = document.getElementById('po-pagination');
+ const searchQuery = (document.getElementById('po-search')?.value || '').toLowerCase();
+ const filtered = searchQuery
+ ? state.pos.filter(po => `${po.poNumber} ${po.vendor} ${po.status}`.toLowerCase().includes(searchQuery))
+ : state.pos;
document.getElementById('po-count').textContent = state.pos.length;
- if (!state.pos.length) {
- tbody.innerHTML = '
|
';
+ if (!filtered.length) {
+ tbody.innerHTML = `
๐ฆ ${searchQuery ? 'No matching POs' : 'No POs yet'} |
`;
+ paginationEl.innerHTML = '';
return;
}
- tbody.innerHTML = state.pos.map(po => {
+
+ const totalPages = Math.ceil(filtered.length / state.poPerPage);
+ if (state.poPage > totalPages) state.poPage = totalPages;
+ if (state.poPage < 1) state.poPage = 1;
+ const start = (state.poPage - 1) * state.poPerPage;
+ const pageItems = filtered.slice(start, start + state.poPerPage);
+
+ tbody.innerHTML = pageItems.map(po => {
const remaining = po.amount - (po.billed || 0);
- const pct = po.amount > 0 ? Math.round((po.billed||0)/po.amount*100) : 0;
return `
| ${po.poNumber} |
${po.vendor} |
@@ -1739,6 +1569,38 @@ function refreshPOTable() {
|
`;
}).join('');
+
+ // Render pagination controls
+ if (totalPages <= 1) { paginationEl.innerHTML = ''; return; }
+ let btns = `
`;
+ const maxVisible = 7;
+ let pages = [];
+ if (totalPages <= maxVisible) {
+ for (let i = 1; i <= totalPages; i++) pages.push(i);
+ } else {
+ pages.push(1);
+ let lo = Math.max(2, state.poPage - 1);
+ let hi = Math.min(totalPages - 1, state.poPage + 1);
+ if (state.poPage <= 3) hi = Math.min(totalPages - 1, 4);
+ if (state.poPage >= totalPages - 2) lo = Math.max(2, totalPages - 3);
+ if (lo > 2) pages.push('โฆ');
+ for (let i = lo; i <= hi; i++) pages.push(i);
+ if (hi < totalPages - 1) pages.push('โฆ');
+ pages.push(totalPages);
+ }
+ pages.forEach(p => {
+ if (p === 'โฆ') { btns += `
โฆ`; }
+ else { btns += `
`; }
+ });
+ btns += `
`;
+ btns += `
${start+1}โ${Math.min(start+state.poPerPage, filtered.length)} of ${filtered.length}`;
+ paginationEl.innerHTML = btns;
+}
+
+function goToPOPage(page) {
+ state.poPage = page;
+ refreshPOTable();
+ document.getElementById('po-table').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function billAgainstPO(poId) {
@@ -2453,8 +2315,16 @@ function filterTable(tableId, query) {
// DATA LOADERS
// ============================================================
async function loadPOs() {
- const data = await api('/pos?limit=200');
- if (data && data.items) state.pos = data.items;
+ let all = [];
+ let cursor = null;
+ do {
+ const url = '/pos?limit=200' + (cursor ? '&cursor=' + encodeURIComponent(cursor) : '');
+ const data = await api(url);
+ if (!data || !data.items) break;
+ all = all.concat(data.items);
+ cursor = data.cursor || null;
+ } while (cursor);
+ state.pos = all;
}
async function loadInvoices() {
diff --git a/login.html b/login.html
index 3d11269..749b556 100644
--- a/login.html
+++ b/login.html
@@ -6,8 +6,19 @@
LedgerFlow โ Sign In
-
-
+
+