ledgerflow-frontend/accounting-app.html

2374 lines
96 KiB
HTML
Raw Normal View History

<!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>