ledgerflow-frontend/accounting-app.html
Adam Moussa 5708347c62 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>
2026-04-03 13:58:28 -04:00

2373 lines
96 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LedgerFlow — B2B Accounting</title>
<!-- Config (API URL + Client ID) -->
<script src="config.js"></script>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0c0f;
--surface: #111318;
--surface2: #181c24;
--border: #1f2533;
--border2: #2a3348;
--accent: #00e5a0;
--accent2: #0066ff;
--accent3: #ff6b35;
--text: #e8edf5;
--text2: #8892a4;
--text3: #4a5568;
--red: #ff4757;
--yellow: #ffd32a;
--green: #00e5a0;
--mono: 'DM Mono', monospace;
--sans: 'Syne', sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--mono);
font-size: 13px;
min-height: 100vh;
display: flex;
overflow: hidden;
}
/* SIDEBAR */
.sidebar {
width: 220px;
min-width: 220px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 0;
position: relative;
z-index: 10;
}
.logo {
padding: 24px 20px 20px;
border-bottom: 1px solid var(--border);
}
.logo-mark {
font-family: var(--sans);
font-weight: 800;
font-size: 18px;
color: var(--accent);
letter-spacing: -0.5px;
}
.logo-sub {
font-size: 10px;
color: var(--text3);
letter-spacing: 2px;
text-transform: uppercase;
margin-top: 2px;
}
.nav {
flex: 1;
padding: 16px 0;
}
.nav-section {
padding: 8px 16px 4px;
font-size: 9px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text3);
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 16px;
cursor: pointer;
color: var(--text2);
font-size: 12px;
border-left: 2px solid transparent;
transition: all 0.15s;
user-select: none;
}
.nav-item:hover { background: var(--surface2); color: var(--text); }
.nav-item.active {
background: rgba(0,229,160,0.06);
color: var(--accent);
border-left-color: var(--accent);
}
.nav-icon { width: 16px; text-align: center; font-size: 14px; }
.sidebar-footer {
padding: 16px;
border-top: 1px solid var(--border);
}
.connection-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 6px;
background: var(--surface2);
border: 1px solid var(--border);
font-size: 11px;
color: var(--text2);
margin-bottom: 6px;
cursor: pointer;
transition: border-color 0.15s;
}
.connection-badge:hover { border-color: var(--border2); }
.connection-badge .dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--text3);
flex-shrink: 0;
}
.connection-badge.connected .dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
.logout-btn {
width: 100%;
margin-top: 8px;
padding: 8px 10px;
border-radius: 6px;
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text2);
font-size: 11px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.logout-btn:hover { background: var(--danger, #e74c3c); color: #fff; border-color: var(--danger, #e74c3c); }
/* MAIN */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
height: 56px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 24px;
gap: 16px;
background: var(--surface);
}
.topbar-title {
font-family: var(--sans);
font-weight: 700;
font-size: 16px;
color: var(--text);
flex: 1;
}
.topbar-actions { display: flex; gap: 8px; }
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border-radius: 6px;
border: 1px solid var(--border2);
background: var(--surface2);
color: var(--text);
font-family: var(--mono);
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.primary {
background: var(--accent);
border-color: var(--accent);
color: #000;
font-weight: 500;
}
.btn.primary:hover { background: #00ffb2; border-color: #00ffb2; color: #000; }
.btn.danger { border-color: var(--red); color: var(--red); }
.btn.danger:hover { background: rgba(255,71,87,0.1); }
.content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.content::-webkit-scrollbar { width: 4px; }
.content::-webkit-scrollbar-track { background: transparent; }
.content::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
/* VIEWS */
.view { display: none; }
.view.active { display: block; }
/* DASHBOARD */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent), transparent);
}
.stat-label { font-size: 10px; letter-spacing: 2px; text-transform: uppercase; color: var(--text3); margin-bottom: 8px; }
.stat-value { font-family: var(--sans); font-size: 26px; font-weight: 700; color: var(--text); }
.stat-sub { font-size: 11px; color: var(--text2); margin-top: 4px; }
.stat-delta { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; margin-top: 6px; }
.stat-delta.up { color: var(--green); }
.stat-delta.down { color: var(--red); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--border);
}
.card-title { font-family: var(--sans); font-weight: 600; font-size: 13px; color: var(--text); }
.card-body { padding: 18px; }
/* 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;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--text3);
text-align: left;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.1s;
cursor: pointer;
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--surface2); }
tbody td {
padding: 11px 14px;
font-size: 12px;
color: var(--text2);
white-space: nowrap;
}
tbody td.mono { font-family: var(--mono); color: var(--text); }
tbody td.bold { color: var(--text); font-weight: 500; }
/* BADGES */
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
letter-spacing: 0.5px;
font-weight: 500;
text-transform: uppercase;
}
.badge.open { background: rgba(0,102,255,0.15); color: #5b9bff; border: 1px solid rgba(0,102,255,0.2); }
.badge.pending { background: rgba(255,211,42,0.12); color: var(--yellow); border: 1px solid rgba(255,211,42,0.2); }
.badge.submitted { background: rgba(0,229,160,0.1); color: var(--accent); border: 1px solid rgba(0,229,160,0.2); }
.badge.modified { background: rgba(255,140,50,0.12); color: #ff8c32; border: 1px solid rgba(255,140,50,0.25); }
.badge.paid { background: rgba(0,229,160,0.05); color: #4a8; border: 1px solid rgba(0,229,160,0.1); }
.badge.overdue { background: rgba(255,71,87,0.12); color: var(--red); border: 1px solid rgba(255,71,87,0.2); }
.badge.partial { background: rgba(255,107,53,0.12); color: var(--accent3); border: 1px solid rgba(255,107,53,0.2); }
.badge-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
/* MODAL */
.modal-overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.7);
z-index: 1000;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.modal-overlay.open { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border2);
border-radius: 12px;
width: 580px;
max-width: 95vw;
max-height: 88vh;
overflow-y: auto;
box-shadow: 0 24px 60px rgba(0,0,0,0.6);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--border);
position: sticky; top: 0;
background: var(--surface);
z-index: 1;
}
.modal-title { font-family: var(--sans); font-weight: 700; font-size: 15px; }
.modal-close { background: none; border: none; color: var(--text2); cursor: pointer; font-size: 20px; padding: 2px; }
.modal-close:hover { color: var(--text); }
.modal-body { padding: 24px; }
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 10px;
background: var(--surface);
position: sticky; bottom: 0;
}
/* FORMS */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px; }
.form-row.full { grid-template-columns: 1fr; }
.form-row.three { grid-template-columns: 1fr 1fr 1fr; }
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-label { font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--text2); }
.form-input, .form-select, .form-textarea {
background: var(--surface2);
border: 1px solid var(--border2);
border-radius: 6px;
padding: 9px 12px;
color: var(--text);
font-family: var(--mono);
font-size: 12px;
outline: none;
transition: border-color 0.15s;
width: 100%;
}
.form-input:focus, .form-select:focus, .form-textarea:focus { border-color: var(--accent); }
.form-select { appearance: none; cursor: pointer; }
.form-textarea { resize: vertical; min-height: 80px; }
.form-hint { font-size: 10px; color: var(--text3); }
/* LINE ITEMS */
.line-items-table { width: 100%; border-collapse: collapse; margin-top: 6px; }
.line-items-table th {
font-size: 9px; letter-spacing: 1.5px; text-transform: uppercase;
color: var(--text3); text-align: left; padding: 6px 8px;
border-bottom: 1px solid var(--border);
}
.line-items-table td { padding: 6px 8px; }
.line-items-table td input {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 4px;
padding: 5px 8px;
color: var(--text);
font-family: var(--mono);
font-size: 11px;
width: 100%;
outline: none;
}
.line-items-table td input:focus { border-color: var(--accent); }
.li-del { background: none; border: none; color: var(--text3); cursor: pointer; font-size: 16px; padding: 2px 6px; }
.li-del:hover { color: var(--red); }
.totals-box {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
margin-top: 14px;
}
.totals-row { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: 12px; color: var(--text2); }
.totals-row.total { font-family: var(--sans); font-weight: 700; font-size: 16px; color: var(--text); padding-top: 10px; border-top: 1px solid var(--border); margin-top: 6px; }
/* SETTINGS / CONNECTIONS */
.settings-section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 20px;
overflow: hidden;
}
.settings-section-header {
padding: 14px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.settings-section-icon { font-size: 18px; }
.settings-section-title { font-family: var(--sans); font-weight: 700; font-size: 14px; }
.settings-section-body { padding: 20px; }
.settings-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px; }
.test-btn-wrap { display: flex; gap: 10px; align-items: flex-end; }
/* NOTIFICATION TOAST */
#toast {
position: fixed;
bottom: 24px; right: 24px;
background: var(--surface2);
border: 1px solid var(--border2);
border-radius: 8px;
padding: 12px 18px;
font-size: 12px;
color: var(--text);
z-index: 9999;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
transform: translateY(80px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
#toast.show { transform: translateY(0); opacity: 1; }
#toast .t-icon { font-size: 16px; }
/* PROGRESS BAR */
.progress-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent2));
border-radius: 2px;
transition: width 0.3s;
}
/* EMPTY STATE */
.empty-state {
text-align: center;
padding: 48px 24px;
color: var(--text3);
}
.empty-state .es-icon { font-size: 36px; margin-bottom: 12px; }
.empty-state p { font-size: 13px; }
/* ACTIVITY FEED */
.activity-item {
display: flex;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid var(--border);
}
.activity-item:last-child { border-bottom: none; }
.activity-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; margin-top: 4px; }
.activity-dot.blue { background: var(--accent2); }
.activity-dot.orange { background: var(--accent3); }
.activity-text { font-size: 12px; color: var(--text2); line-height: 1.5; }
.activity-time { font-size: 10px; color: var(--text3); margin-top: 2px; }
/* EDI STATUS */
.edi-log {
background: #070a0e;
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
font-size: 11px;
line-height: 1.8;
color: var(--text2);
max-height: 200px;
overflow-y: auto;
font-family: var(--mono);
}
.edi-log span.ok { color: var(--accent); }
.edi-log span.err { color: var(--red); }
.edi-log span.info { color: var(--accent2); }
.edi-log span.warn { color: var(--yellow); }
.po-match-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--surface2);
border-radius: 6px;
border: 1px solid var(--border);
margin-bottom: 8px;
cursor: pointer;
transition: border-color 0.15s;
}
.po-match-bar:hover { border-color: var(--accent); }
.po-match-bar.selected { border-color: var(--accent); background: rgba(0,229,160,0.06); }
.po-match-check { width: 16px; height: 16px; border-radius: 4px; border: 1.5px solid var(--border2); display: flex; align-items: center; justify-content: center; color: var(--accent); font-size: 12px; }
.po-match-bar.selected .po-match-check { background: var(--accent); border-color: var(--accent); color: #000; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
.pulsing { animation: pulse 2s infinite; }
@keyframes fadeIn { from { opacity:0; transform: translateY(6px); } to { opacity:1; transform: none; } }
.view.active { animation: fadeIn 0.2s ease; }
</style>
</head>
<body>
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="logo">
<div class="logo-mark">LedgerFlow</div>
<div class="logo-sub">B2B Accounting</div>
</div>
<nav class="nav">
<div class="nav-section">Overview</div>
<div class="nav-item active" onclick="navigate('dashboard', this)">
<span class="nav-icon">◈</span> Dashboard
</div>
<div class="nav-section">Procurement</div>
<div class="nav-item" onclick="navigate('pos', this)">
<span class="nav-icon">📦</span> Purchase Orders
</div>
<div class="nav-item" onclick="navigate('invoices', this)">
<span class="nav-icon">🧾</span> Invoices
</div>
<div class="nav-item" onclick="navigate('billing', this)">
<span class="nav-icon">💳</span> Billing
</div>
<div class="nav-section">Integration</div>
<div class="nav-item" onclick="navigate('edi', this)">
<span class="nav-icon">⚡</span> EDI Console
</div>
<div class="nav-item" onclick="navigate('settings', this)">
<span class="nav-icon">⚙</span> Connections
</div>
</nav>
<div class="sidebar-footer">
<div class="connection-badge" id="dynamo-badge" onclick="navigate('settings', document.querySelector('[onclick*=settings]'))">
<div class="dot" id="dynamo-dot"></div>
<span id="dynamo-label">DynamoDB</span>
</div>
<div class="connection-badge" id="edi-badge" onclick="navigate('settings', document.querySelector('[onclick*=settings]'))">
<div class="dot" id="edi-dot"></div>
<span id="edi-label">AWS B2B EDI</span>
</div>
<button class="logout-btn" onclick="logout()">Sign Out</button>
</div>
</aside>
<!-- MAIN -->
<div class="main">
<div class="topbar">
<div class="topbar-title" id="page-title">Dashboard</div>
<div class="topbar-actions" id="topbar-actions">
<button class="btn primary" onclick="openModal('newInvoiceModal')">+ New Invoice</button>
</div>
</div>
<div class="content">
<!-- DASHBOARD -->
<div class="view active" id="view-dashboard">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Open POs</div>
<div class="stat-value" id="stat-pos">0</div>
<div class="stat-sub">Total value: <span id="stat-pos-val">$0</span></div>
<div class="stat-delta up" id="stat-pos-delta">↑ 0 this month</div>
</div>
<div class="stat-card">
<div class="stat-label">Pending Invoices</div>
<div class="stat-value" id="stat-inv">0</div>
<div class="stat-sub">Awaiting payment</div>
<div class="stat-delta" id="stat-inv-delta" style="color:var(--yellow)">● 0 overdue</div>
</div>
<div class="stat-card">
<div class="stat-label">Billed (MTD)</div>
<div class="stat-value" id="stat-billed">$0</div>
<div class="stat-sub">Month to date</div>
<div class="stat-delta up">↑ 0%</div>
</div>
<div class="stat-card">
<div class="stat-label">EDI Transactions</div>
<div class="stat-value" id="stat-edi">0</div>
<div class="stat-sub">Submitted today</div>
<div class="stat-delta" id="stat-edi-delta" style="color:var(--accent2)">● 0 processing</div>
</div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-header">
<span class="card-title">Recent Purchase Orders</span>
<button class="btn" style="padding:4px 10px;font-size:11px" onclick="navigate('pos', document.querySelectorAll('.nav-item')[2])">View All</button>
</div>
<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 synced yet</p></div></td></tr></tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Activity Feed</span>
</div>
<div class="card-body" id="activity-feed">
<div class="empty-state" style="padding:12px">
<div class="es-icon">📋</div>
<p>Activity will appear here</p>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Recent Invoices</span>
<button class="btn" style="padding:4px 10px;font-size:11px" onclick="navigate('invoices', document.querySelectorAll('.nav-item')[3])">View All</button>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Invoice #</th><th>PO Reference</th><th>Vendor</th><th>Amount</th><th>EDI Status</th><th>Status</th></tr></thead>
<tbody id="dashboard-inv-table"><tr><td colspan="6"><div class="empty-state" style="padding:20px"><div class="es-icon">🧾</div><p>No invoices created yet</p></div></td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- PURCHASE ORDERS -->
<div class="view" id="view-pos">
<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">Auto-synced from DynamoDB · <span id="po-count">0</span> records</p>
</div>
<div style="display:flex;gap:8px">
<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..." 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. POs sync automatically from DynamoDB.</p></div></td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="po-pagination"></div>
</div>
</div>
<!-- INVOICES -->
<div class="view" id="view-invoices">
<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">Invoices</h2>
<p style="color:var(--text3);font-size:11px;margin-top:4px"><span id="inv-count">0</span> invoices · Submit via AWS B2B EDI</p>
</div>
<button class="btn primary" onclick="openModal('newInvoiceModal')">+ Create Invoice</button>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">All Invoices</span>
<div style="display:flex;align-items:center;gap:8px">
<button class="btn" id="inv-bulk-delete-btn" style="display:none;padding:4px 12px;font-size:11px;color:var(--red);border-color:var(--red)" onclick="confirmBulkDelete()">Delete Selected (<span id="inv-selected-count">0</span>)</button>
<input class="form-input" style="width:200px" placeholder="🔍 Search..." oninput="filterTable('inv-table', this.value)">
</div>
</div>
<div class="table-wrap">
<table id="inv-table">
<thead><tr><th style="width:32px"><input type="checkbox" id="inv-select-all" onchange="toggleAllInvoices(this.checked)"></th><th>Invoice #</th><th>PO Reference</th><th>Vendor</th><th>Issued</th><th>Due</th><th>Amount</th><th>EDI Status</th><th>Status</th><th>Actions</th></tr></thead>
<tbody id="inv-table-body">
<tr><td colspan="10"><div class="empty-state"><div class="es-icon">🧾</div><p>No invoices yet. Create one against a PO.</p></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- BILLING -->
<div class="view" id="view-billing">
<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">Billing</h2>
<p style="color:var(--text3);font-size:11px;margin-top:4px">Bill against open Purchase Orders</p>
</div>
<button class="btn primary" onclick="openModal('newInvoiceModal')">+ New Billing</button>
</div>
<div class="stats-grid" style="grid-template-columns:repeat(3,1fr)">
<div class="stat-card">
<div class="stat-label">Total Invoiced</div>
<div class="stat-value" id="bill-total">$0.00</div>
</div>
<div class="stat-card">
<div class="stat-label">Collected</div>
<div class="stat-value" id="bill-paid" style="color:var(--accent)">$0.00</div>
</div>
<div class="stat-card">
<div class="stat-label">Outstanding</div>
<div class="stat-value" id="bill-outstanding" style="color:var(--yellow)">$0.00</div>
</div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">PO Utilization</span></div>
<div class="table-wrap">
<table>
<thead><tr><th>PO Number</th><th>Vendor</th><th>PO Total</th><th>Billed</th><th>Remaining</th><th>Utilization</th></tr></thead>
<tbody id="billing-table-body">
<tr><td colspan="6"><div class="empty-state"><div class="es-icon">💳</div><p>Import POs to view billing utilization</p></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- EDI CONSOLE -->
<div class="view" id="view-edi">
<div style="margin-bottom:20px">
<h2 style="font-family:var(--sans);font-weight:700;font-size:18px">EDI Console</h2>
<p style="color:var(--text3);font-size:11px;margin-top:4px">AWS B2B EDI · X12 004010 · Transaction Monitor</p>
</div>
<div class="grid-2">
<div class="card">
<div class="card-header"><span class="card-title">Submit EDI Transaction</span></div>
<div class="card-body">
<div class="form-group" style="margin-bottom:12px">
<label class="form-label">Transaction Type</label>
<select class="form-select" id="edi-tx-type">
<option value="810">810 — Invoice</option>
<option value="850">850 — Purchase Order</option>
<option value="856">856 — Advance Ship Notice</option>
<option value="855">855 — PO Acknowledgment</option>
</select>
</div>
<div class="form-group" style="margin-bottom:12px">
<label class="form-label">Select Invoice</label>
<select class="form-select" id="edi-invoice-select">
<option value="">— Select invoice —</option>
</select>
</div>
<div class="form-group" style="margin-bottom:16px">
<label class="form-label">Trading Partner ID</label>
<input class="form-input" id="edi-partner-id" placeholder="e.g. VENDOR-001">
</div>
<div style="display:flex;gap:8px">
<button class="btn" style="flex:1" onclick="downloadEDI()">&#11015; Download EDI File</button>
<button class="btn primary" style="flex:1" onclick="submitEDI()">⚡ Submit to AWS B2B EDI</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Transaction Log</span>
<button class="btn" style="padding:4px 10px;font-size:11px" onclick="clearEDILog()">Clear</button>
</div>
<div class="card-body">
<div class="edi-log" id="edi-log">
<span class="info">[SYSTEM]</span> EDI console ready. Configure AWS connection in Settings.<br>
<span class="info">[INFO]</span> X12 004010 · Supported: 810, 850, 855, 856 transaction sets.<br>
</div>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-header"><span class="card-title">EDI Transaction History</span></div>
<div class="table-wrap">
<table>
<thead><tr><th>Timestamp</th><th>TX Type</th><th>Invoice #</th><th>Partner</th><th>ISA Control #</th><th>Status</th></tr></thead>
<tbody id="edi-history-body">
<tr><td colspan="6"><div class="empty-state" style="padding:20px"><div class="es-icon">⚡</div><p>No EDI transactions yet</p></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- SETTINGS / CONNECTIONS -->
<div class="view" id="view-settings">
<div style="margin-bottom:20px">
<h2 style="font-family:var(--sans);font-weight:700;font-size:18px">Connections & Settings</h2>
<p style="color:var(--text3);font-size:11px;margin-top:4px">Configure AWS DynamoDB and B2B EDI integrations</p>
</div>
<div class="settings-section">
<div class="settings-section-header">
<span class="settings-section-icon">🗄</span>
<div>
<div class="settings-section-title">AWS DynamoDB</div>
<div style="font-size:11px;color:var(--text3)">Import Purchase Orders from your DynamoDB table</div>
</div>
<div style="margin-left:auto" id="dynamo-status-badge"><span class="badge pending"><span class="badge-dot"></span>Not Connected</span></div>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="form-group">
<label class="form-label">AWS Region</label>
<select class="form-select" id="cfg-dynamo-region">
<option>us-east-1</option><option>us-east-2</option><option>us-west-1</option><option>us-west-2</option>
<option>eu-west-1</option><option>eu-west-2</option><option>eu-central-1</option>
<option>ap-southeast-1</option><option>ap-northeast-1</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Table Name</label>
<input class="form-input" id="cfg-dynamo-table" placeholder="e.g. PurchaseOrders">
</div>
</div>
<div class="settings-row">
<div class="form-group">
<label class="form-label">Access Key ID</label>
<input class="form-input" id="cfg-dynamo-key" placeholder="AKIA...">
</div>
<div class="form-group">
<label class="form-label">Secret Access Key</label>
<input class="form-input" type="password" id="cfg-dynamo-secret" placeholder="••••••••••••••••">
</div>
</div>
<div class="settings-row">
<div class="form-group">
<label class="form-label">PO Number Field</label>
<input class="form-input" id="cfg-dynamo-po-field" placeholder="po_number" value="po_number">
</div>
<div class="form-group">
<label class="form-label">Vendor Field</label>
<input class="form-input" id="cfg-dynamo-vendor-field" placeholder="vendor_name" value="vendor_name">
</div>
</div>
<div class="settings-row">
<div class="form-group">
<label class="form-label">Amount Field</label>
<input class="form-input" id="cfg-dynamo-amount-field" placeholder="total_amount" value="total_amount">
</div>
<div class="form-group">
<label class="form-label">Status Field</label>
<input class="form-input" id="cfg-dynamo-status-field" placeholder="status" value="status">
</div>
</div>
<div class="test-btn-wrap">
<button class="btn" onclick="testDynamoConnection()">🔌 Test Connection</button>
<button class="btn primary" onclick="saveDynamoConfig()">Save Configuration</button>
</div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-header">
<span class="settings-section-icon">⚡</span>
<div>
<div class="settings-section-title">AWS B2B EDI</div>
<div style="font-size:11px;color:var(--text3)">Submit EDI transactions (810, 850, 855, 856)</div>
</div>
<div style="margin-left:auto" id="edi-status-badge"><span class="badge pending"><span class="badge-dot"></span>Not Connected</span></div>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="form-group">
<label class="form-label">AWS Region</label>
<select class="form-select" id="cfg-edi-region">
<option>us-east-1</option><option>us-east-2</option><option>us-west-2</option>
<option>eu-west-1</option><option>ap-southeast-1</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Partnership ID</label>
<input class="form-input" id="cfg-edi-partnership" placeholder="p-XXXXXXXXXX">
</div>
</div>
<div class="settings-row">
<div class="form-group">
<label class="form-label">Access Key ID</label>
<input class="form-input" id="cfg-edi-key" placeholder="AKIA...">
</div>
<div class="form-group">
<label class="form-label">Secret Access Key</label>
<input class="form-input" type="password" id="cfg-edi-secret" placeholder="••••••••••••••••">
</div>
</div>
<div class="settings-row">
<div class="form-group">
<label class="form-label">ISA Sender ID</label>
<input class="form-input" id="cfg-edi-sender" placeholder="Your EDI Sender ID">
</div>
<div class="form-group">
<label class="form-label">ISA Receiver ID</label>
<input class="form-input" id="cfg-edi-receiver" placeholder="Partner EDI Receiver ID">
</div>
</div>
<div class="form-group" style="margin-bottom:14px">
<label class="form-label">Transformer ID (X12 810 · 004010)</label>
<input class="form-input" id="cfg-edi-transformer" placeholder="tr-XXXXXXXXXX">
<span class="form-hint">Found in AWS B2B Data Interchange console</span>
</div>
<div class="test-btn-wrap">
<button class="btn" onclick="testEDIConnection()">🔌 Test Connection</button>
<button class="btn primary" onclick="saveEDIConfig()">Save Configuration</button>
</div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-header">
<span class="settings-section-icon">🏢</span>
<div><div class="settings-section-title">Company Settings</div></div>
</div>
<div class="settings-section-body">
<div class="settings-row">
<div class="form-group">
<label class="form-label">Company Name</label>
<input class="form-input" id="cfg-company-name" placeholder="Acme Corp">
</div>
<div class="form-group">
<label class="form-label">Tax ID / EIN</label>
<input class="form-input" id="cfg-tax-id" placeholder="XX-XXXXXXX">
</div>
</div>
<div class="settings-row">
<div class="form-group">
<label class="form-label">Default Payment Terms</label>
<select class="form-select" id="cfg-payment-terms">
<option>Net 30</option><option>Net 15</option><option>Net 45</option><option>Net 60</option><option>90 NET</option><option>Due on Receipt</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Default Currency</label>
<select class="form-select" id="cfg-currency">
<option>USD</option><option>EUR</option><option>GBP</option><option>CAD</option>
</select>
</div>
</div>
<div class="settings-row">
<div class="form-group">
<label class="form-label">Invoice Number Prefix</label>
<input class="form-input" id="cfg-inv-prefix" placeholder="INV-" value="INV-">
</div>
<div class="form-group">
<label class="form-label">Default Tax Rate (%)</label>
<input class="form-input" id="cfg-tax-rate" placeholder="0" value="0" type="number">
</div>
</div>
<button class="btn primary" onclick="saveCompanyConfig()">Save Settings</button>
</div>
</div>
</div>
</div><!-- /content -->
</div><!-- /main -->
<!-- ===================== MODALS ===================== -->
<!-- Add Manual PO Modal -->
<div class="modal-overlay" id="addPOModal">
<div class="modal">
<div class="modal-header">
<span class="modal-title">Add Purchase Order</span>
<button class="modal-close" onclick="closeModal('addPOModal')">×</button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">PO Number *</label>
<input class="form-input" id="add-po-number" placeholder="PO-001">
</div>
<div class="form-group">
<label class="form-label">Vendor Name *</label>
<input class="form-input" id="add-po-vendor" placeholder="Supplier Inc.">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Total Amount *</label>
<input class="form-input" id="add-po-amount" type="number" placeholder="0.00">
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-select" id="add-po-status">
<option value="open">Open</option>
<option value="pending">Pending</option>
<option value="closed">Closed</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Issue Date</label>
<input class="form-input" id="add-po-issue" type="date">
</div>
<div class="form-group">
<label class="form-label">Due Date</label>
<input class="form-input" id="add-po-due" type="date">
</div>
</div>
<div class="form-row full">
<div class="form-group">
<label class="form-label">Description / Notes</label>
<textarea class="form-textarea" id="add-po-notes" placeholder="Optional notes..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('addPOModal')">Cancel</button>
<button class="btn primary" onclick="addManualPO()">Add PO</button>
</div>
</div>
</div>
<!-- Edit PO Modal -->
<div class="modal-overlay" id="editPOModal">
<div class="modal">
<div class="modal-header">
<span class="modal-title">Edit Purchase Order</span>
<button class="modal-close" onclick="closeModal('editPOModal')">×</button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-po-id">
<div class="form-row">
<div class="form-group">
<label class="form-label">PO Number *</label>
<input class="form-input" id="edit-po-number" placeholder="PO-001">
</div>
<div class="form-group">
<label class="form-label">Vendor Name *</label>
<input class="form-input" id="edit-po-vendor" placeholder="Supplier Inc.">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Total Amount *</label>
<input class="form-input" id="edit-po-amount" type="number" placeholder="0.00">
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-select" id="edit-po-status">
<option value="open">Open</option>
<option value="pending">Pending</option>
<option value="closed">Closed</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Issue Date</label>
<input class="form-input" id="edit-po-issue" type="date">
</div>
<div class="form-group">
<label class="form-label">Due Date</label>
<input class="form-input" id="edit-po-due" type="date">
</div>
</div>
<div class="form-row full">
<div class="form-group">
<label class="form-label">Description / Notes</label>
<textarea class="form-textarea" id="edit-po-notes" placeholder="Optional notes..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('editPOModal')">Cancel</button>
<button class="btn primary" onclick="saveEditPO()">Save Changes</button>
</div>
</div>
</div>
<!-- New / Edit Invoice Modal -->
<div class="modal-overlay" id="newInvoiceModal">
<div class="modal" style="width:660px">
<div class="modal-header">
<span class="modal-title" id="inv-modal-title">Create Invoice</span>
<button class="modal-close" onclick="closeModal('newInvoiceModal')">×</button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">Invoice Number</label>
<input class="form-input" id="inv-number" placeholder="INV-0001">
</div>
<div class="form-group">
<label class="form-label">Bill Against PO</label>
<select class="form-select" id="inv-po-ref" onchange="fillPOData(this.value)">
<option value="">— Select PO (optional) —</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Vendor / Bill To *</label>
<input class="form-input" id="inv-vendor" placeholder="Vendor name">
</div>
<div class="form-group">
<label class="form-label">Issue Date</label>
<input class="form-input" id="inv-issue" type="date">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Due Date</label>
<input class="form-input" id="inv-due" type="date">
</div>
<div class="form-group">
<label class="form-label">Payment Terms</label>
<select class="form-select" id="inv-terms" onchange="updateDueDate()">
<option>Net 30</option><option>Net 15</option><option>Net 45</option><option>Net 60</option><option>90 NET</option><option>Due on Receipt</option>
</select>
</div>
</div>
<!-- Line Items -->
<div style="margin-top:6px;margin-bottom:4px;font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:var(--text3)">Line Items</div>
<table class="line-items-table" id="line-items-table">
<thead><tr><th style="width:40%">Description</th><th>Qty</th><th>Unit Price</th><th>Total</th><th></th></tr></thead>
<tbody id="line-items-body"></tbody>
</table>
<button class="btn" style="margin-top:8px;width:100%;font-size:11px" onclick="addLineItem()">+ Add Line Item</button>
<div class="totals-box" id="invoice-totals">
<div class="totals-row"><span>Subtotal</span><span id="inv-subtotal">$0.00</span></div>
<div class="totals-row"><span>Tax (<span id="inv-tax-rate-label">0</span>%)</span><span id="inv-tax">$0.00</span></div>
<div class="totals-row total"><span>Total</span><span id="inv-total">$0.00</span></div>
<div class="totals-row" id="po-remaining-row" style="display:none;border-top:1px solid var(--border);padding-top:6px;margin-top:4px">
<span>PO Remaining</span><span id="inv-po-remaining" style="font-weight:500">—</span>
</div>
</div>
<div class="form-row full" style="margin-top:14px">
<div class="form-group">
<label class="form-label">Notes</label>
<textarea class="form-textarea" id="inv-notes" placeholder="Payment instructions, remittance info..."></textarea>
</div>
</div>
</div>
<div class="modal-footer" id="inv-modal-footer">
<button class="btn" onclick="closeModal('newInvoiceModal')">Cancel</button>
<button class="btn" onclick="saveInvoice('draft')">Save Draft</button>
<button class="btn primary" onclick="saveInvoice('pending')">Create & Queue for EDI</button>
</div>
</div>
</div>
<!-- View Invoice Modal -->
<div class="modal-overlay" id="viewInvoiceModal">
<div class="modal">
<div class="modal-header">
<span class="modal-title" id="view-inv-title">Invoice Details</span>
<button class="modal-close" onclick="closeModal('viewInvoiceModal')">×</button>
</div>
<div class="modal-body" id="view-inv-body"></div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('viewInvoiceModal')">Close</button>
<button class="btn" id="view-inv-delete-btn" style="color:var(--red)">Delete</button>
<button class="btn" id="view-inv-edit-btn">Edit</button>
<button class="btn" id="view-inv-edi-btn">⚡ Submit via EDI</button>
<button class="btn primary" id="view-inv-paid-btn">✓ Mark as Paid</button>
</div>
</div>
</div>
<!-- Bulk Delete Confirmation Modal -->
<div class="modal-overlay" id="bulkDeleteModal">
<div class="modal" style="width:440px">
<div class="modal-header">
<span class="modal-title">Confirm Deletion</span>
<button class="modal-close" onclick="closeModal('bulkDeleteModal')">×</button>
</div>
<div class="modal-body">
<p style="color:var(--text);margin-bottom:12px">Are you sure you want to delete the following <strong id="bulk-del-count">0</strong> invoice(s)? This cannot be undone.</p>
<div id="bulk-del-list" style="max-height:200px;overflow-y:auto;background:var(--surface2);border-radius:6px;padding:8px 12px;font-size:12px;color:var(--text2)"></div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('bulkDeleteModal')">Cancel</button>
<button class="btn" style="background:var(--red);color:#fff;border-color:var(--red)" onclick="executeBulkDelete()">Delete</button>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast"><span class="t-icon" id="toast-icon">✓</span><span id="toast-msg"></span></div>
<script>
// ============================================================
// API LAYER
// ============================================================
const API_URL = window.LEDGERFLOW_API_URL || "";
function getToken() {
return sessionStorage.getItem("ledgerflow_token");
}
async function logout() {
const token = getToken();
if (token) {
await fetch(`${API_URL}/auth/logout`, { method: "POST", headers: { Authorization: `Bearer ${token}` } }).catch(() => {});
}
sessionStorage.clear();
window.location.href = "login.html";
}
async function api(path, opts = {}) {
const token = getToken();
if (!token) { toast('Not authenticated — please log in', 'err'); throw new Error("Not authenticated"); }
const res = await fetch(`${API_URL}${path}`, {
...opts,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...(opts.headers || {}),
},
body: opts.body ? (typeof opts.body === "string" ? opts.body : JSON.stringify(opts.body)) : undefined,
});
if (res.status === 401) { toast('Session expired — please log in again', 'err'); return null; }
if (res.status === 204) return null;
const data = await res.json();
if (!res.ok) { toast(data.message || data.error || 'API error', 'err'); return null; }
return data;
}
// ============================================================
// STATE
// ============================================================
let state = {
pos: [],
invoices: [],
ediTransactions: [],
config: {
dynamo: { region: 'us-east-1', table: '', key: '', secret: '', fields: { poNumber:'po_number', vendor:'vendor_name', amount:'total_amount', status:'status', issueDate:'issue_date', dueDate:'due_date' }, connected: false },
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 }
},
invCounter: 1,
poPage: 1,
poPerPage: 20
};
// Load settings from backend
async function loadSettings() {
const data = await api('/settings');
if (data) {
Object.assign(state.config, data.config || {});
state.invCounter = data.invCounter || 1;
}
}
// Persist settings to backend (debounced to avoid rapid-fire writes)
let _saveTimer = null;
function save() {
if (!getToken()) return;
clearTimeout(_saveTimer);
_saveTimer = setTimeout(async () => {
try {
await api('/settings', { method: 'PUT', body: { config: state.config, invCounter: state.invCounter } });
} catch (e) { console.error('Failed to save settings', e); }
}, 500);
}
// ============================================================
// NAVIGATION
// ============================================================
function navigate(view, el) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('view-' + view).classList.add('active');
if (el) el.classList.add('active');
const titles = { dashboard:'Dashboard', pos:'Purchase Orders', invoices:'Invoices', billing:'Billing', edi:'EDI Console', settings:'Connections & Settings' };
const actions = {
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: '',
settings: ''
};
document.getElementById('page-title').textContent = titles[view] || view;
document.getElementById('topbar-actions').innerHTML = actions[view] || '';
if (view === 'settings') loadSettingsForm();
if (view === 'edi') refreshEDIInvoiceSelect();
if (view === 'billing') refreshBillingTable();
refreshDashboard();
}
// ============================================================
// MODALS
// ============================================================
function openModal(id) {
document.getElementById(id).classList.add('open');
if (id === 'newInvoiceModal' && !editingInvoiceId) initNewInvoiceModal();
}
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
document.querySelectorAll('.modal-overlay').forEach(o => {
o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); });
});
// ============================================================
// TOAST
// ============================================================
let toastTimer;
function toast(msg, type = 'ok') {
const el = document.getElementById('toast');
const icons = { ok:'✓', err:'✗', info:'ℹ', warn:'⚠' };
const colors = { ok:'var(--accent)', err:'var(--red)', info:'var(--accent2)', warn:'var(--yellow)' };
document.getElementById('toast-icon').textContent = icons[type];
document.getElementById('toast-icon').style.color = colors[type];
document.getElementById('toast-msg').textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 3200);
}
// ============================================================
// SETTINGS
// ============================================================
function loadSettingsForm() {
const c = state.config;
// DynamoDB
if (c.dynamo.region) document.getElementById('cfg-dynamo-region').value = c.dynamo.region;
if (c.dynamo.table) document.getElementById('cfg-dynamo-table').value = c.dynamo.table;
if (c.dynamo.key) document.getElementById('cfg-dynamo-key').value = c.dynamo.key;
if (c.dynamo.secret) document.getElementById('cfg-dynamo-secret').value = c.dynamo.secret;
if (c.dynamo.fields) {
if (c.dynamo.fields.poNumber) document.getElementById('cfg-dynamo-po-field').value = c.dynamo.fields.poNumber;
if (c.dynamo.fields.vendor) document.getElementById('cfg-dynamo-vendor-field').value = c.dynamo.fields.vendor;
if (c.dynamo.fields.amount) document.getElementById('cfg-dynamo-amount-field').value = c.dynamo.fields.amount;
if (c.dynamo.fields.status) document.getElementById('cfg-dynamo-status-field').value = c.dynamo.fields.status;
}
// EDI
if (c.edi.region) document.getElementById('cfg-edi-region').value = c.edi.region;
if (c.edi.partnershipId) document.getElementById('cfg-edi-partnership').value = c.edi.partnershipId;
if (c.edi.key) document.getElementById('cfg-edi-key').value = c.edi.key;
if (c.edi.secret) document.getElementById('cfg-edi-secret').value = c.edi.secret;
if (c.edi.senderId) document.getElementById('cfg-edi-sender').value = c.edi.senderId;
if (c.edi.receiverId) document.getElementById('cfg-edi-receiver').value = c.edi.receiverId;
if (c.edi.transformerId) document.getElementById('cfg-edi-transformer').value = c.edi.transformerId;
// Company
if (c.company.name) document.getElementById('cfg-company-name').value = c.company.name;
if (c.company.taxId) document.getElementById('cfg-tax-id').value = c.company.taxId;
if (c.company.terms) document.getElementById('cfg-payment-terms').value = c.company.terms;
if (c.company.currency) document.getElementById('cfg-currency').value = c.company.currency;
if (c.company.invPrefix) document.getElementById('cfg-inv-prefix').value = c.company.invPrefix;
if (c.company.taxRate !== undefined) document.getElementById('cfg-tax-rate').value = c.company.taxRate;
updateConnectionBadges();
}
function saveDynamoConfig() {
state.config.dynamo.region = document.getElementById('cfg-dynamo-region').value;
state.config.dynamo.table = document.getElementById('cfg-dynamo-table').value;
state.config.dynamo.key = document.getElementById('cfg-dynamo-key').value;
state.config.dynamo.secret = document.getElementById('cfg-dynamo-secret').value;
state.config.dynamo.fields.poNumber = document.getElementById('cfg-dynamo-po-field').value;
state.config.dynamo.fields.vendor = document.getElementById('cfg-dynamo-vendor-field').value;
state.config.dynamo.fields.amount = document.getElementById('cfg-dynamo-amount-field').value;
state.config.dynamo.fields.status = document.getElementById('cfg-dynamo-status-field').value;
save(); toast('DynamoDB configuration saved', 'ok');
}
function saveEDIConfig() {
state.config.edi.region = document.getElementById('cfg-edi-region').value;
state.config.edi.partnershipId = document.getElementById('cfg-edi-partnership').value;
state.config.edi.key = document.getElementById('cfg-edi-key').value;
state.config.edi.secret = document.getElementById('cfg-edi-secret').value;
state.config.edi.senderId = document.getElementById('cfg-edi-sender').value;
state.config.edi.receiverId = document.getElementById('cfg-edi-receiver').value;
state.config.edi.transformerId = document.getElementById('cfg-edi-transformer').value;
save(); toast('EDI configuration saved', 'ok');
}
function saveCompanyConfig() {
state.config.company.name = document.getElementById('cfg-company-name').value;
state.config.company.taxId = document.getElementById('cfg-tax-id').value;
state.config.company.terms = document.getElementById('cfg-payment-terms').value;
state.config.company.currency = document.getElementById('cfg-currency').value;
state.config.company.invPrefix = document.getElementById('cfg-inv-prefix').value;
state.config.company.taxRate = parseFloat(document.getElementById('cfg-tax-rate').value) || 0;
save(); toast('Company settings saved', 'ok');
}
function testDynamoConnection() {
const region = document.getElementById('cfg-dynamo-region').value;
const table = document.getElementById('cfg-dynamo-table').value;
const key = document.getElementById('cfg-dynamo-key').value;
if (!table || !key) { toast('Please fill in Region, Table Name, and Access Key', 'warn'); return; }
toast('Testing DynamoDB connection...', 'info');
setTimeout(() => {
// In production: call AWS SDK / API Gateway endpoint
// Marking as connected for demo purposes if credentials are provided
state.config.dynamo.connected = true;
document.getElementById('dynamo-status-badge').innerHTML = '<span class="badge submitted"><span class="badge-dot"></span>Connected</span>';
updateConnectionBadges();
save();
toast(`Connected to DynamoDB (${region}/${table})`, 'ok');
}, 1200);
}
function testEDIConnection() {
const partnershipId = document.getElementById('cfg-edi-partnership').value;
const key = document.getElementById('cfg-edi-key').value;
if (!partnershipId || !key) { toast('Please fill in Partnership ID and Access Key', 'warn'); return; }
toast('Testing EDI connection...', 'info');
setTimeout(() => {
state.config.edi.connected = true;
document.getElementById('edi-status-badge').innerHTML = '<span class="badge submitted"><span class="badge-dot"></span>Connected</span>';
updateConnectionBadges();
save();
toast('Connected to AWS B2B EDI', 'ok');
}, 1400);
}
function updateConnectionBadges() {
const dd = document.getElementById('dynamo-dot');
const dl = document.getElementById('dynamo-label');
const ed = document.getElementById('edi-dot');
const el = document.getElementById('edi-label');
const db = document.getElementById('dynamo-badge');
const eb = document.getElementById('edi-badge');
if (state.config.dynamo.connected) {
dd.style.background = 'var(--accent)';
dd.style.boxShadow = '0 0 6px var(--accent)';
dl.textContent = state.config.dynamo.table || 'DynamoDB';
db.classList.add('connected');
}
if (state.config.edi.connected) {
ed.style.background = 'var(--accent)';
ed.style.boxShadow = '0 0 6px var(--accent)';
el.textContent = 'EDI Connected';
eb.classList.add('connected');
}
}
// ============================================================
// PURCHASE ORDERS
// ============================================================
async function addManualPO() {
const poNumber = document.getElementById('add-po-number').value.trim();
const vendor = document.getElementById('add-po-vendor').value.trim();
const amount = parseFloat(document.getElementById('add-po-amount').value) || 0;
if (!poNumber || !vendor) { toast('PO Number and Vendor are required', 'warn'); return; }
const data = await api('/pos', {
method: 'POST',
body: {
poNumber, vendor, amount,
status: document.getElementById('add-po-status').value,
issueDate: document.getElementById('add-po-issue').value,
dueDate: document.getElementById('add-po-due').value,
notes: document.getElementById('add-po-notes').value,
},
});
if (!data) return;
state.pos.push(data);
closeModal('addPOModal');
refreshPOTable(); refreshDashboard();
addActivity(`Added manual PO ${poNumber}`, 'blue');
toast(`PO ${poNumber} added`, 'ok');
['add-po-number','add-po-vendor','add-po-amount','add-po-notes'].forEach(id => document.getElementById(id).value = '');
}
function openEditPO(poId) {
const po = state.pos.find(p => p.id === poId);
if (!po) return;
document.getElementById('edit-po-id').value = po.id;
document.getElementById('edit-po-number').value = po.poNumber || '';
document.getElementById('edit-po-vendor').value = po.vendor || '';
document.getElementById('edit-po-amount').value = po.amount || '';
document.getElementById('edit-po-status').value = po.status || 'open';
document.getElementById('edit-po-issue').value = po.issueDate || '';
document.getElementById('edit-po-due').value = po.dueDate || '';
document.getElementById('edit-po-notes').value = po.notes || '';
openModal('editPOModal');
}
async function saveEditPO() {
const id = document.getElementById('edit-po-id').value;
const poNumber = document.getElementById('edit-po-number').value.trim();
const vendor = document.getElementById('edit-po-vendor').value.trim();
if (!poNumber || !vendor) { toast('PO Number and Vendor are required', 'warn'); return; }
const body = {
poNumber, vendor,
amount: parseFloat(document.getElementById('edit-po-amount').value) || 0,
status: document.getElementById('edit-po-status').value,
issueDate: document.getElementById('edit-po-issue').value,
dueDate: document.getElementById('edit-po-due').value,
notes: document.getElementById('edit-po-notes').value,
};
const data = await api(`/pos/${id}`, { method: 'PUT', body });
if (!data) return;
const idx = state.pos.findIndex(p => p.id === id);
if (idx !== -1) state.pos[idx] = data;
closeModal('editPOModal');
refreshPOTable(); refreshDashboard();
toast(`PO ${poNumber} updated`, 'ok');
}
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 (!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;
}
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);
return `<tr>
<td class="mono">${po.poNumber}</td>
<td class="bold">${po.vendor}</td>
<td>${po.issueDate || '—'}</td>
<td>${po.dueDate || '—'}</td>
<td class="mono">${fmt(po.amount)}</td>
<td class="mono" style="color:var(--accent3)">${fmt(po.billed||0)}</td>
<td class="mono" style="color:var(--accent)">${fmt(remaining)}</td>
<td><span class="badge ${po.status}"><span class="badge-dot"></span>${po.status}</span></td>
<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' : ''}>&laquo;</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' : ''}>&raquo;</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) {
const po = state.pos.find(p => p.id === poId);
if (!po) return;
openModal('newInvoiceModal');
setTimeout(() => {
document.getElementById('inv-po-ref').value = po.id;
fillPOData(po.id);
}, 100);
}
// ============================================================
// INVOICES
// ============================================================
function termsToDays(terms) {
if (!terms) return 30;
const m = terms.match(/(\d+)/);
if (m) return parseInt(m[1], 10);
if (terms.toLowerCase().includes('receipt')) return 0;
return 30;
}
function updateDueDate() {
const terms = document.getElementById('inv-terms').value;
const issueDate = document.getElementById('inv-issue').value;
const base = issueDate ? new Date(issueDate + 'T00:00:00') : new Date();
base.setDate(base.getDate() + termsToDays(terms));
document.getElementById('inv-due').value = base.toISOString().split('T')[0];
}
function initNewInvoiceModal() {
editingInvoiceId = null;
// Reset modal to create mode
document.getElementById('inv-modal-title').textContent = 'Create Invoice';
document.getElementById('inv-modal-footer').innerHTML = `
<button class="btn" onclick="closeModal('newInvoiceModal')">Cancel</button>
<button class="btn" onclick="saveInvoice('draft')">Save Draft</button>
<button class="btn primary" onclick="saveInvoice('pending')">Create & Queue for EDI</button>
`;
document.getElementById('inv-number').disabled = false;
document.getElementById('inv-po-ref').disabled = false;
const prefix = state.config.company.invPrefix || 'INV-';
document.getElementById('inv-number').value = prefix + String(state.invCounter).padStart(4, '0');
document.getElementById('inv-issue').value = today();
document.getElementById('inv-notes').value = '';
updateDueDate();
document.getElementById('inv-tax-rate-label').textContent = state.config.company.taxRate || 0;
const sel = document.getElementById('inv-po-ref');
sel.innerHTML = '<option value="">— Select PO (optional) —</option>';
state.pos.filter(p => p.status !== 'closed' && p.status !== 'cancelled').forEach(po => {
const opt = document.createElement('option');
opt.value = po.id;
opt.textContent = `${po.poNumber} — ${po.vendor} (${fmt(po.amount - (po.billed||0))} remaining)`;
sel.appendChild(opt);
});
// Reset line items
document.getElementById('line-items-body').innerHTML = '';
addLineItem();
calcTotals();
}
function fillPOData(poId) {
const row = document.getElementById('po-remaining-row');
if (!poId) { row.style.display = 'none'; return; }
const po = state.pos.find(p => p.id === poId);
if (!po) { row.style.display = 'none'; return; }
document.getElementById('inv-vendor').value = po.vendor;
row.style.display = '';
calcTotals();
}
let lineItemCounter = 0;
function addLineItem() {
lineItemCounter++;
const id = 'li-' + lineItemCounter;
const row = document.createElement('tr');
row.id = id;
row.innerHTML = `
<td><input placeholder="Description" oninput="calcTotals()"></td>
<td style="width:70px"><input type="number" value="1" min="0" oninput="calcTotals()" style="width:60px"></td>
<td style="width:100px"><input type="number" value="0" min="0" step="0.01" oninput="calcTotals()" style="width:90px"></td>
<td style="width:90px;color:var(--text);padding:6px 8px" class="li-total">$0.00</td>
<td><button class="li-del" onclick="removeLineItem('${id}')">×</button></td>`;
document.getElementById('line-items-body').appendChild(row);
}
function removeLineItem(id) {
document.getElementById(id)?.remove();
calcTotals();
}
function calcTotals() {
let subtotal = 0;
document.querySelectorAll('#line-items-body tr').forEach(row => {
const inputs = row.querySelectorAll('input');
const qty = parseFloat(inputs[1]?.value) || 0;
const price = parseFloat(inputs[2]?.value) || 0;
const total = qty * price;
row.querySelector('.li-total').textContent = fmt(total);
subtotal += total;
});
const taxRate = state.config.company.taxRate || 0;
const tax = subtotal * (taxRate / 100);
const total = subtotal + tax;
document.getElementById('inv-subtotal').textContent = fmt(subtotal);
document.getElementById('inv-tax').textContent = fmt(tax);
document.getElementById('inv-total').textContent = fmt(total);
// Update PO remaining if a PO is selected
const poId = document.getElementById('inv-po-ref')?.value;
const remainEl = document.getElementById('inv-po-remaining');
if (poId) {
const po = state.pos.find(p => p.id === poId);
if (po) {
// When editing, the PO's billed amount already includes this invoice — add it back
const editingInv = editingInvoiceId ? state.invoices.find(i => i.id === editingInvoiceId) : null;
const editingInvTotal = editingInv ? parseFloat(editingInv.total || 0) : 0;
const poRemaining = parseFloat(po.amount || 0) - parseFloat(po.billed || 0) + editingInvTotal - total;
remainEl.textContent = fmt(poRemaining);
remainEl.style.color = poRemaining < 0 ? 'var(--red, #e74c3c)' : 'var(--accent)';
}
}
return { subtotal, tax, total };
}
function getLineItems() {
const items = [];
document.querySelectorAll('#line-items-body tr').forEach(row => {
const inputs = row.querySelectorAll('input');
const desc = inputs[0]?.value;
const qty = parseFloat(inputs[1]?.value) || 0;
const price = parseFloat(inputs[2]?.value) || 0;
if (desc || qty) items.push({ description: desc, qty, unitPrice: price, total: qty * price });
});
return items;
}
async function saveInvoice(status) {
const number = document.getElementById('inv-number').value.trim();
const vendor = document.getElementById('inv-vendor').value.trim();
if (!number || !vendor) { toast('Invoice number and vendor are required', 'warn'); return; }
const totals = calcTotals();
const poRef = document.getElementById('inv-po-ref').value;
const poObj = state.pos.find(p => p.id === poRef);
const body = {
number,
vendor,
poId: poRef || undefined,
poNumber: poObj?.poNumber || undefined,
issueDate: document.getElementById('inv-issue').value,
dueDate: document.getElementById('inv-due').value,
terms: document.getElementById('inv-terms').value,
lineItems: getLineItems(),
taxRate: state.config.company.taxRate || 0,
notes: document.getElementById('inv-notes').value,
status,
};
const data = await api('/invoices', { method: 'POST', body });
if (!data) return;
state.invoices.push(data);
state.invCounter++;
save();
// Refresh POs from backend (billed amount updated server-side)
if (poRef) await loadPOs();
closeModal('newInvoiceModal');
refreshInvoiceTable();
refreshPOTable();
refreshDashboard();
addActivity(`Invoice ${number} created (${fmt(data.total)})`, 'blue');
toast(`Invoice ${number} created`, 'ok');
}
function refreshInvoiceTable() {
const tbody = document.getElementById('inv-table-body');
document.getElementById('inv-count').textContent = state.invoices.length;
document.getElementById('inv-select-all').checked = false;
document.getElementById('inv-bulk-delete-btn').style.display = 'none';
if (!state.invoices.length) {
tbody.innerHTML = '<tr><td colspan="10"><div class="empty-state"><div class="es-icon">🧾</div><p>No invoices yet</p></div></td></tr>';
return;
}
const canSelect = inv => inv.status !== 'paid' && inv.ediStatus !== 'submitted';
tbody.innerHTML = state.invoices.map(inv => `
<tr onclick="viewInvoice('${inv.id}')">
<td onclick="event.stopPropagation()">
${canSelect(inv) ? `<input type="checkbox" class="inv-select-cb" data-id="${inv.id}" onchange="updateBulkDeleteBtn()">` : ''}
</td>
<td class="mono">${inv.number}</td>
<td class="mono">${inv.poNumber || '—'}</td>
<td class="bold">${inv.vendor}</td>
<td>${inv.issueDate || '—'}</td>
<td>${inv.dueDate || '—'}</td>
<td class="mono" style="color:var(--text)">${fmt(inv.total)}</td>
<td><span class="badge ${ediStatusClass(inv.ediStatus)}">${ediStatusLabel(inv.ediStatus)}</span></td>
<td><span class="badge ${inv.status}"><span class="badge-dot"></span>${inv.status}</span></td>
<td onclick="event.stopPropagation()">
${inv.status !== 'paid' ? `<button class="btn" style="padding:3px 9px;font-size:11px;margin-right:4px" onclick="editInvoice('${inv.id}')">Edit</button>` : ''}
<button class="btn" style="padding:3px 9px;font-size:11px;margin-right:4px" onclick="submitInvoiceEDI('${inv.id}')">⚡ EDI</button>
${inv.status !== 'paid' ? `<button class="btn" style="padding:3px 9px;font-size:11px" onclick="markPaid('${inv.id}')">✓ Paid</button>` : ''}
</td>
</tr>`).join('');
}
function viewInvoice(id) {
const inv = state.invoices.find(i => i.id === id);
if (!inv) return;
document.getElementById('view-inv-title').textContent = `Invoice ${inv.number}`;
document.getElementById('view-inv-body').innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:16px">
<div><div class="form-label">Vendor</div><div style="color:var(--text);margin-top:4px;font-weight:500">${inv.vendor}</div></div>
<div><div class="form-label">PO Reference</div><div style="color:var(--text);margin-top:4px">${inv.poNumber || '—'}</div></div>
<div><div class="form-label">Issue Date</div><div style="color:var(--text);margin-top:4px">${inv.issueDate}</div></div>
<div><div class="form-label">Due Date</div><div style="color:var(--text);margin-top:4px">${inv.dueDate}</div></div>
<div><div class="form-label">Status</div><div style="margin-top:4px"><span class="badge ${inv.status}">${inv.status}</span></div></div>
<div><div class="form-label">EDI Status</div><div style="margin-top:4px"><span class="badge ${ediStatusClass(inv.ediStatus)}">${ediStatusLabel(inv.ediStatus)}</span></div></div>
</div>
<table class="line-items-table" style="margin-bottom:12px">
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Total</th></tr></thead>
<tbody>${inv.lineItems.map(li => `<tr><td style="padding:7px 8px;color:var(--text2)">${li.description}</td><td style="padding:7px 8px;color:var(--text2)">${li.qty}</td><td style="padding:7px 8px;color:var(--text2)">${fmt(li.unitPrice)}</td><td style="padding:7px 8px;color:var(--text)">${fmt(li.total)}</td></tr>`).join('')}</tbody>
</table>
<div class="totals-box">
<div class="totals-row"><span>Subtotal</span><span>${fmt(inv.subtotal)}</span></div>
<div class="totals-row"><span>Tax</span><span>${fmt(inv.tax)}</span></div>
<div class="totals-row total"><span>Total</span><span>${fmt(inv.total)}</span></div>
</div>
${inv.notes ? `<div style="margin-top:12px;padding:10px 12px;background:var(--surface2);border-radius:6px;font-size:12px;color:var(--text2)">${inv.notes}</div>` : ''}
`;
document.getElementById('view-inv-edi-btn').onclick = () => { closeModal('viewInvoiceModal'); submitInvoiceEDI(id); };
document.getElementById('view-inv-paid-btn').onclick = () => { markPaid(id); closeModal('viewInvoiceModal'); };
// Edit button — hidden for paid invoices
const editBtn = document.getElementById('view-inv-edit-btn');
editBtn.style.display = inv.status === 'paid' ? 'none' : '';
editBtn.onclick = () => { closeModal('viewInvoiceModal'); editInvoice(id); };
// Delete button — hidden for paid invoices
const deleteBtn = document.getElementById('view-inv-delete-btn');
deleteBtn.style.display = inv.status === 'paid' ? 'none' : '';
deleteBtn.onclick = () => { deleteInvoice(id); };
openModal('viewInvoiceModal');
}
let editingInvoiceId = null;
function editInvoice(id) {
const inv = state.invoices.find(i => i.id === id);
if (!inv) return;
editingInvoiceId = id;
// Set modal to edit mode
document.getElementById('inv-modal-title').textContent = `Edit Invoice ${inv.number}`;
document.getElementById('inv-modal-footer').innerHTML = `
<button class="btn" onclick="closeModal('newInvoiceModal'); editingInvoiceId = null;">Cancel</button>
<button class="btn primary" onclick="saveInvoiceEdit()">Save Changes</button>
`;
// Populate fields
document.getElementById('inv-number').value = inv.number;
document.getElementById('inv-number').disabled = true;
document.getElementById('inv-vendor').value = inv.vendor;
document.getElementById('inv-issue').value = inv.issueDate || '';
document.getElementById('inv-due').value = inv.dueDate || '';
document.getElementById('inv-terms').value = inv.terms || 'Net 30';
document.getElementById('inv-notes').value = inv.notes || '';
document.getElementById('inv-tax-rate-label').textContent = state.config.company.taxRate || 0;
// Populate PO ref
const sel = document.getElementById('inv-po-ref');
sel.innerHTML = '<option value="">— Select PO (optional) —</option>';
state.pos.filter(p => p.status !== 'closed' && p.status !== 'cancelled').forEach(po => {
const opt = document.createElement('option');
opt.value = po.id;
opt.textContent = `${po.poNumber} — ${po.vendor} (${fmt(po.amount - (po.billed||0))} remaining)`;
sel.appendChild(opt);
});
sel.value = inv.poId || '';
sel.disabled = true; // PO linkage shouldn't change on edit
// Populate line items
document.getElementById('line-items-body').innerHTML = '';
inv.lineItems.forEach(li => {
lineItemCounter++;
const rowId = 'li-' + lineItemCounter;
const row = document.createElement('tr');
row.id = rowId;
row.innerHTML = `
<td><input placeholder="Description" oninput="calcTotals()" value="${li.description}"></td>
<td style="width:70px"><input type="number" value="${li.qty}" min="0" oninput="calcTotals()" style="width:60px"></td>
<td style="width:100px"><input type="number" value="${li.unitPrice}" min="0" step="0.01" oninput="calcTotals()" style="width:90px"></td>
<td style="width:90px;color:var(--text);padding:6px 8px" class="li-total">${fmt(li.total)}</td>
<td><button class="li-del" onclick="removeLineItem('${rowId}')">×</button></td>`;
document.getElementById('line-items-body').appendChild(row);
});
if (inv.poId) {
document.getElementById('po-remaining-row').style.display = '';
} else {
document.getElementById('po-remaining-row').style.display = 'none';
}
calcTotals();
openModal('newInvoiceModal');
}
async function saveInvoiceEdit() {
if (!editingInvoiceId) return;
const vendor = document.getElementById('inv-vendor').value.trim();
if (!vendor) { toast('Vendor is required', 'warn'); return; }
const body = {
vendor,
issueDate: document.getElementById('inv-issue').value,
dueDate: document.getElementById('inv-due').value,
terms: document.getElementById('inv-terms').value,
lineItems: getLineItems(),
taxRate: state.config.company.taxRate || 0,
notes: document.getElementById('inv-notes').value,
};
const data = await api(`/invoices/${editingInvoiceId}`, { method: 'PUT', body });
if (!data) return;
// Update local state
const idx = state.invoices.findIndex(i => i.id === editingInvoiceId);
if (idx !== -1) state.invoices[idx] = data;
save();
// Refresh POs if linked
if (data.poId) await loadPOs();
closeModal('newInvoiceModal');
editingInvoiceId = null;
document.getElementById('inv-number').disabled = false;
document.getElementById('inv-po-ref').disabled = false;
refreshInvoiceTable();
refreshPOTable();
refreshDashboard();
addActivity(`Invoice ${data.number} updated`, 'blue');
toast(`Invoice ${data.number} updated`, 'ok');
}
async function deleteInvoice(id) {
const inv = state.invoices.find(i => i.id === id);
if (!inv) return;
if (!confirm(`Delete invoice ${inv.number}? This cannot be undone.`)) return;
const res = await fetch(`${API_URL}/invoices/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!res.ok && res.status !== 204) {
const err = await res.json().catch(() => ({}));
toast(err.message || err.error || 'Failed to delete invoice', 'err');
return;
}
state.invoices = state.invoices.filter(i => i.id !== id);
save();
if (inv.poId) await loadPOs();
closeModal('viewInvoiceModal');
refreshInvoiceTable();
refreshPOTable();
refreshDashboard();
addActivity(`Invoice ${inv.number} deleted`, 'red');
toast(`Invoice ${inv.number} deleted`, 'ok');
}
function toggleAllInvoices(checked) {
document.querySelectorAll('.inv-select-cb').forEach(cb => { cb.checked = checked; });
updateBulkDeleteBtn();
}
function updateBulkDeleteBtn() {
const selected = document.querySelectorAll('.inv-select-cb:checked');
const btn = document.getElementById('inv-bulk-delete-btn');
document.getElementById('inv-selected-count').textContent = selected.length;
btn.style.display = selected.length > 0 ? '' : 'none';
// Update select-all checkbox state
const all = document.querySelectorAll('.inv-select-cb');
document.getElementById('inv-select-all').checked = all.length > 0 && selected.length === all.length;
}
function confirmBulkDelete() {
const selectedIds = Array.from(document.querySelectorAll('.inv-select-cb:checked')).map(cb => cb.dataset.id);
if (!selectedIds.length) return;
const invoices = selectedIds.map(id => state.invoices.find(i => i.id === id)).filter(Boolean);
document.getElementById('bulk-del-count').textContent = invoices.length;
document.getElementById('bulk-del-list').innerHTML = invoices.map(inv =>
`<div style="padding:4px 0;border-bottom:1px solid var(--border)"><strong>${inv.number}</strong> — ${inv.vendor} (${fmt(inv.total)})</div>`
).join('');
openModal('bulkDeleteModal');
}
async function executeBulkDelete() {
const selectedIds = Array.from(document.querySelectorAll('.inv-select-cb:checked')).map(cb => cb.dataset.id);
if (!selectedIds.length) return;
closeModal('bulkDeleteModal');
let deleted = 0;
let hadPO = false;
for (const id of selectedIds) {
const inv = state.invoices.find(i => i.id === id);
if (!inv) continue;
const res = await fetch(`${API_URL}/invoices/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${getToken()}` },
});
if (res.ok || res.status === 204) {
if (inv.poId) hadPO = true;
state.invoices = state.invoices.filter(i => i.id !== id);
deleted++;
}
}
if (deleted) {
save();
if (hadPO) await loadPOs();
refreshInvoiceTable();
refreshPOTable();
refreshDashboard();
addActivity(`Deleted ${deleted} invoice(s)`, 'red');
toast(`Deleted ${deleted} invoice(s)`, 'ok');
}
}
async function markPaid(id) {
const inv = state.invoices.find(i => i.id === id);
if (!inv) return;
const data = await api(`/invoices/${id}/status`, {
method: 'PATCH',
body: { status: 'paid' },
});
if (!data) return;
// Update local state with server response
Object.assign(inv, data);
refreshInvoiceTable(); refreshDashboard(); refreshBillingTable();
addActivity(`Invoice ${inv.number} marked as paid`, 'green');
toast(`Invoice ${inv.number} marked as paid`, 'ok');
}
// ============================================================
// EDI
// ============================================================
function refreshEDIInvoiceSelect() {
const sel = document.getElementById('edi-invoice-select');
sel.innerHTML = '<option value="">— Select invoice —</option>';
state.invoices.filter(i => i.status !== 'paid' && i.ediStatus !== 'submitted').forEach(inv => {
const opt = document.createElement('option');
opt.value = inv.id;
opt.textContent = `${inv.number} — ${inv.vendor} (${fmt(inv.total)})`;
sel.appendChild(opt);
});
}
function submitInvoiceEDI(invId) {
const inv = state.invoices.find(i => i.id === invId);
if (!inv) return;
navigate('edi', document.querySelectorAll('.nav-item')[4]);
setTimeout(() => {
document.getElementById('edi-invoice-select').value = invId;
document.getElementById('edi-tx-type').value = '810';
}, 100);
}
async function downloadEDI() {
const txType = document.getElementById('edi-tx-type').value;
const invId = document.getElementById('edi-invoice-select').value;
if (!invId) { toast('Select an invoice first', 'warn'); return; }
const data = await api('/edi/preview', {
method: 'POST',
body: { invoiceId: invId, txType },
});
if (!data) return;
const blob = new Blob([data.ediDocument], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${data.invoiceNumber}_${txType}.txt`;
a.click();
URL.revokeObjectURL(url);
toast(`Downloaded ${data.invoiceNumber}_${txType}.txt`, 'ok');
}
async function submitEDI() {
const txType = document.getElementById('edi-tx-type').value;
const invId = document.getElementById('edi-invoice-select').value;
const partnerId = document.getElementById('edi-partner-id').value.trim() || undefined;
if (!invId) { toast('Select an invoice to submit', 'warn'); return; }
const inv = state.invoices.find(i => i.id === invId);
if (!inv) return;
const log = document.getElementById('edi-log');
const ts = new Date().toLocaleTimeString();
log.innerHTML += `<span class="info">[${ts}]</span> Submitting ${txType} transaction for ${inv.number}...<br>`;
log.scrollTop = log.scrollHeight;
const data = await api('/edi/submit', {
method: 'POST',
body: { invoiceId: invId, txType, partnerId },
});
if (!data) {
log.innerHTML += `<span class="err">[${ts}]</span> Submission failed. Check console for details.<br>`;
log.scrollTop = log.scrollHeight;
return;
}
log.innerHTML += `<span class="ok">[${ts}]</span> ISA control: ${data.isaControl}<br>`;
log.innerHTML += `<span class="ok">[${ts}]</span> ✓ Transmitted to AWS B2B EDI (Partnership: ${data.partnerId || partnerId || 'default'})<br>`;
log.scrollTop = log.scrollHeight;
// Update local invoice state
inv.ediStatus = 'submitted';
inv.ediISA = data.isaControl;
inv.ediSubmittedAt = data.submittedAt || new Date().toISOString();
// Reload EDI history from backend
await loadEDITransactions();
refreshInvoiceTable(); refreshDashboard();
refreshEDIHistory();
addActivity(`EDI ${txType} submitted for ${inv.number}`, 'orange');
toast(`${txType} submitted for ${inv.number}`, 'ok');
}
function refreshEDIHistory() {
const tbody = document.getElementById('edi-history-body');
if (!state.ediTransactions.length) {
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state" style="padding:20px"><div class="es-icon">⚡</div><p>No EDI transactions yet</p></div></td></tr>';
return;
}
// Backend uses createdAt/partnerId; legacy local data may use timestamp/partner
tbody.innerHTML = state.ediTransactions.map(tx => {
const date = tx.createdAt || tx.timestamp;
const partner = tx.partnerId || tx.partner || '—';
const statusClass = tx.status === 'failed' ? 'overdue' : tx.status === 'processing' ? 'pending' : 'submitted';
return `<tr>
<td class="mono" style="font-size:11px">${new Date(date).toLocaleString()}</td>
<td><span class="badge open">${tx.txType}</span></td>
<td class="mono">${tx.invoiceNumber}</td>
<td>${partner}</td>
<td class="mono" style="font-size:11px">${tx.isaControl}</td>
<td><span class="badge ${statusClass}"><span class="badge-dot"></span>${tx.status}</span></td>
</tr>`;
}).join('');
}
function clearEDILog() {
document.getElementById('edi-log').innerHTML = '<span class="info">[SYSTEM]</span> Log cleared.<br>';
}
// ============================================================
// BILLING
// ============================================================
function refreshBillingTable() {
const tbody = document.getElementById('billing-table-body');
if (!state.pos.length) {
tbody.innerHTML = '<tr><td colspan="6"><div class="empty-state"><div class="es-icon">💳</div><p>Import POs to view billing utilization</p></div></td></tr>';
document.getElementById('bill-total').textContent = '$0.00';
document.getElementById('bill-paid').textContent = '$0.00';
document.getElementById('bill-outstanding').textContent = '$0.00';
return;
}
let totalInvoiced = 0, totalPaid = 0;
state.invoices.forEach(inv => {
totalInvoiced += inv.total;
if (inv.status === 'paid') totalPaid += inv.total;
});
document.getElementById('bill-total').textContent = fmt(totalInvoiced);
document.getElementById('bill-paid').textContent = fmt(totalPaid);
document.getElementById('bill-outstanding').textContent = fmt(totalInvoiced - totalPaid);
tbody.innerHTML = state.pos.map(po => {
const billed = po.billed || 0;
const remaining = Math.max(0, po.amount - billed);
const pct = po.amount > 0 ? Math.min(100, Math.round(billed / po.amount * 100)) : 0;
return `<tr>
<td class="mono">${po.poNumber}</td>
<td class="bold">${po.vendor}</td>
<td class="mono">${fmt(po.amount)}</td>
<td class="mono" style="color:var(--accent3)">${fmt(billed)}</td>
<td class="mono" style="color:var(--accent)">${fmt(remaining)}</td>
<td style="min-width:140px">
<div style="display:flex;align-items:center;gap:8px">
<div class="progress-bar" style="flex:1"><div class="progress-fill" style="width:${pct}%"></div></div>
<span style="font-size:11px;color:var(--text2);min-width:32px">${pct}%</span>
</div>
</td>
</tr>`;
}).join('');
}
// ============================================================
// DASHBOARD
// ============================================================
function refreshDashboard() {
// Stats
const openPOs = state.pos.filter(p => p.status === 'open' || p.status === 'approved');
const totalPOVal = openPOs.reduce((s, p) => s + p.amount, 0);
const pendingInv = state.invoices.filter(i => i.status === 'pending');
const overdue = state.invoices.filter(i => i.status !== 'paid' && i.dueDate && i.dueDate < today());
const billed = state.invoices.reduce((s, i) => s + i.total, 0);
const ediToday = state.ediTransactions.filter(t => t.timestamp && t.timestamp.startsWith(today())).length;
document.getElementById('stat-pos').textContent = openPOs.length;
document.getElementById('stat-pos-val').textContent = fmt(totalPOVal);
document.getElementById('stat-pos-delta').textContent = `↑ ${state.pos.length} total`;
document.getElementById('stat-inv').textContent = pendingInv.length;
document.getElementById('stat-inv-delta').textContent = `● ${overdue.length} overdue`;
document.getElementById('stat-billed').textContent = fmt(billed);
document.getElementById('stat-edi').textContent = state.ediTransactions.length;
document.getElementById('stat-edi-delta').textContent = `● ${ediToday} today`;
// Dashboard PO table
const dashPO = document.getElementById('dashboard-po-table');
if (!state.pos.length) {
dashPO.innerHTML = '<tr><td colspan="4"><div class="empty-state" style="padding:16px"><div class="es-icon">📦</div><p>No POs yet</p></div></td></tr>';
} else {
dashPO.innerHTML = state.pos.slice(-5).reverse().map(po => `
<tr onclick="navigate('pos', document.querySelectorAll('.nav-item')[2])">
<td class="mono">${po.poNumber}</td>
<td class="bold">${po.vendor}</td>
<td class="mono">${fmt(po.amount)}</td>
<td><span class="badge ${po.status}"><span class="badge-dot"></span>${po.status}</span></td>
</tr>`).join('');
}
// Dashboard invoice table
const dashInv = document.getElementById('dashboard-inv-table');
if (!state.invoices.length) {
dashInv.innerHTML = '<tr><td colspan="6"><div class="empty-state" style="padding:16px"><div class="es-icon">🧾</div><p>No invoices yet</p></div></td></tr>';
} else {
dashInv.innerHTML = state.invoices.slice(-5).reverse().map(inv => `
<tr onclick="viewInvoice('${inv.id}')">
<td class="mono">${inv.number}</td>
<td class="mono">${inv.poNumber || '—'}</td>
<td class="bold">${inv.vendor}</td>
<td class="mono">${fmt(inv.total)}</td>
<td><span class="badge ${ediStatusClass(inv.ediStatus)}">${ediStatusLabel(inv.ediStatus)}</span></td>
<td><span class="badge ${inv.status}"><span class="badge-dot"></span>${inv.status}</span></td>
</tr>`).join('');
}
}
// ============================================================
// ACTIVITY
// ============================================================
let activities = [];
function addActivity(text, color = 'green') {
activities.unshift({ text, color, time: new Date().toLocaleTimeString() });
if (activities.length > 20) activities.pop();
const feed = document.getElementById('activity-feed');
feed.innerHTML = activities.slice(0,8).map(a =>
`<div class="activity-item">
<div class="activity-dot ${a.color === 'blue' ? 'blue' : a.color === 'orange' ? 'orange' : ''}"></div>
<div><div class="activity-text">${a.text}</div><div class="activity-time">${a.time}</div></div>
</div>`
).join('') || '<div class="empty-state" style="padding:12px"><div class="es-icon">📋</div><p>No activity yet</p></div>';
}
// ============================================================
// HELPERS
// ============================================================
function fmt(n) {
return '$' + (parseFloat(n)||0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function today() { return new Date().toISOString().split('T')[0]; }
function genId() { return Math.random().toString(36).slice(2, 10); }
function ediStatusClass(s) { return s === 'submitted' ? 'submitted' : s === 'modified_after_send' ? 'modified' : s === 'processing' ? 'pending' : 'open'; }
function ediStatusLabel(s) { return s === 'submitted' ? 'Submitted' : s === 'modified_after_send' ? 'Modified After Send' : s === 'processing' ? 'Processing' : 'Not Sent'; }
function filterTable(tableId, query) {
const q = query.toLowerCase();
document.querySelectorAll(`#${tableId} tbody tr`).forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(q) ? '' : 'none';
});
}
// ============================================================
// DATA LOADERS
// ============================================================
async function loadPOs() {
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() {
const data = await api('/invoices?limit=200');
if (data && data.items) state.invoices = data.items;
}
async function loadEDITransactions() {
const data = await api('/edi/transactions?limit=200');
if (data && data.items) state.ediTransactions = data.items;
}
// ============================================================
// INIT
// ============================================================
updateConnectionBadges();
(async function init() {
if (!API_URL || !getToken()) {
// No API configured or not logged in — render with empty/local state
refreshDashboard();
refreshPOTable();
refreshInvoiceTable();
refreshEDIHistory();
if (!getToken()) toast('Not logged in — data will not load from server', 'warn');
return;
}
try {
await loadSettings();
await Promise.all([loadPOs(), loadInvoices(), loadEDITransactions()]);
addActivity('Data loaded from server', 'green');
} catch (e) {
console.error('Init load error:', e);
toast('Failed to load data from server', 'err');
}
refreshDashboard();
refreshPOTable();
refreshInvoiceTable();
refreshEDIHistory();
refreshBillingTable();
})();
</script>
</body>
</html>