PO pagination + fix Google Sign-In client_id error #2
5 changed files with 106 additions and 215 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
node_modules/
|
||||
.DS_Store
|
||||
config.local.js
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ ledgerflow_frontend/
|
|||
## Features
|
||||
|
||||
### Purchase Orders
|
||||
- Import POs from external DynamoDB table (Coupa purchase-orders)
|
||||
- POs auto-sync from external DynamoDB table via DynamoDB Streams (near-real-time)
|
||||
- Paginated table (20 per page) with page navigation controls
|
||||
- Search/filter across all POs (resets to page 1)
|
||||
- Add POs manually
|
||||
- Edit POs after import (vendor, amount, status, dates, notes)
|
||||
- Bill against POs to create invoices
|
||||
|
|
|
|||
|
|
@ -256,6 +256,12 @@
|
|||
|
||||
/* TABLE */
|
||||
.table-wrap { overflow-x: auto; }
|
||||
.pagination { display:flex;align-items:center;justify-content:center;gap:4px;padding:12px 16px;border-top:1px solid var(--border); }
|
||||
.pagination button { background:var(--surface2);border:1px solid var(--border);color:var(--text2);border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;font-family:var(--mono);transition:all .15s; }
|
||||
.pagination button:hover { border-color:var(--accent);color:var(--accent); }
|
||||
.pagination button.active { background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:500; }
|
||||
.pagination button:disabled { opacity:.35;cursor:default; }
|
||||
.pagination .page-info { font-size:11px;color:var(--text3);margin:0 8px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th {
|
||||
font-size: 10px;
|
||||
|
|
@ -579,7 +585,6 @@
|
|||
<div class="topbar">
|
||||
<div class="topbar-title" id="page-title">Dashboard</div>
|
||||
<div class="topbar-actions" id="topbar-actions">
|
||||
<button class="btn" onclick="openModal('importPOModal')">⬇ Import POs</button>
|
||||
<button class="btn primary" onclick="openModal('newInvoiceModal')">+ New Invoice</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -624,7 +629,7 @@
|
|||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>PO Number</th><th>Vendor</th><th>Amount</th><th>Status</th></tr></thead>
|
||||
<tbody id="dashboard-po-table"><tr><td colspan="4"><div class="empty-state" style="padding:20px"><div class="es-icon">📦</div><p>No POs imported yet</p></div></td></tr></tbody>
|
||||
<tbody id="dashboard-po-table"><tr><td colspan="4"><div class="empty-state" style="padding:20px"><div class="es-icon">📦</div><p>No POs synced yet</p></div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -660,26 +665,26 @@
|
|||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">
|
||||
<div>
|
||||
<h2 style="font-family:var(--sans);font-weight:700;font-size:18px">Purchase Orders</h2>
|
||||
<p style="color:var(--text3);font-size:11px;margin-top:4px">Imported from DynamoDB · <span id="po-count">0</span> records</p>
|
||||
<p style="color:var(--text3);font-size:11px;margin-top:4px">Auto-synced from DynamoDB · <span id="po-count">0</span> records</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn" onclick="openModal('importPOModal')">⬇ Import from DynamoDB</button>
|
||||
<button class="btn" onclick="openModal('addPOModal')">+ Add Manual PO</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">All Purchase Orders</span>
|
||||
<input class="form-input" style="width:200px" placeholder="🔍 Search POs..." oninput="filterTable('po-table', this.value)">
|
||||
<input class="form-input" style="width:200px" placeholder="🔍 Search POs..." id="po-search" oninput="state.poPage=1;refreshPOTable()">
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="po-table">
|
||||
<thead><tr><th>PO Number</th><th>Vendor</th><th>Issue Date</th><th>Due Date</th><th>Amount</th><th>Billed</th><th>Remaining</th><th>Status</th><th>Action</th></tr></thead>
|
||||
<tbody id="po-table-body">
|
||||
<tr><td colspan="9"><div class="empty-state"><div class="es-icon">📦</div><p>No purchase orders yet. Import from DynamoDB or add manually.</p></div></td></tr>
|
||||
<tr><td colspan="9"><div class="empty-state"><div class="es-icon">📦</div><p>No purchase orders yet. POs sync automatically from DynamoDB.</p></div></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination" id="po-pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -983,55 +988,6 @@
|
|||
|
||||
<!-- ===================== MODALS ===================== -->
|
||||
|
||||
<!-- Import PO Modal -->
|
||||
<div class="modal-overlay" id="importPOModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">Import Purchase Orders from DynamoDB</span>
|
||||
<button class="modal-close" onclick="closeModal('importPOModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="dynamo-import-state">
|
||||
<div style="margin-bottom:16px;padding:12px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text2)">
|
||||
<strong style="color:var(--text)">DynamoDB Configuration</strong><br>
|
||||
Region: <span id="imp-region">—</span> · Table: <span id="imp-table">—</span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Filter by Status</label>
|
||||
<select class="form-select" id="imp-status-filter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="OPEN">Open</option>
|
||||
<option value="PENDING">Pending</option>
|
||||
<option value="APPROVED">Approved</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Limit Records</label>
|
||||
<select class="form-select" id="imp-limit">
|
||||
<option>25</option><option>50</option><option>100</option><option>250</option><option>All</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" style="width:100%;margin-bottom:12px" onclick="fetchFromDynamo()">🔄 Fetch from DynamoDB</button>
|
||||
<div id="dynamo-preview" style="display:none">
|
||||
<div style="font-size:11px;color:var(--text3);margin-bottom:8px">PREVIEW — <span id="preview-count">0</span> records fetched</div>
|
||||
<div id="dynamo-preview-list"></div>
|
||||
</div>
|
||||
<div style="margin-top:14px;padding-top:14px;border-top:1px solid var(--border)">
|
||||
<div style="font-size:11px;color:var(--text3);margin-bottom:8px">OR PASTE SAMPLE JSON (for demo / testing)</div>
|
||||
<textarea class="form-textarea" id="dynamo-json-paste" placeholder='[{"po_number":"PO-001","vendor_name":"Acme Supplies","total_amount":15000,"status":"OPEN","issue_date":"2026-01-15","due_date":"2026-02-15"}]' style="height:90px;font-size:11px"></textarea>
|
||||
<button class="btn" style="width:100%;margin-top:8px" onclick="importFromJSON()">⬇ Import from JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" onclick="closeModal('importPOModal')">Cancel</button>
|
||||
<button class="btn primary" id="import-confirm-btn" onclick="confirmImport()">Import Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Manual PO Modal -->
|
||||
<div class="modal-overlay" id="addPOModal">
|
||||
<div class="modal">
|
||||
|
|
@ -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: '<button class="btn" onclick="openModal(\'importPOModal\')">⬇ Import POs</button><button class="btn primary" onclick="openModal(\'newInvoiceModal\')">+ New Invoice</button>',
|
||||
pos: '<button class="btn" onclick="openModal(\'importPOModal\')">⬇ Import from DynamoDB</button><button class="btn primary" onclick="openModal(\'addPOModal\')">+ Add PO</button>',
|
||||
dashboard: '<button class="btn primary" onclick="openModal(\'newInvoiceModal\')">+ New Invoice</button>',
|
||||
pos: '<button class="btn primary" onclick="openModal(\'addPOModal\')">+ Add PO</button>',
|
||||
invoices: '<button class="btn primary" onclick="openModal(\'newInvoiceModal\')">+ Create Invoice</button>',
|
||||
billing: '<button class="btn primary" onclick="openModal(\'newInvoiceModal\')">+ New Billing</button>',
|
||||
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 = '<p style="font-size:11px;color:var(--text3)">No records found in table.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = state.pendingImport.map((po, i) =>
|
||||
`<div class="po-match-bar selected" onclick="togglePendingPO(this,${i})" data-idx="${i}">
|
||||
<div class="po-match-check">✓</div>
|
||||
<div style="flex:1">
|
||||
<span style="color:var(--text);font-weight:500">${po.poNumber}</span>
|
||||
<span style="color:var(--text3);margin:0 8px">·</span>
|
||||
<span>${po.vendor}</span>
|
||||
</div>
|
||||
<span style="color:var(--accent)">${fmt(po.amount)}</span>
|
||||
<span class="badge ${po.status}" style="margin-left:8px">${po.status}</span>
|
||||
</div>`
|
||||
).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) =>
|
||||
`<div class="po-match-bar selected" onclick="togglePendingPO(this,${i})" data-idx="${i}">
|
||||
<div class="po-match-check">✓</div>
|
||||
<div style="flex:1">
|
||||
<span style="color:var(--text);font-weight:500">${po.poNumber}</span>
|
||||
<span style="color:var(--text3);margin:0 8px">·</span>
|
||||
<span>${po.vendor}</span>
|
||||
</div>
|
||||
<span style="color:var(--accent)">${fmt(po.amount)}</span>
|
||||
<span class="badge ${po.status}" style="margin-left:8px">${po.status}</span>
|
||||
</div>`
|
||||
).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 = '<tr><td colspan="9"><div class="empty-state"><div class="es-icon">📦</div><p>No POs yet</p></div></td></tr>';
|
||||
if (!filtered.length) {
|
||||
tbody.innerHTML = `<tr><td colspan="9"><div class="empty-state"><div class="es-icon">📦</div><p>${searchQuery ? 'No matching POs' : 'No POs yet'}</p></div></td></tr>`;
|
||||
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 `<tr>
|
||||
<td class="mono">${po.poNumber}</td>
|
||||
<td class="bold">${po.vendor}</td>
|
||||
|
|
@ -1739,6 +1569,38 @@ function refreshPOTable() {
|
|||
<td><button class="btn" style="padding:3px 9px;font-size:11px" onclick="billAgainstPO('${po.id}')">Bill</button> <button class="btn" style="padding:3px 9px;font-size:11px" onclick="openEditPO('${po.id}')">Edit</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Render pagination controls
|
||||
if (totalPages <= 1) { paginationEl.innerHTML = ''; return; }
|
||||
let btns = `<button onclick="goToPOPage(${state.poPage - 1})" ${state.poPage === 1 ? 'disabled' : ''}>«</button>`;
|
||||
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 += `<span class="page-info">…</span>`; }
|
||||
else { btns += `<button class="${p === state.poPage ? 'active' : ''}" onclick="goToPOPage(${p})">${p}</button>`; }
|
||||
});
|
||||
btns += `<button onclick="goToPOPage(${state.poPage + 1})" ${state.poPage === totalPages ? 'disabled' : ''}>»</button>`;
|
||||
btns += `<span class="page-info">${start+1}–${Math.min(start+state.poPerPage, filtered.length)} of ${filtered.length}</span>`;
|
||||
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() {
|
||||
|
|
|
|||
26
login.html
26
login.html
|
|
@ -6,8 +6,19 @@
|
|||
<title>LedgerFlow — Sign In</title>
|
||||
<!-- Config (API URL + Client ID) -->
|
||||
<script src="config.js"></script>
|
||||
<!-- Google Identity Services -->
|
||||
<script src="https://accounts.google.com/gsi/client" async defer onload="onGisLoaded()"></script>
|
||||
<!-- Google Identity Services — loaded dynamically after config.js sets GOOGLE_CLIENT_ID -->
|
||||
<script>
|
||||
// Load GSI only after client ID is available, preventing "Missing required parameter" errors
|
||||
(function() {
|
||||
var s = document.createElement('script');
|
||||
s.src = 'https://accounts.google.com/gsi/client';
|
||||
s.async = true;
|
||||
s.defer = true;
|
||||
if (window.GOOGLE_CLIENT_ID) s.setAttribute('data-client_id', window.GOOGLE_CLIENT_ID);
|
||||
s.onload = function() { if (typeof onGisLoaded === 'function') onGisLoaded(); };
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0c0f;
|
||||
|
|
@ -205,17 +216,17 @@ let _pendingClientId = null;
|
|||
|
||||
window.addEventListener("load", async () => {
|
||||
// Fetch Google Client ID from backend config endpoint
|
||||
let clientId = GOOGLE_CLIENT_ID;
|
||||
try {
|
||||
if (API_URL) {
|
||||
const res = await fetch(`${API_URL}/auth/config`);
|
||||
const cfg = await res.json();
|
||||
if (cfg.googleClientId) initGoogleSignIn(cfg.googleClientId);
|
||||
} else if (GOOGLE_CLIENT_ID) {
|
||||
initGoogleSignIn(GOOGLE_CLIENT_ID);
|
||||
if (cfg.googleClientId) clientId = cfg.googleClientId;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Could not load auth config:", e);
|
||||
}
|
||||
if (clientId) initGoogleSignIn(clientId);
|
||||
});
|
||||
|
||||
function initGoogleSignIn(clientId) {
|
||||
|
|
@ -245,7 +256,8 @@ function initGoogleSignIn(clientId) {
|
|||
);
|
||||
|
||||
// Hide the fallback button
|
||||
document.getElementById("fallback-btn").style.display = "none";
|
||||
const fb = document.getElementById("fallback-btn");
|
||||
if (fb) fb.style.display = "none";
|
||||
|
||||
// Show One Tap prompt
|
||||
google.accounts.id.prompt();
|
||||
|
|
@ -351,8 +363,6 @@ window.LedgerFlowAPI = {
|
|||
delete: (id) => window.LedgerFlowAPI._fetch(`/pos/${id}`, { method: "DELETE" }),
|
||||
import: (items, fieldMap, source) =>
|
||||
window.LedgerFlowAPI._fetch("/pos/import", { method: "POST", body: JSON.stringify({ items, fieldMap, source }) }),
|
||||
dynamoScan: (table, status, limit) =>
|
||||
window.LedgerFlowAPI._fetch(`/pos/dynamo-scan?table=${table}&status=${status || ""}&limit=${limit || 50}`),
|
||||
},
|
||||
|
||||
// Invoices
|
||||
|
|
|
|||
12
serve.js
12
serve.js
|
|
@ -1,5 +1,6 @@
|
|||
// Simple local dev server — serves frontend on http://localhost:3000
|
||||
// Usage: node serve.js
|
||||
// Usage: node serve.js (uses config.js — points to deployed API)
|
||||
// node serve.js --local (uses config.local.js — points to SAM local on :3001)
|
||||
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
|
|
@ -7,6 +8,7 @@ const path = require("path");
|
|||
|
||||
const PORT = 3000;
|
||||
const DIR = __dirname;
|
||||
const LOCAL_MODE = process.argv.includes("--local");
|
||||
|
||||
const MIME = {
|
||||
".html": "text/html",
|
||||
|
|
@ -22,6 +24,11 @@ http.createServer((req, res) => {
|
|||
let url = req.url.split("?")[0];
|
||||
if (url === "/") url = "/login.html";
|
||||
|
||||
// In local mode, serve config.local.js when config.js is requested
|
||||
if (LOCAL_MODE && url === "/config.js") {
|
||||
url = "/config.local.js";
|
||||
}
|
||||
|
||||
const filePath = path.join(DIR, url);
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
|
|
@ -35,5 +42,6 @@ http.createServer((req, res) => {
|
|||
res.end(data);
|
||||
});
|
||||
}).listen(PORT, () => {
|
||||
console.log(`\n LedgerFlow frontend: http://localhost:${PORT}\n`);
|
||||
const mode = LOCAL_MODE ? "LOCAL (API → localhost:3001)" : "DEPLOYED (API → production)";
|
||||
console.log(`\n LedgerFlow frontend: http://localhost:${PORT} [${mode}]\n`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue