ledgerflow-frontend/accounting-app.html
Adam Moussa 4fbd664d64 Initial commit — LedgerFlow frontend
Single-page B2B accounting app with PO management (import, edit),
invoice creation with auto due date calculation, EDI preview/submit,
Google SSO, and sign out support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 18:19:49 -04:00

2250 lines
91 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; }
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.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" onclick="openModal('importPOModal')">⬇ Import POs</button>
<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 imported 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">Imported from DynamoDB · <span id="po-count">0</span> records</p>
</div>
<div style="display:flex;gap:8px">
<button class="btn" onclick="openModal('importPOModal')">⬇ Import from DynamoDB</button>
<button class="btn" onclick="openModal('addPOModal')">+ Add Manual PO</button>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">All Purchase Orders</span>
<input class="form-input" style="width:200px" placeholder="🔍 Search POs..." oninput="filterTable('po-table', this.value)">
</div>
<div class="table-wrap">
<table id="po-table">
<thead><tr><th>PO Number</th><th>Vendor</th><th>Issue Date</th><th>Due Date</th><th>Amount</th><th>Billed</th><th>Remaining</th><th>Status</th><th>Action</th></tr></thead>
<tbody id="po-table-body">
<tr><td colspan="9"><div class="empty-state"><div class="es-icon">📦</div><p>No purchase orders yet. Import from DynamoDB or add manually.</p></div></td></tr>
</tbody>
</table>
</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>
<input class="form-input" style="width:200px" placeholder="🔍 Search..." oninput="filterTable('inv-table', this.value)">
</div>
<div class="table-wrap">
<table id="inv-table">
<thead><tr><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="9"><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 ===================== -->
<!-- Import PO Modal -->
<div class="modal-overlay" id="importPOModal">
<div class="modal">
<div class="modal-header">
<span class="modal-title">Import Purchase Orders from DynamoDB</span>
<button class="modal-close" onclick="closeModal('importPOModal')">×</button>
</div>
<div class="modal-body">
<div id="dynamo-import-state">
<div style="margin-bottom:16px;padding:12px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text2)">
<strong style="color:var(--text)">DynamoDB Configuration</strong><br>
Region: <span id="imp-region">—</span> · Table: <span id="imp-table">—</span>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Filter by Status</label>
<select class="form-select" id="imp-status-filter">
<option value="">All Statuses</option>
<option value="OPEN">Open</option>
<option value="PENDING">Pending</option>
<option value="APPROVED">Approved</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Limit Records</label>
<select class="form-select" id="imp-limit">
<option>25</option><option>50</option><option>100</option><option>250</option><option>All</option>
</select>
</div>
</div>
<button class="btn" style="width:100%;margin-bottom:12px" onclick="fetchFromDynamo()">🔄 Fetch from DynamoDB</button>
<div id="dynamo-preview" style="display:none">
<div style="font-size:11px;color:var(--text3);margin-bottom:8px">PREVIEW — <span id="preview-count">0</span> records fetched</div>
<div id="dynamo-preview-list"></div>
</div>
<div style="margin-top:14px;padding-top:14px;border-top:1px solid var(--border)">
<div style="font-size:11px;color:var(--text3);margin-bottom:8px">OR PASTE SAMPLE JSON (for demo / testing)</div>
<textarea class="form-textarea" id="dynamo-json-paste" placeholder='[{"po_number":"PO-001","vendor_name":"Acme Supplies","total_amount":15000,"status":"OPEN","issue_date":"2026-01-15","due_date":"2026-02-15"}]' style="height:90px;font-size:11px"></textarea>
<button class="btn" style="width:100%;margin-top:8px" onclick="importFromJSON()">⬇ Import from JSON</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('importPOModal')">Cancel</button>
<button class="btn primary" id="import-confirm-btn" onclick="confirmImport()">Import Selected</button>
</div>
</div>
</div>
<!-- Add Manual PO Modal -->
<div class="modal-overlay" id="addPOModal">
<div class="modal">
<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 Invoice Modal -->
<div class="modal-overlay" id="newInvoiceModal">
<div class="modal" style="width:660px">
<div class="modal-header">
<span class="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">
<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-edi-btn">⚡ Submit via EDI</button>
<button class="btn primary" id="view-inv-paid-btn">✓ Mark as Paid</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 }
},
pendingImport: [],
invCounter: 1
};
// 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" onclick="openModal(\'importPOModal\')">⬇ Import POs</button><button class="btn primary" onclick="openModal(\'newInvoiceModal\')">+ New Invoice</button>',
pos: '<button class="btn" onclick="openModal(\'importPOModal\')">⬇ Import from DynamoDB</button><button class="btn primary" onclick="openModal(\'addPOModal\')">+ Add PO</button>',
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') initNewInvoiceModal();
if (id === 'importPOModal') {
document.getElementById('imp-region').textContent = state.config.dynamo.region || '—';
document.getElementById('imp-table').textContent = state.config.dynamo.table || '—';
}
}
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
document.querySelectorAll('.modal-overlay').forEach(o => {
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 fetchFromDynamo() {
if (!state.config.dynamo.table) {
toast('Configure DynamoDB table name in Settings first', 'warn');
return;
}
toast('Fetching from DynamoDB...', 'info');
const params = new URLSearchParams({ limit: '200' });
const data = await api(`/pos/dynamo-scan?${params}`);
if (!data) return;
// Backend returns already-mapped items using the user's field config
state.pendingImport = (data.items || []).map(r => ({
poNumber: r.poNumber || '—',
vendor: r.vendor || '—',
amount: parseFloat(r.amount || 0),
status: (r.status || 'open').toLowerCase(),
issueDate: r.issueDate || '',
dueDate: r.dueDate || '',
notes: r._raw?.notes || r._raw?.description || ''
}));
document.getElementById('dynamo-preview').style.display = 'block';
document.getElementById('preview-count').textContent = state.pendingImport.length;
const list = document.getElementById('dynamo-preview-list');
if (!state.pendingImport.length) {
list.innerHTML = '<p style="font-size:11px;color:var(--text3)">No records found in table.</p>';
return;
}
list.innerHTML = state.pendingImport.map((po, i) =>
`<div class="po-match-bar selected" onclick="togglePendingPO(this,${i})" data-idx="${i}">
<div class="po-match-check">✓</div>
<div style="flex:1">
<span style="color:var(--text);font-weight:500">${po.poNumber}</span>
<span style="color:var(--text3);margin:0 8px">·</span>
<span>${po.vendor}</span>
</div>
<span style="color:var(--accent)">${fmt(po.amount)}</span>
<span class="badge ${po.status}" style="margin-left:8px">${po.status}</span>
</div>`
).join('');
toast(`${state.pendingImport.length} records fetched from DynamoDB`, 'ok');
}
// Unmarshall DynamoDB JSON ({S:"val"}, {N:"123"}, {NULL:true}, etc.) to plain values
function unmarshallValue(v) {
if (v === null || v === undefined) return null;
if (typeof v !== 'object') return v;
if ('S' in v) return v.S;
if ('N' in v) return parseFloat(v.N);
if ('BOOL' in v) return v.BOOL;
if ('NULL' in v) return null;
if ('L' in v) return v.L.map(unmarshallValue);
if ('M' in v) return unmarshallItem(v.M);
return v;
}
function unmarshallItem(item) {
const out = {};
for (const [k, v] of Object.entries(item)) out[k] = unmarshallValue(v);
return out;
}
function isDynamoJSON(obj) {
const vals = Object.values(obj);
if (vals.length === 0) return false;
return vals.some(v => v && typeof v === 'object' && !Array.isArray(v) && ('S' in v || 'N' in v || 'NULL' in v || 'BOOL' in v || 'M' in v || 'L' in v));
}
function importFromJSON() {
const raw = document.getElementById('dynamo-json-paste').value.trim();
if (!raw) { toast('Paste JSON data first', 'warn'); return; }
try {
let data = JSON.parse(raw);
if (!Array.isArray(data)) data = [data];
// Auto-detect and unmarshall DynamoDB JSON format
data = data.map(item => isDynamoJSON(item) ? unmarshallItem(item) : item);
const fields = state.config.dynamo.fields;
state.pendingImport = data.map(r => ({
poNumber: r[fields.poNumber] || r.po_number || r.poNumber || '—',
vendor: r[fields.vendor] || r.vendor_name || r.vendor || r.supplier?.name || '—',
amount: parseFloat(r[fields.amount] || r.total_amount || r.amount || 0),
status: (r[fields.status] || r.po_status || r.status || 'open').toLowerCase(),
issueDate: r[fields.issueDate] || r.issue_date || r.issueDate || r.order_date || '',
dueDate: r[fields.dueDate] || r.due_date || r.dueDate || '',
notes: r.notes || r.description || ''
}));
document.getElementById('dynamo-preview').style.display = 'block';
document.getElementById('preview-count').textContent = state.pendingImport.length;
const list = document.getElementById('dynamo-preview-list');
list.innerHTML = state.pendingImport.map((po, i) =>
`<div class="po-match-bar selected" onclick="togglePendingPO(this,${i})" data-idx="${i}">
<div class="po-match-check">✓</div>
<div style="flex:1">
<span style="color:var(--text);font-weight:500">${po.poNumber}</span>
<span style="color:var(--text3);margin:0 8px">·</span>
<span>${po.vendor}</span>
</div>
<span style="color:var(--accent)">${fmt(po.amount)}</span>
<span class="badge ${po.status}" style="margin-left:8px">${po.status}</span>
</div>`
).join('');
toast(`${state.pendingImport.length} records parsed`, 'ok');
} catch(e) {
toast('Invalid JSON: ' + e.message, 'err');
}
}
function togglePendingPO(el, idx) {
el.classList.toggle('selected');
}
async function confirmImport() {
const selected = document.querySelectorAll('#dynamo-preview-list .po-match-bar.selected');
if (selected.length === 0) { toast('Select at least one PO to import', 'warn'); return; }
const items = [];
selected.forEach(el => {
const idx = parseInt(el.dataset.idx);
items.push(state.pendingImport[idx]);
});
toast('Importing POs...', 'info');
const data = await api('/pos/import', {
method: 'POST',
body: { items, source: 'dynamodb' },
});
if (!data) return;
closeModal('importPOModal');
await loadPOs();
refreshPOTable();
refreshDashboard();
addActivity(`Imported ${data.imported} PO(s) from DynamoDB (${data.skipped} skipped)`, 'green');
toast(`Imported ${data.imported} PO(s) successfully`, 'ok');
state.pendingImport = [];
}
async function addManualPO() {
const poNumber = document.getElementById('add-po-number').value.trim();
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');
document.getElementById('po-count').textContent = state.pos.length;
if (!state.pos.length) {
tbody.innerHTML = '<tr><td colspan="9"><div class="empty-state"><div class="es-icon">📦</div><p>No POs yet</p></div></td></tr>';
return;
}
tbody.innerHTML = state.pos.map(po => {
const remaining = po.amount - (po.billed || 0);
const pct = po.amount > 0 ? Math.round((po.billed||0)/po.amount*100) : 0;
return `<tr>
<td class="mono">${po.poNumber}</td>
<td class="bold">${po.vendor}</td>
<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('');
}
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() {
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();
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) {
const poRemaining = po.amount - (po.billed || 0) - 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;
if (!state.invoices.length) {
tbody.innerHTML = '<tr><td colspan="9"><div class="empty-state"><div class="es-icon">🧾</div><p>No invoices yet</p></div></td></tr>';
return;
}
tbody.innerHTML = state.invoices.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>${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()">
<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'); };
openModal('viewInvoiceModal');
}
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 === 'processing' ? 'pending' : 'open'; }
function ediStatusLabel(s) { return s === 'submitted' ? 'Submitted' : 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() {
const data = await api('/pos?limit=200');
if (data && data.items) state.pos = data.items;
}
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>