Add PO pagination and load all POs from backend
Purchase orders page now shows 20 items per page with page navigation controls. loadPOs() fetches all pages via cursor pagination instead of capping at 200. Search filters the full dataset and resets to page 1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4c749e1299
commit
5708347c62
4 changed files with 85 additions and 208 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
node_modules/
|
||||
.DS_Store
|
||||
config.local.js
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -351,8 +351,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