RESTMED LAB CONSULT LTD
Management System
Welcome back 👋
Sign in to your account to continue
👤
🧑
🔒
❌ Incorrect email or password. Please try again.

Offline mode — sign in with your Raymond/Resty account

Secure access for RESTMED authorised personnel only

v1.0  •  Phase 1 — Dashboard

RESTMED
Management System
R
Resty
Administrator
Main
📊 Dashboard
Operations
📦 Inventory
📋 Quotations
🧾 Invoices 3
✅ Receipts
🚚 Delivery Notes
Procurement
🛒 Purchase Orders
🏭 Suppliers
Relationships
👥 Clients
Finance
📥 Income
📤 Expenses
💰 Finance
📄 Financial Statements
📈 Reports
System
⚙️ Settings
Dashboard
🔴 Offline

Good morning, Resty 👋

Here is your business overview for today

🏥 RESTMED Management System

Phase 1 Active  •  All systems operational  •  Last sync: just now

93
Stock Items
2
Users Online
v1.0
Version
📥
Total Income
UGX 0
↑ All service lines combined
📤
Total Expenses
UGX 0
↓ All business expenditure
🛒
Total Purchases
UGX 0
Updates as you post POs
📈
Net Profit
UGX 0
Revenue − Expenses − Purchases
🧾
Open Invoices
0
Awaiting payment
💳
Outstanding Balance
UGX 0
Unpaid invoices
⚠️
Low Stock Alerts
3
Needs restocking
👥
Active Clients
0
Add clients to track
🧾 Recent Transactions
View All
Doc # Client Amount Status
INV-001 Mulago Hospital 375,000 Issued
QT-005 Health Scope Ltd 8,000 USD Open
RCP-001 UVRI 1,250,000 Paid
⚡ Quick Actions
⚠️ Low Stock Alerts
View Inventory
Face Masks N95, Box of 20
PPE & Safety • Reorder: 5
2 left
HIV Test Kits, 100 Pcs
Diagnostic Kits • Reorder: 5
3 left
DPBS, 1 Litre
Reagents • Reorder: 5
Out
🕐 Recent Activity
QT-005 created
Health Scope Ltd • GeneXpert Machine • by Resty
Today
INV-001 posted
Mulago Hospital • Pipette Tips • by Resty
Today
93 items loaded
Inventory updated from price list
Today

📦 Inventory

Manage all your stock items — live stock tracking, categories and pricing

📦
Total Items
93
In product catalogue
✅
In Stock
0
Available items
⚠️
Low Stock
93
Below reorder level
❌
Out of Stock
0
Needs restocking
💰
Stock Value (Cost)
UGX 0
At cost price
💎
Stock Value (Sell)
UGX 0
At selling price
🔍
Item Code Description Category Unit Unit Cost Selling Price Stock Sold Status Actions
🔍
No items match your search.
Try a different search term or category.
Showing 93 of 93 items Page 2 — Inventory Module ✅

➕ Add New Item

🛒 Restock Item

📋 Quotations

Create quotations — convert to invoices with one click

📋
Total Quotations
0
All time
⏳
Open
0
Pending response
✅
Accepted/Converted
0
Converted
❌
Rejected
0
Not accepted
💰
Total Value
0
Combined
✏️ Create New Quotation
QT-001
Bill To / Client Details
# Item Code Description (auto) Unit Qty Price Disc% Total
1
0
2
0
3
0
4
0
5
0
6
0
7
0
8
0
9
0
10
0
SubtotalUGX 0
UGX 0
GRAND TOTALUGX 0
📋 Quotation Log
📋

No quotations yet.
Create your first one!

🖨️ Quotation Preview

🧾 Invoices

Create invoices — post to update stock and revenue automatically

🧾
Total Invoices
0
All time
📤
Issued
0
Awaiting payment
✅
Paid
0
Fully settled
⚠️
Overdue
0
Past due
💰
Total Value
0
Combined
✏️ Create New Invoice
INV-001
Bill To / Client Details
# Item Code Description (auto) Unit Qty Price Disc% Total
1
0
2
0
3
0
4
0
5
0
6
0
7
0
8
0
9
0
10
0
SubtotalUGX 0
UGX 0
GRAND TOTALUGX 0
Balance Due0
📋 Invoice Log
🧾

No invoices yet.
Create your first one!

🖨️ Invoice Preview

✅ Receipts

Record payments received against invoices — automatically updates invoice balance and status

✅
Total Receipts
0
All time
💰
Total Collected
0
UGX, all time
📅
Collected Today
0
UGX
📄
Invoices Outstanding
0
Not fully paid
✏️ Record New Receipt
RCT-001
Gross Amount SettledUGX 0
Withholding Tax (6%)UGX 0
Net Cash You'll Actually ReceiveUGX 0
📜 Receipt Log

✅ Receipt Preview

🚚 Delivery Notes

Create delivery notes and track dispatch status — optionally linked to an invoice or quotation

🚚
Total Delivery Notes
0
All time
⏳
Pending
0
Not yet dispatched
🛣️
In Transit
0
On the way
✅
Delivered
0
Confirmed received
✏️ Create New Delivery Note
DN-001

Selecting a document auto-fills the client and items below — you can still adjust quantities before saving.

Deliver To / Client Details
# Item Code Description (auto) Unit Qty
1
2
3
4
5
6
7
8
9
10
📜 Delivery Log

🚚 Delivery Note Preview

🛒 Purchase Orders

Order from suppliers — receive goods and update stock automatically

🛒
Total POs
0
All time
⏳
Draft / Sent
0
Awaiting delivery
📦
Received
0
Stock updated
❌
Cancelled
0
Not fulfilled
💰
Total Value
0
Combined
✏️ Create New Purchase Order
PO-001
Supplier / Order From
# Item Code Description (auto) Unit Qty Cost Price Disc% Total
1
0
2
0
3
0
4
0
5
0
6
0
7
0
8
0
9
0
10
0
SubtotalUGX 0
UGX 0
GRAND TOTALUGX 0
🛒 Purchase Order Log
🛒

No purchase orders yet.
Create your first one!

🖨️ Purchase Order Preview

🏭
Total Suppliers
8
In database
🛒
Total Ordered
UGX 0
All time purchases
✅
Total Paid
UGX 0
Payments made
💳
Amount Owed
UGX 0
Outstanding payables
🌍
Countries
4
Supplier locations
🔍
Supplier ID Supplier / Company Category Contact Person Phone / Email Location Total Ordered Actions
Showing 8 suppliers Phase 3 — Suppliers Module ✅

➕ Add New Supplier

👥
Total Clients
7
In database
🧾
Total Invoiced
UGX 0
All time
✅
Total Received
UGX 0
Payments collected
💳
Outstanding
UGX 0
Unpaid balances
🏆
Top Client
—
By revenue
🔍
Client ID Client / Organisation Category Contact Person Phone Location Total Invoiced Balance Due Actions
Showing 7 clients Phase 3 — Clients Module ✅

➕ Add New Client

📥 Income Tracker

Track all income by RESTMED service line — see exactly where your revenue comes from

🧪
Lab & Medical Supplies
UGX 0
Product sales income
🏗️
Lab Design & Consultancy
UGX 0
Consultancy fees
📋
Quality Management
UGX 0
LQMS & audit fees
🔧
Equipment Installation
UGX 0
Installation & support fees
🚚
Logistics & Supply Chain
UGX 0
Procurement & delivery fees
📊
Data Analysis Services
UGX 0
STATA, SPSS, R, Python
🖥️
Custom Database Solutions
UGX 0
Software & system builds
💰
TOTAL INCOME
UGX 0
All service lines combined
📥 Record New Income
Gross AmountUGX 0
Withholding Tax (6%)UGX 0
Net Cash ReceivedUGX 0
📋 Income Records
Export CSV
Date Service Line Client Amount Method Ref Actions
📥
No income recorded yet.
Record your first income entry using the form.
📊 Income Breakdown by Service Line
📊
Chart will appear here once you record income entries

📤 Expenses

Track all business expenditure

📤
Total Expenses
UGX 0
All recorded expenses
📅
This Month
UGX 0
Current month expenses
📊
Largest Category
—
Highest spending area
📤 Record New Expense
📋 Recent Expenses
Export
Date Category Description Amount Method Actions
📤
No expenses recorded yet.
Add your first expense using the form.
📊 Expenses by Category
No expenses recorded yet — this will fill in automatically.

💰 Finance

Cash position, receivables and money movements at a glance

💵
Total Cash In
UGX 0
Income + Receipts
💸
Total Cash Out
UGX 0
All expenses
🏦
Net Cash Position
UGX 0
Cash in − Cash out
📥
Accounts Receivable
UGX 0
Unpaid invoices
📤
Accounts Payable
UGX 0
Owed to suppliers
🧾
Overdue Invoices
0
Need follow-up
🧾
WHT Tax Credits
UGX 0
Withheld by clients, all time

💳 Cash by Payment Method

Income & Receipts collected minus expenses paid, per method

MethodCollected Paid OutNet Balance

📥 Accounts Receivable — Unpaid & Partially Paid Invoices

Invoice #ClientDateDue TotalPaid BalanceStatus

📤 Accounts Payable — Unpaid Purchase Orders

What RESTMED still owes its suppliers

PO #SupplierDateStatus Amount OwedAction

🧾 Withholding Tax (WHT) Credits

6% deducted by clients on payments — keep for URA reconciliation against WHT certificates

DateClientReference Gross Amount WHT (6%) Net Cash Received

📒 Money Movement Ledger

Every income entry, receipt and expense in one place

DateTypeDescriptionParty MethodMoney In Money Out

📄 Financial Statements

Profit & Loss, Balance Sheet and Cash Flow — built from your live data

📊 Profit & Loss Statement

For the period selected

Revenue
Invoiced SalesUGX 0
Other Service IncomeUGX 0
Total RevenueUGX 0
Cost of Goods Sold (estimated at current unit cost)
Cost of Items SoldUGX 0
Gross ProfitUGX 0
Operating Expenses
Total Operating ExpensesUGX 0
NET PROFIT UGX 0
Net Margin0%

🏦 Balance Sheet

As of today — current position, not limited to the period above

Assets
Cash & BankUGX 0
Accounts ReceivableUGX 0
Inventory (at cost)UGX 0
Total AssetsUGX 0
Liabilities & Equity
Accounts PayableUGX 0
Total LiabilitiesUGX 0
Owner's Equity (plug)UGX 0
Total Liabilities + EquityUGX 0

💵 Cash Flow Statement

For the period selected

Opening Cash BalanceUGX 0
Cash In (net of Withholding Tax deducted)
Manual Income EntriesUGX 0
Receipts (invoice & direct sale collections)UGX 0
Quick Invoice PaymentsUGX 0
Total Cash InUGX 0
🧾 of which Withholding Tax deducted by clients (info only)UGX 0
Cash Out
ExpensesUGX 0
Purchase Order PaymentsUGX 0
Total Cash OutUGX 0
Net Cash Flow for PeriodUGX 0
Closing Cash Balance UGX 0

📈 Reports & Analytics

Sales, stock, revenue and client insights — built live from your data

💰
Revenue (YTD)
UGX 0
Jan 1 to today
💸
Expenses (YTD)
UGX 0
Jan 1 to today
📈
Net Profit (YTD)
UGX 0
Revenue − Expenses
📦
Inventory Value
UGX 0
At cost, all stock
⚠️
Low Stock Items
0
At or below reorder level
🔄
Quote → Invoice Rate
0%
0 of 0 quotations

📊 Revenue vs Expenses — Last 6 Months

🥧 Income by Category

🏆 Top 5 Clients by Revenue

🚚 Top 5 Suppliers by Purchase Value

⚠️ Low Stock Alert

CodeItemCategory In StockReorder Level Value at Cost

⚙️ Settings

Company profile, users, VAT, currencies and preferences

⚠️ This app runs fully offline — Settings are saved to this browser's storage and persist across refreshes, but only on this computer. If Raymond and Resty each open the file on their own machines, their changes don't share automatically.
🏢 Company Profile Used on Quotations, Invoices, POs, Delivery Notes & Receipts
Payment Accounts (shown on Quotation / Invoice / PO notes)
TypeLabelDetailsAction
Saved ✓ (this session)
🙋 Signed In As
—
👥 Registered Users ↻ Refresh
NameRoleUser ID
Loading users…
ℹ️ This is offline mode — there's no server, so there's no self-signup and no in-app account/role editor. The only accounts are Raymond and Resty, built directly into this file's code (see the USERS object). To add or change an account, someone needs to edit the file itself.
💰 VAT & Currency Defaults
CodeLabelAction
Saved ✓ (this session)
🛠️ Document & Inventory Preferences
Pre-filled reorder level when adding a new Inventory item — still editable per item.
Shown in the Unit dropdown when adding or editing an Inventory item.
Saved ✓ (this session)
⬇️ Export All Data

This app's data lives only in this browser, on this computer. Use this to save a backup file, or to move everything to another device — download it here, then use "Import" below on the other machine.

Downloaded ✓
⬆️ Import Data

Select a backup file downloaded from this app (yours or from another device). This replaces everything currently saved here — Inventory, Clients, Invoices, Settings, all of it. There's no undo, so if you have data on this device you haven't backed up yet, export it first.

🔍 Search Inventory Item ×
`); w.document.close(); } // ── Clear form ───────────────────────────────────────────────────── function clearDocForm(type) { for(let i=1;i<=10;i++) { ["code","desc","unit","qty","price","disc"].forEach(f=>{ const e=document.getElementById(type+"-"+f+"-"+i); if(e) e.value=""; }); const lt=document.getElementById(type+"-linetotal-"+i); if(lt) lt.textContent="0"; const r=document.getElementById(type+"-row-"+i); if(r) r.style.display=i===1?"":"none"; } const sel=document.getElementById(type+"-client-sel"); if(sel) sel.value=""; ["cli-name","cli-cat","cli-contact"].forEach(f=>{ const e=document.getElementById(type+"-"+f); if(e) e.value=""; }); if(type==="dn") { ["driver","vehicle","receivedby","notes"].forEach(f=>{ const e=document.getElementById("dn-"+f); if(e) e.value=""; }); const linkSel=document.getElementById("dn-link-sel"); if(linkSel) linkSel.value=""; initDeliveryForm(); const editIdx = document.getElementById("dn-edit-idx"); if(editIdx) editIdx.value = ""; const saveBtn = document.getElementById("dn-save-btn"); if(saveBtn) saveBtn.textContent = "✅ Save Delivery Note"; const cancelBtn = document.getElementById("dn-cancel-btn"); if(cancelBtn) cancelBtn.style.display = "none"; } else if(type==="po") { initPOForm(); } else { initDocForm(type); const editIdx = document.getElementById(type+"-edit-idx"); if(editIdx) editIdx.value = ""; const saveBtn = document.getElementById(type+"-save-btn"); if(saveBtn) saveBtn.textContent = type==="qt" ? "✅ Save & Post Quotation" : "✅ Save & Post Invoice"; const cancelBtn = document.getElementById(type+"-cancel-btn"); if(cancelBtn) cancelBtn.style.display = "none"; } } // ── Export logs ──────────────────────────────────────────────────── function exportQtLog() { if(!QUOTATIONS.length){alert("No quotations to export.");return;} let csv="Doc No,Date,Valid Until,Client,Category,Currency,Total,Status\n"; QUOTATIONS.forEach(d=>{csv+=`${d.docNum},${d.date},${d.due},"${d.clientName}","${d.clientCat}",${d.currency},${Math.round(d.grand)},${d.status}\n`;}); const a=document.createElement("a"); a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"})); a.download="RESTMED_Quotations_"+new Date().toISOString().slice(0,10)+".csv"; a.click(); } function exportInvLog() { if(!INVOICES.length){alert("No invoices to export.");return;} let csv="Doc No,Date,Due,Client,Category,Currency,Subtotal,VAT,Total,Status\n"; INVOICES.forEach(d=>{csv+=`${d.docNum},${d.date},${d.due},"${d.clientName}","${d.clientCat}",${d.currency},${Math.round(d.sub)},${Math.round(d.vat)},${Math.round(d.grand)},${d.status}\n`;}); const a=document.createElement("a"); a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"})); a.download="RESTMED_Invoices_"+new Date().toISOString().slice(0,10)+".csv"; a.click(); } // ══════════════════════════════════════════════════════════════════ // PHASE 6 — PURCHASE ORDERS // Reuses the generic qt/inv document machinery (calcLine, addDocRow, // calcDocTotals, itemSuggest, applyItem, gatherDoc, buildDocHTML, // previewDoc, printPreview, clearDocForm) with type="po". The // "client-sel/cli-name/cli-cat/cli-contact" ids are repurposed to // hold the selected SUPPLIER instead of a client. // ══════════════════════════════════════════════════════════════════ let PO_COUNTER = 1; const PURCHASE_ORDERS = []; // ── Purchase Orders persistence (localStorage) ────────────────────── function loadPurchaseOrdersFromLocal() { const saved = lsLoad("purchase_orders"); if (saved && Array.isArray(saved)) { PURCHASE_ORDERS.length = 0; saved.forEach(d => PURCHASE_ORDERS.push(d)); } PO_COUNTER = nextCounterFor(PURCHASE_ORDERS, "PO"); const sec = document.getElementById("section-purchases"); if (typeof renderPOLog === "function" && sec && sec.style.display !== "none") renderPOLog(); if (typeof updatePOKPIs === "function") updatePOKPIs(); syncPullModule("purchase_orders", (rows) => { rows.forEach(row => { const idx = PURCHASE_ORDERS.findIndex(d => d.docNum === row.key); if (row.deleted) { if (idx >= 0) PURCHASE_ORDERS.splice(idx, 1); } else if (idx >= 0) PURCHASE_ORDERS[idx] = row.data; else PURCHASE_ORDERS.push(row.data); }); PO_COUNTER = nextCounterFor(PURCHASE_ORDERS, "PO"); lsSave("purchase_orders", PURCHASE_ORDERS); const s = document.getElementById("section-purchases"); if (typeof renderPOLog === "function" && s && s.style.display !== "none") renderPOLog(); if (typeof updatePOKPIs === "function") updatePOKPIs(); }); subscribeModuleRealtime("purchase_orders", (payload) => { const row = payload.new || payload.old; if (!row) return; const idx = PURCHASE_ORDERS.findIndex(d => d.docNum === row.key); if (row.deleted) { if (idx >= 0) PURCHASE_ORDERS.splice(idx, 1); } else if (idx >= 0) PURCHASE_ORDERS[idx] = row.data; else PURCHASE_ORDERS.push(row.data); PO_COUNTER = nextCounterFor(PURCHASE_ORDERS, "PO"); lsSave("purchase_orders", PURCHASE_ORDERS); const s = document.getElementById("section-purchases"); if (typeof renderPOLog === "function" && s && s.style.display !== "none") renderPOLog(); if (typeof updatePOKPIs === "function") updatePOKPIs(); }); } function pushPurchaseOrderUpsert(doc) { lsSave("purchase_orders", PURCHASE_ORDERS); if (doc) syncPush("purchase_orders", doc.docNum, doc, false); } function getPoNotes() { return `Please confirm receipt of this order and expected delivery date. Prices in the stated currency; VAT at ${SETTINGS.vatRate}% added where applicable. Payment Terms: As agreed with supplier — see company payment policy. Deliver to: ${COMPANY_PROFILE.name}, ${COMPANY_PROFILE.address}. For queries: ${COMPANY_PROFILE.email} | ${COMPANY_PROFILE.phone1} / ${COMPANY_PROFILE.phone2}`; } // ── Init PO form ──────────────────────────────────────────────────── function initPOForm() { const now = new Date(); const today = now.toISOString().slice(0,10); const due14 = new Date(now.getTime() + SETTINGS.poDeliveryDays*24*60*60*1000).toISOString().slice(0,10); const docNum = "PO-"+pad(PO_COUNTER); const set = (id,v) => { const el=document.getElementById(id); if(el) el.value=v||""; }; set("po-doc-num", docNum); set("po-date", today); set("po-due", due14); set("po-notes", getPoNotes()); set("po-prepby", currentUser ? currentUser.name : ""); const disp = document.getElementById("po-num-display"); if(disp) disp.textContent = docNum; const sel = document.getElementById("po-client-sel"); if(sel) { sel.innerHTML = ""; SUPPLIERS.forEach(s => { const o = document.createElement("option"); o.value=s.id; o.textContent=s.id+" | "+s.name; sel.appendChild(o); }); } for(let i=1;i<=10;i++) { const r = document.getElementById("po-row-"+i); if(r) r.style.display = i===1 ? "" : "none"; } calcDocTotals("po"); } // ── Supplier autofill (mirrors fillClient) ────────────────────────── function fillSupplier(type) { const id = (document.getElementById(type+"-client-sel")||{}).value; const sup = SUPPLIERS.find(s=>s.id===id)||{}; const set = (s,v) => { const e=document.getElementById(type+s); if(e) e.value=v||""; }; set("-cli-name", sup.name||""); set("-cli-cat", sup.cat||""); set("-cli-contact", (sup.contact||"")+(sup.phone?" | "+sup.phone:"")); } // ── Save PO ────────────────────────────────────────────────────────── function savePurchaseOrder() { const data = gatherDoc("po"); if(!data.clientId) { alert("Please select a supplier."); return; } if(!data.items.length) { alert("Please add at least one item with quantity."); return; } data.status = "Sent"; data.received = false; data.paid = false; data.paidDate = ""; data.paidMethod = ""; PURCHASE_ORDERS.push(data); PO_COUNTER++; renderPOLog(); updatePOKPIs(); alert("✅ "+data.docNum+" sent!\\nSupplier: "+data.clientName+"\\nTotal: "+data.currency+" "+fmtMoney(data.grand)); clearDocForm("po"); pushPurchaseOrderUpsert(data); } // ── Render PO log ──────────────────────────────────────────────────── function renderPOLog() { const el = document.getElementById("po-log"); if(!el) return; if(!PURCHASE_ORDERS.length) { el.innerHTML = `
🛒

No purchase orders yet.

`; return; } const statusBg={Draft:"#f3f4f6",Sent:"#dbeafe",Received:"#dcfce7",Cancelled:"#fee2e2"}; const statusFg={Draft:"#6b7280",Sent:"#1d4ed8",Received:"#15803d",Cancelled:"#dc2626"}; el.innerHTML=[...PURCHASE_ORDERS].reverse().map((po,ri)=>{ const idx=PURCHASE_ORDERS.length-1-ri; const sb=statusBg[po.status]||"#f3f4f6"; const sf=statusFg[po.status]||"#6b7280"; const owesMoney = po.status!=="Cancelled" && !po.paid; return `
${po.docNum} ${po.currency} ${fmtMoney(po.grand)}
${po.clientName} • ${po.date}
${po.status} ${po.paid?`💰 Paid` :(owesMoney?`Unpaid`:"")}
${po.status!=="Received"&&po.status!=="Cancelled"?` `:""} ${owesMoney?``:""}
`; }).join(""); } // ── KPIs ────────────────────────────────────────────────────────────── function updatePOKPIs() { let open=0,good=0,bad=0,val=0; PURCHASE_ORDERS.forEach(po=>{ if(po.status==="Draft"||po.status==="Sent") open++; if(po.status==="Received") good++; if(po.status==="Cancelled") bad++; val+=po.grand||0; }); const set=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v;}; set("po-kpi-total", PURCHASE_ORDERS.length); set("po-kpi-open", open); set("po-kpi-good", good); set("po-kpi-bad", bad); set("po-kpi-value", fmtMoney(val)); } // ── Mark PO Received: updates inventory stock + supplier totals ────── function markPOReceived(idx) { const po = PURCHASE_ORDERS[idx]; if(!po) return; if(!confirm("Mark "+po.docNum+" as received?\\nThis will add the ordered quantities to inventory stock.")) return; po.items.forEach(item => { const inv = INVENTORY.find(i=>i.code===item.code); if(inv) { inv.stock = (inv.stock||0)+item.qty; inv.purchased = (inv.purchased||0)+item.qty; } }); const sup = SUPPLIERS.find(s=>s.id===po.clientId); if(sup) sup.totalOrdered = (sup.totalOrdered||0)+po.grand; po.status = "Received"; po.received = true; renderPOLog(); updatePOKPIs(); if(typeof renderSuppliers==="function") renderSuppliers(); if(typeof renderInventory==="function") renderInventory(); pushPurchaseOrderUpsert(po); po.items.forEach(item => { const invItem = INVENTORY.find(i => i.code === item.code); if (invItem) pushInventoryUpsert(invItem); }); if (sup) pushSupplierUpsert(sup); alert("✅ "+po.docNum+" received!\\nInventory stock updated automatically."); } function cancelPO(idx) { const po = PURCHASE_ORDERS[idx]; if(!po) return; if(!confirm("Cancel "+po.docNum+"?")) return; po.status = "Cancelled"; renderPOLog(); updatePOKPIs(); pushPurchaseOrderUpsert(po); } // ── Mark PO as Paid: records the payment, updates supplier + finance ─ function markPOPaid(idx) { const po = PURCHASE_ORDERS[idx]; if(!po) return; const methods = ["Cash","Bank Transfer","Mobile Money","Cheque"]; let method = prompt( "Record payment for "+po.docNum+" ("+po.currency+" "+fmtMoney(po.grand)+")\n\n"+ "Payment method — type one of:\nCash / Bank Transfer / Mobile Money / Cheque", "Bank Transfer"); if(method===null) return; // user cancelled method = method.trim(); const match = methods.find(m=>m.toLowerCase()===method.toLowerCase()); method = match || "Bank Transfer"; po.paid = true; po.paidDate = new Date().toISOString().slice(0,10); po.paidMethod = method; const sup = SUPPLIERS.find(s=>s.id===po.clientId); if(sup) sup.totalPaid = (sup.totalPaid||0)+po.grand; renderPOLog(); updatePOKPIs(); if(typeof renderSuppliers==="function") renderSuppliers(); pushPurchaseOrderUpsert(po); if (sup) pushSupplierUpsert(sup); alert("✅ Payment recorded for "+po.docNum+"\nMethod: "+method+"\nSupplier accounts updated."); } // ── Export PO log ────────────────────────────────────────────────── function exportPOLog() { if(!PURCHASE_ORDERS.length){alert("No purchase orders to export.");return;} let csv="Doc No,Date,Expected Delivery,Supplier,Category,Currency,Subtotal,VAT,Total,Status\n"; PURCHASE_ORDERS.forEach(d=>{csv+=`${d.docNum},${d.date},${d.due},"${d.clientName}","${d.clientCat}",${d.currency},${Math.round(d.sub)},${Math.round(d.vat)},${Math.round(d.grand)},${d.status}\n`;}); const a=document.createElement("a"); a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"})); a.download="RESTMED_PurchaseOrders_"+new Date().toISOString().slice(0,10)+".csv"; a.click(); } // ══════════════════════════════════════════════════════════════════ // PHASE 5 — DELIVERY NOTES // ══════════════════════════════════════════════════════════════════ let DN_COUNTER = 1; const DELIVERY_NOTES = []; // ── Delivery Notes persistence (localStorage) ─────────────────────── function loadDeliveryNotesFromLocal() { const saved = lsLoad("delivery_notes"); if (saved && Array.isArray(saved)) { DELIVERY_NOTES.length = 0; saved.forEach(d => DELIVERY_NOTES.push(d)); } DN_COUNTER = nextCounterFor(DELIVERY_NOTES, "DN"); const sec = document.getElementById("section-delivery"); if (typeof renderDNLog === "function" && sec && sec.style.display !== "none") renderDNLog(); if (typeof updateDNKPIs === "function") updateDNKPIs(); syncPullModule("delivery_notes", (rows) => { rows.forEach(row => { const idx = DELIVERY_NOTES.findIndex(d => d.docNum === row.key); if (row.deleted) { if (idx >= 0) DELIVERY_NOTES.splice(idx, 1); } else if (idx >= 0) DELIVERY_NOTES[idx] = row.data; else DELIVERY_NOTES.push(row.data); }); DN_COUNTER = nextCounterFor(DELIVERY_NOTES, "DN"); lsSave("delivery_notes", DELIVERY_NOTES); const s = document.getElementById("section-delivery"); if (typeof renderDNLog === "function" && s && s.style.display !== "none") renderDNLog(); if (typeof updateDNKPIs === "function") updateDNKPIs(); }); subscribeModuleRealtime("delivery_notes", (payload) => { const row = payload.new || payload.old; if (!row) return; const idx = DELIVERY_NOTES.findIndex(d => d.docNum === row.key); if (row.deleted) { if (idx >= 0) DELIVERY_NOTES.splice(idx, 1); } else if (idx >= 0) DELIVERY_NOTES[idx] = row.data; else DELIVERY_NOTES.push(row.data); DN_COUNTER = nextCounterFor(DELIVERY_NOTES, "DN"); lsSave("delivery_notes", DELIVERY_NOTES); const s = document.getElementById("section-delivery"); if (typeof renderDNLog === "function" && s && s.style.display !== "none") renderDNLog(); if (typeof updateDNKPIs === "function") updateDNKPIs(); }); } function pushDeliveryNoteUpsert(doc) { lsSave("delivery_notes", DELIVERY_NOTES); if (doc) syncPush("delivery_notes", doc.docNum, doc, false); } // ── Init delivery note form ───────────────────────────────────────── function initDeliveryForm() { const now = new Date(); const today = now.toISOString().slice(0,10); const docNum = "DN-"+pad(DN_COUNTER); const set = (id,v) => { const el=document.getElementById(id); if(el) el.value=v||""; }; set("dn-doc-num", docNum); set("dn-date", today); set("dn-dispatchedby", currentUser ? currentUser.name : ""); const statusEl = document.getElementById("dn-status"); if(statusEl) statusEl.value = "Pending"; const disp = document.getElementById("dn-num-display"); if(disp) disp.textContent = docNum; // Populate client dropdown const sel = document.getElementById("dn-client-sel"); if(sel) { sel.innerHTML = ""; CLIENTS.forEach(c => { const o = document.createElement("option"); o.value=c.id; o.textContent=c.id+" | "+c.name; sel.appendChild(o); }); } // Populate linked-document dropdown (Quotations + Invoices) const linkSel = document.getElementById("dn-link-sel"); if(linkSel) { linkSel.innerHTML = ""; INVOICES.forEach((d,i) => { const o = document.createElement("option"); o.value = "inv:"+i; o.textContent = "INV "+d.docNum+" — "+d.clientName; linkSel.appendChild(o); }); QUOTATIONS.forEach((d,i) => { const o = document.createElement("option"); o.value = "qt:"+i; o.textContent = "QT "+d.docNum+" — "+d.clientName; linkSel.appendChild(o); }); } // Show only row 1 for(let i=1;i<=10;i++) { const r = document.getElementById("dn-row-"+i); if(r) r.style.display = i===1 ? "" : "none"; } } // ── Auto-fill client & items from a linked Invoice/Quotation ───────── function fillFromLinkedDoc() { const val = (document.getElementById("dn-link-sel")||{}).value; if(!val) return; const [kind, idxStr] = val.split(":"); const idx = parseInt(idxStr); const doc = (kind==="inv" ? INVOICES : QUOTATIONS)[idx]; if(!doc) return; const set = (id,v) => { const e=document.getElementById(id); if(e) e.value=v||""; }; set("dn-client-sel", doc.clientId); fillClient("dn"); // Reset rows then fill from the linked document's items for(let i=1;i<=10;i++) { const r=document.getElementById("dn-row-"+i); if(r) r.style.display = i===1 ? "" : "none"; ["code","desc","unit","qty"].forEach(f=>{ const e=document.getElementById("dn-"+f+"-"+i); if(e) e.value=""; }); } doc.items.forEach((item,i) => { if(i>0) { const r=document.getElementById("dn-row-"+(i+1)); if(r) r.style.display=""; } set("dn-code-"+(i+1), item.code); set("dn-desc-"+(i+1), item.desc); set("dn-unit-"+(i+1), item.unit); set("dn-qty-"+(i+1), item.qty); }); } // ── Gather delivery note data ───────────────────────────────────────── function gatherDeliveryNote() { const g = id => (document.getElementById("dn-"+id)||{}).value||""; const items=[]; for(let i=1;i<=10;i++) { const r=document.getElementById("dn-row-"+i); if(!r||r.style.display==="none") continue; const code = (document.getElementById("dn-code-"+i)||{}).value||""; if(!code) continue; const qty = parseFloat((document.getElementById("dn-qty-"+i)||{}).value)||0; const desc = (document.getElementById("dn-desc-"+i)||{}).value||""; const unit = (document.getElementById("dn-unit-"+i)||{}).value||""; if(qty>0) items.push({code,desc,unit,qty}); } const linkVal = g("link-sel"); let linkedDocNum = ""; if(linkVal) { const [kind, idxStr] = linkVal.split(":"); const doc = (kind==="inv" ? INVOICES : QUOTATIONS)[parseInt(idxStr)]; if(doc) linkedDocNum = (kind==="inv"?"INV ":"QT ")+doc.docNum; } return { docNum: g("doc-num"), date: g("date"), status: g("status")||"Pending", linkedDocNum, clientId: g("client-sel"), clientName: g("cli-name"), clientCat: g("cli-cat"), contact: g("cli-contact"), driver: g("driver"), vehicle: g("vehicle"), dispatchedBy:g("dispatchedby"), receivedBy: g("receivedby"), notes: g("notes"), items, created: new Date().toISOString() }; } // ── Save Delivery Note ──────────────────────────────────────────────── function saveDeliveryNote() { const data = gatherDeliveryNote(); if(!data.clientId) { alert("Please select a client."); return; } if(!data.items.length) { alert("Please add at least one item with quantity."); return; } const editIdx = document.getElementById("dn-edit-idx").value; if (editIdx !== "") { const idx = parseInt(editIdx, 10); const existing = DELIVERY_NOTES[idx]; if (existing) { data.docNum = existing.docNum; // keep the original delivery note number data.created = existing.created; // keep the original creation timestamp DELIVERY_NOTES[idx] = data; } renderDNLog(); updateDNKPIs(); alert("✅ "+data.docNum+" updated!"); clearDocForm("dn"); pushDeliveryNoteUpsert(data); return; } DELIVERY_NOTES.push(data); DN_COUNTER++; renderDNLog(); updateDNKPIs(); alert("✅ "+data.docNum+" saved!\nClient: "+data.clientName+"\nItems: "+data.items.length); clearDocForm("dn"); pushDeliveryNoteUpsert(data); } // ── Edit an existing delivery note ────────────────────────────────── function editDN(idx) { const data = DELIVERY_NOTES[idx]; if(!data) return; const set = (id,v) => { const e=document.getElementById("dn-"+id); if(e) e.value=v||""; }; set("date", data.date); set("status",data.status); set("driver",data.driver); set("vehicle",data.vehicle); set("dispatchedby",data.dispatchedBy); set("receivedby", data.receivedBy); set("notes", data.notes); const disp = document.getElementById("dn-num-display"); if(disp) disp.textContent = data.docNum; const docNumEl = document.getElementById("dn-doc-num"); if(docNumEl) docNumEl.value = data.docNum; const sel = document.getElementById("dn-client-sel"); if(sel) { sel.value = data.clientId; fillClient("dn"); } // Reset all item rows, then populate one per saved item (DN has no price/disc). for(let i=1;i<=10;i++) { const r = document.getElementById("dn-row-"+i); if(r) r.style.display = i===1 ? "" : "none"; ["code","desc","unit","qty"].forEach(f=>{ const e = document.getElementById("dn-"+f+"-"+i); if(e) e.value=""; }); } data.items.forEach((item, i) => { const row = i+1; if(row > 10) return; if(row > 1) { const r = document.getElementById("dn-row-"+row); if(r) r.style.display = ""; } const setRow = (f,v) => { const e=document.getElementById("dn-"+f+"-"+row); if(e) e.value=v; }; setRow("code", item.code); setRow("desc", item.desc); setRow("unit", item.unit); setRow("qty", item.qty); }); document.getElementById("dn-edit-idx").value = idx; const saveBtn = document.getElementById("dn-save-btn"); if(saveBtn) saveBtn.textContent = "💾 Update Delivery Note"; const cancelBtn = document.getElementById("dn-cancel-btn"); if(cancelBtn) cancelBtn.style.display = ""; const anchor = document.getElementById("dn-driver"); if (anchor) anchor.scrollIntoView({behavior:"smooth", block:"center"}); } // ── Delete a delivery note ─────────────────────────────────────────── function deleteDN(idx) { const dn = DELIVERY_NOTES[idx]; if(!dn) return; if(!confirm("Delete delivery note "+dn.docNum+"?\nThis cannot be undone.")) return; DELIVERY_NOTES.splice(idx,1); renderDNLog(); updateDNKPIs(); lsSave("delivery_notes", DELIVERY_NOTES); syncPush("delivery_notes", dn.docNum, null, true); } // ── Render delivery log ───────────────────────────────────────────── function renderDNLog() { const el = document.getElementById("dn-log"); if(!el) return; if(!DELIVERY_NOTES.length) { el.innerHTML=`
🚚

No delivery notes yet.

`; return; } const statusBg={Pending:"#fef9c3","In Transit":"#dbeafe",Delivered:"#dcfce7"}; const statusFg={Pending:"#854d0e","In Transit":"#1d4ed8",Delivered:"#15803d"}; el.innerHTML=[...DELIVERY_NOTES].reverse().map((dn,ri)=>{ const idx=DELIVERY_NOTES.length-1-ri; const sb=statusBg[dn.status]||"#f3f4f6"; const sf=statusFg[dn.status]||"#6b7280"; return `
${dn.docNum} ${dn.items.length} item(s)
${dn.clientName} • ${dn.date}
${dn.linkedDocNum?`
🔗 ${dn.linkedDocNum}
`:""}
${dn.status}
${dn.status!=="Delivered"?``:""}
`; }).join(""); } // ── Update KPIs ──────────────────────────────────────────────────── function updateDNKPIs() { let pending=0, transit=0, delivered=0; DELIVERY_NOTES.forEach(d=>{ if(d.status==="Pending") pending++; if(d.status==="In Transit") transit++; if(d.status==="Delivered") delivered++; }); const set=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v;}; set("dn-kpi-total", DELIVERY_NOTES.length); set("dn-kpi-pending", pending); set("dn-kpi-transit", transit); set("dn-kpi-delivered", delivered); } // ── Mark delivered ───────────────────────────────────────────────── function markDNDelivered(idx) { if(!DELIVERY_NOTES[idx]) return; DELIVERY_NOTES[idx].status="Delivered"; renderDNLog(); updateDNKPIs(); pushDeliveryNoteUpsert(DELIVERY_NOTES[idx]); alert("✅ "+DELIVERY_NOTES[idx].docNum+" marked as Delivered!"); } // ── PDF preview ──────────────────────────────────────────────────── function buildDNHTML(data) { const itemRows = data.items.map((it,i)=>` ${i+1} ${it.code} ${it.desc} ${it.unit} ${it.qty} `).join(""); return `
RESTMED
${COMPANY_PROFILE.address}
Tel: ${COMPANY_PROFILE.phone1} / ${COMPANY_PROFILE.phone2} | ${COMPANY_PROFILE.email}
${COMPANY_PROFILE.website} | TIN: ${COMPANY_PROFILE.tin}

DELIVERY NOTE

${data.docNum}
Dispatch Date: ${data.date}
${data.linkedDocNum?`
Ref: ${data.linkedDocNum}
`:""}
Deliver To
${data.clientName}
${data.clientCat} | Client ID: ${data.clientId}
${data.contact||""}
Dispatch Details
Driver: ${data.driver||"—"}
Vehicle: ${data.vehicle||"—"}
Status: ${data.status}
${itemRows}
# Code Description Unit Qty
${data.notes?`
Notes
${data.notes}
`:""}
Dispatched By
${data.dispatchedBy||""}
Signature & Date
Received By
${data.receivedBy||""}
Name, Signature & Date
${COMPANY_PROFILE.name} | ${COMPANY_PROFILE.tagline} | ${COMPANY_PROFILE.website} | TIN: ${COMPANY_PROFILE.tin}
`; } function previewDN(idx) { const data = DELIVERY_NOTES[idx]; if(!data) return; const modal = document.getElementById("dn-preview-modal"); const content = document.getElementById("dn-preview-content"); if(!modal||!content) return; content.innerHTML = buildDNHTML(data); modal.style.display = "flex"; } function printDNPreview() { const content = document.getElementById("dn-preview-content"); if(!content) return; const w = window.open("","_blank","width=900,height=700"); w.document.write(`RESTMED Delivery Note ${content.innerHTML} `); w.document.close(); } // ── Export delivery log ───────────────────────────────────────────── function exportDNLog() { if(!DELIVERY_NOTES.length){alert("No delivery notes to export.");return;} let csv="Doc No,Date,Client,Status,Driver,Vehicle,Ref Doc,Items,Dispatched By,Received By\n"; DELIVERY_NOTES.forEach(d=>{csv+=`${d.docNum},${d.date},"${d.clientName}",${d.status},"${d.driver||""}","${d.vehicle||""}","${d.linkedDocNum||""}",${d.items.length},"${d.dispatchedBy||""}","${d.receivedBy||""}"\n`;}); const a=document.createElement("a"); a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"})); a.download="RESTMED_DeliveryNotes_"+new Date().toISOString().slice(0,10)+".csv"; a.click(); } // Close DN modal / suggestion dropdowns on outside click document.addEventListener("click", e=>{ const m=document.getElementById("dn-preview-modal"); if(m && e.target===m) m.style.display="none"; document.querySelectorAll("[id^='dn-sug-']").forEach(el=>{ if(!el.contains(e.target)) el.style.display="none"; }); }); // ══════════════════════════════════════════════════════════════════ // PHASE 5 — RECEIPTS // ══════════════════════════════════════════════════════════════════ let RCT_COUNTER = 1; const RECEIPTS = []; let RCT_TYPE = "invoice"; // "invoice" | "direct" // ── Receipts persistence (localStorage) ───────────────────────────── function loadReceiptsFromLocal() { const saved = lsLoad("receipts"); if (saved && Array.isArray(saved)) { RECEIPTS.length = 0; saved.forEach(d => RECEIPTS.push(d)); } RCT_COUNTER = nextCounterFor(RECEIPTS, "RCT"); const sec = document.getElementById("section-receipts"); if (typeof renderRCTLog === "function" && sec && sec.style.display !== "none") renderRCTLog(); if (typeof updateRCTKPIs === "function") updateRCTKPIs(); syncPullModule("receipts", (rows) => { rows.forEach(row => { const idx = RECEIPTS.findIndex(d => d.docNum === row.key); if (row.deleted) { if (idx >= 0) RECEIPTS.splice(idx, 1); } else if (idx >= 0) RECEIPTS[idx] = row.data; else RECEIPTS.push(row.data); }); RCT_COUNTER = nextCounterFor(RECEIPTS, "RCT"); lsSave("receipts", RECEIPTS); const s = document.getElementById("section-receipts"); if (typeof renderRCTLog === "function" && s && s.style.display !== "none") renderRCTLog(); if (typeof updateRCTKPIs === "function") updateRCTKPIs(); }); subscribeModuleRealtime("receipts", (payload) => { const row = payload.new || payload.old; if (!row) return; const idx = RECEIPTS.findIndex(d => d.docNum === row.key); if (row.deleted) { if (idx >= 0) RECEIPTS.splice(idx, 1); } else if (idx >= 0) RECEIPTS[idx] = row.data; else RECEIPTS.push(row.data); RCT_COUNTER = nextCounterFor(RECEIPTS, "RCT"); lsSave("receipts", RECEIPTS); const s = document.getElementById("section-receipts"); if (typeof renderRCTLog === "function" && s && s.style.display !== "none") renderRCTLog(); if (typeof updateRCTKPIs === "function") updateRCTKPIs(); }); } function pushReceiptUpsert(doc) { lsSave("receipts", RECEIPTS); if (doc) syncPush("receipts", doc.docNum, doc, false); } // ── Toggle between Invoice-payment and Direct-sale modes ───────────── function setReceiptType(type) { RCT_TYPE = type; const invBtn = document.getElementById("rct-type-inv"); const dirBtn = document.getElementById("rct-type-direct"); const invBlock = document.getElementById("rct-invoice-block"); const autoBlock = document.getElementById("rct-client-auto-block"); const directBlock= document.getElementById("rct-direct-block"); if(type==="invoice") { if(invBtn) { invBtn.style.background="var(--green-dark)"; invBtn.style.color="white"; } if(dirBtn) { dirBtn.style.background="transparent"; dirBtn.style.color="#6b7280"; } if(invBlock) invBlock.style.display=""; if(autoBlock) autoBlock.style.display=""; if(directBlock)directBlock.style.display="none"; } else { if(dirBtn) { dirBtn.style.background="var(--green-dark)"; dirBtn.style.color="white"; } if(invBtn) { invBtn.style.background="transparent"; invBtn.style.color="#6b7280"; } if(invBlock) invBlock.style.display="none"; if(autoBlock) autoBlock.style.display="none"; if(directBlock)directBlock.style.display=""; } } // ── Init receipt form ───────────────────────────────────────────── function initReceiptForm() { const today = new Date().toISOString().slice(0,10); const docNum = "RCT-"+pad(RCT_COUNTER); const set = (id,v) => { const el=document.getElementById(id); if(el) el.value=v||""; }; set("rct-doc-num", docNum); set("rct-date", today); set("rct-receivedby", currentUser ? currentUser.name : ""); const disp = document.getElementById("rct-num-display"); if(disp) disp.textContent = docNum; // Populate invoice dropdown — only invoices not already fully paid const sel = document.getElementById("rct-inv-sel"); if(sel) { sel.innerHTML = ""; INVOICES.forEach((inv,i) => { const paid = inv.paidAmount||0; const bal = (inv.grand||0) - paid; if(bal <= 0) return; // fully paid, skip const o = document.createElement("option"); o.value = i; o.textContent = inv.docNum+" — "+inv.clientName+" (Bal: "+fmtMoney(bal)+" "+(inv.currency||"UGX")+")"; sel.appendChild(o); }); } // Populate direct-sale client dropdown const dsel = document.getElementById("rct-direct-client-sel"); if(dsel) { dsel.innerHTML = ""; CLIENTS.forEach(c => { const o = document.createElement("option"); o.value=c.id; o.textContent=c.id+" | "+c.name; dsel.appendChild(o); }); } set("rct-amount",""); set("rct-ref",""); set("rct-notes",""); set("rct-direct-name",""); set("rct-direct-desc",""); set("rct-cli-name",""); set("rct-cli-cat",""); const inv=document.getElementById("rct-inv-summary"); if(inv) inv.innerHTML=""; setReceiptType("invoice"); updateRCTKPIs(); } // ── Auto-fill client + balance from selected invoice ────────────── function fillInvoiceForReceipt() { const sel = document.getElementById("rct-inv-sel"); const summary = document.getElementById("rct-inv-summary"); const idx = sel ? sel.value : ""; if(idx==="") { ["rct-cli-name","rct-cli-cat"].forEach(id=>{const e=document.getElementById(id); if(e) e.value="";}); if(summary) summary.innerHTML=""; return; } const inv = INVOICES[parseInt(idx)]; if(!inv) return; const set = (id,v) => { const e=document.getElementById(id); if(e) e.value=v||""; }; set("rct-cli-name", inv.clientName); set("rct-cli-cat", inv.clientCat); const paid = inv.paidAmount||0; const bal = (inv.grand||0) - paid; const amtEl = document.getElementById("rct-amount"); if(amtEl) amtEl.value = Math.round(bal); if(summary) { summary.innerHTML = `Invoice Total: ${fmtMoney(inv.grand||0)} ${inv.currency||"UGX"}  |  Already Paid: ${fmtMoney(paid)}  |  Balance Due: ${fmtMoney(bal)}`; } } // ── Auto-fill client name field for direct sales ─────────────────── function fillDirectClient() { const sel = document.getElementById("rct-direct-client-sel"); const nameEl = document.getElementById("rct-direct-name"); if(!sel || !nameEl) return; if(!sel.value) { nameEl.value=""; nameEl.readOnly=false; return; } const c = CLIENTS.find(c=>c.id===sel.value); if(c) { nameEl.value = c.name; nameEl.readOnly = true; } } // ── Gather receipt data ───────────────────────────────────────────── function gatherReceipt() { const g = id => (document.getElementById("rct-"+id)||{}).value||""; const amount = parseFloat(g("amount"))||0; const whtApplied = document.getElementById("rct-wht-chk").checked; const whtAmount = whtApplied ? Math.round(amount*0.06) : 0; const netCash = amount - whtAmount; if(RCT_TYPE==="invoice") { const idx = g("inv-sel"); if(idx==="") return {type:"invoice", invoiceIdx:null, amount, whtApplied, whtAmount, netCash, error:"no-invoice"}; const inv = INVOICES[parseInt(idx)]; return { type: "invoice", docNum: g("doc-num"), date: g("date"), invoiceIdx: parseInt(idx), invoiceDocNum: inv ? inv.docNum : "", clientId: inv ? inv.clientId : "", clientName: inv ? inv.clientName : "", clientCat: inv ? inv.clientCat : "", amount, whtApplied, whtAmount, netCash, method: g("method"), ref: g("ref"), receivedBy: g("receivedby"), notes: g("notes"), created: new Date().toISOString() }; } else { const dsel = document.getElementById("rct-direct-client-sel"); const clientId = dsel ? dsel.value : ""; const client = clientId ? CLIENTS.find(c=>c.id===clientId) : null; return { type: "direct", docNum: g("doc-num"), date: g("date"), invoiceIdx: null, clientId: clientId||"", clientName: g("direct-name") || (client?client.name:"Walk-in Customer"), clientCat: client ? client.category : "Walk-in", description: g("direct-desc"), amount, whtApplied, whtAmount, netCash, method: g("method"), ref: g("ref"), receivedBy: g("receivedby"), notes: g("notes"), created: new Date().toISOString() }; } } // ── Save Receipt (supports create + edit-in-place) ────────────────── function saveReceipt() { const data = gatherReceipt(); if(!data.amount || data.amount<=0) { alert("Please enter the amount received."); return; } if(data.type==="invoice" && data.invoiceIdx===null) { alert("Please select the invoice being paid, or switch to Direct Sale."); return; } if(data.type==="direct" && !data.clientName.trim()) { alert("Please enter a customer name for this direct sale."); return; } const editIdx = document.getElementById("rct-edit-idx").value; const isEdit = editIdx !== ""; const idx = isEdit ? parseInt(editIdx, 10) : null; let oldInvForPush = null; // If editing, first reverse the OLD receipt's effect on its linked invoice. if (isEdit) { const old = RECEIPTS[idx]; if (old && old.type==="invoice" && old.invoiceIdx!=null && INVOICES[old.invoiceIdx]) { const oldInv = INVOICES[old.invoiceIdx]; oldInv.paidAmount = Math.max((oldInv.paidAmount||0) - old.amount, 0); oldInv.status = oldInv.paidAmount >= (oldInv.grand||0)-0.5 ? "Paid" : (oldInv.paidAmount>0 ? "Partially Paid" : "Issued"); oldInvForPush = oldInv; } } // Apply the NEW receipt's effect on its linked invoice. if(data.type==="invoice") { const inv = INVOICES[data.invoiceIdx]; if(inv) { inv.paidAmount = (inv.paidAmount||0) + data.amount; if(inv.paidAmount >= (inv.grand||0) - 0.5) { inv.status = "Paid"; } else if(inv.paidAmount > 0) { inv.status = "Partially Paid"; } } } if(typeof renderDocLog==="function") renderDocLog("inv"); if(typeof updateDocKPIs==="function") updateDocKPIs("inv"); const whtDesc = data.whtApplied ? (" (net of 6% WHT: UGX "+fmtMoney(data.whtAmount)+")") : ""; const incDesc = (data.type==="invoice" ? ("Payment received for "+(data.invoiceDocNum||"invoice")) : ("Direct sale — "+(data.description||"walk-in"))) + whtDesc; if (isEdit) { RECEIPTS[idx] = data; // Find and update the mirrored income record (tagged with this receipt's doc number). const incIdx = incomeRecords.findIndex(r => r.source==="receipt" && r.ref===data.docNum); if (incIdx >= 0) { const rec = incomeRecords[incIdx]; rec.client = data.clientName; rec.desc = incDesc; rec.amount = data.amount; rec.date = data.date; rec.method = data.method; rec.by = currentUser ? currentUser.name : (data.receivedBy||""); rec.whtApplied = data.whtApplied; rec.whtAmount = data.whtAmount; rec.netCash = data.netCash; pushIncomeUpsert(rec); } alert("✅ "+data.docNum+" updated!"); } else { RECEIPTS.push(data); RCT_COUNTER++; // Auto-record this collection as income so it doesn't need a duplicate entry. const incRec = { id: "INC-" + String(incomeRecords.length+1).padStart(3,"0"), service: "supplies", client: data.clientName, desc: incDesc, amount: data.amount, date: data.date, method: data.method, ref: data.docNum, by: currentUser ? currentUser.name : (data.receivedBy||""), source: "receipt", whtApplied: data.whtApplied, whtAmount: data.whtAmount, netCash: data.netCash }; incomeRecords.push(incRec); pushIncomeUpsert(incRec); alert("✅ "+data.docNum+" saved!\nCustomer: "+data.clientName+"\nAmount: "+fmtMoney(data.amount)+ (data.whtApplied ? ("\nNet cash after 6% WHT: UGX "+fmtMoney(data.netCash)) : "") + "\nIncome recorded automatically — no need to enter it again."); } renderRCTLog(); updateRCTKPIs(); updateIncomeKPIs(); renderIncomeTable(); updateIncomeChart(); clearReceiptForm(); pushReceiptUpsert(data); // The linked invoice's paidAmount/status was changed above — push it (and, // if editing switched to a different invoice, the one it was un-applied from). if (isEdit && oldInvForPush) pushInvoiceUpsert(oldInvForPush); if (data.type==="invoice" && INVOICES[data.invoiceIdx]) pushInvoiceUpsert(INVOICES[data.invoiceIdx]); } // ── Edit / Cancel Edit ─────────────────────────────────────────────── function editReceipt(idx) { const r = RECEIPTS[idx]; if (!r) return; setReceiptType(r.type); const set = (id,v) => { const e=document.getElementById(id); if(e) e.value=v||""; }; set("rct-doc-num", r.docNum); set("rct-date", r.date); set("rct-amount", r.amount); set("rct-method", r.method); set("rct-ref", r.ref); set("rct-receivedby", r.receivedBy); set("rct-notes", r.notes); if (r.type==="invoice") { const sel = document.getElementById("rct-inv-sel"); if (sel) { // Ensure the currently-linked invoice appears as an option even if fully paid elsewhere. let found = false; for (const opt of sel.options) { if (opt.value == r.invoiceIdx) { found = true; break; } } if (!found) { const inv = INVOICES[r.invoiceIdx]; if (inv) { const o = document.createElement("option"); o.value = r.invoiceIdx; o.textContent = inv.docNum+" — "+inv.clientName; sel.appendChild(o); } } sel.value = r.invoiceIdx; } set("rct-cli-name", r.clientName); set("rct-cli-cat", r.clientCat); } else { set("rct-direct-name", r.clientName); set("rct-direct-desc", r.description); } document.getElementById("rct-edit-idx").value = idx; document.getElementById("rct-save-btn").textContent = "💾 Update Receipt"; document.getElementById("rct-cancel-btn").style.display = ""; document.getElementById("rct-wht-chk").checked = !!r.whtApplied; calcWHT("rct"); } function cancelReceiptEdit() { clearReceiptForm(); } // ── Clear form ───────────────────────────────────────────────────── function clearReceiptForm() { initReceiptForm(); document.getElementById("rct-edit-idx").value = ""; document.getElementById("rct-save-btn").textContent = "✅ Save Receipt"; document.getElementById("rct-cancel-btn").style.display = "none"; document.getElementById("rct-wht-chk").checked = false; document.getElementById("rct-wht-info").style.display = "none"; } // ── Render receipt log ────────────────────────────────────────────── function renderRCTLog() { const el = document.getElementById("rct-log"); if(!el) return; if(!RECEIPTS.length) { el.innerHTML=`
✅

No receipts yet.

`; return; } el.innerHTML=[...RECEIPTS].reverse().map((r,ri)=>{ const idx=RECEIPTS.length-1-ri; const tag = r.type==="direct" ? `DIRECT SALE` : `${r.invoiceDocNum||"INVOICE"}`; const whtTag = r.whtApplied ? `🧾 WHT 6%` : ""; return `
${r.docNum} ${tag}${whtTag}
${r.clientName} • ${r.date}
${fmtMoney(r.amount)} UGX
${r.method}
`; }).join(""); } // ── Update KPIs ──────────────────────────────────────────────────── function updateRCTKPIs() { const today = new Date().toISOString().slice(0,10); let collected=0, todayTotal=0; RECEIPTS.forEach(r=>{ collected += r.amount; if(r.date===today) todayTotal += r.amount; }); let outstanding=0; INVOICES.forEach(inv=>{ const bal=(inv.grand||0)-(inv.paidAmount||0); if(bal>0.5) outstanding++; }); const set=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v;}; set("rct-kpi-total", RECEIPTS.length); set("rct-kpi-collected", fmtMoney(collected)); set("rct-kpi-today", fmtMoney(todayTotal)); set("rct-kpi-outstanding", outstanding); } // ── PDF preview ──────────────────────────────────────────────────── function buildRCTHTML(data) { return `
RESTMED
${COMPANY_PROFILE.address}
Tel: ${COMPANY_PROFILE.phone1} / ${COMPANY_PROFILE.phone2} | ${COMPANY_PROFILE.email}
${COMPANY_PROFILE.website} | TIN: ${COMPANY_PROFILE.tin}

RECEIPT

${data.docNum}
Date: ${data.date}
${data.invoiceDocNum?`
Ref: ${data.invoiceDocNum}
`:`
Direct Sale
`}
Received From
${data.clientName}
${data.clientCat?`
${data.clientCat}
`:""} ${data.description?`
Items: ${data.description}
`:""}
Amount Received
UGX ${fmtMoney(data.amount)}
Payment Method
${data.method}
Reference
${data.ref||"—"}
${data.notes?`
Notes
${data.notes}
`:""}
Received By
${data.receivedBy||""}
Signature & Date
${COMPANY_PROFILE.name} | ${COMPANY_PROFILE.tagline} | ${COMPANY_PROFILE.website} | TIN: ${COMPANY_PROFILE.tin}
`; } function previewRCT(idx) { const data = RECEIPTS[idx]; if(!data) return; const modal = document.getElementById("rct-preview-modal"); const content = document.getElementById("rct-preview-content"); if(!modal||!content) return; content.innerHTML = buildRCTHTML(data); modal.style.display = "flex"; } function printRCTPreview() { const content = document.getElementById("rct-preview-content"); if(!content) return; const w = window.open("","_blank","width=800,height=700"); w.document.write(`RESTMED Receipt ${content.innerHTML} `); w.document.close(); } // ── Export receipt log ────────────────────────────────────────────── function exportReceiptLog() { if(!RECEIPTS.length){alert("No receipts to export.");return;} let csv="Doc No,Date,Type,Client,Ref Invoice,Amount,Method,Reference,Received By\n"; RECEIPTS.forEach(r=>{csv+=`${r.docNum},${r.date},${r.type},"${r.clientName}","${r.invoiceDocNum||""}",${Math.round(r.amount)},${r.method},"${r.ref||""}","${r.receivedBy||""}"\n`;}); const a=document.createElement("a"); a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"})); a.download="RESTMED_Receipts_"+new Date().toISOString().slice(0,10)+".csv"; a.click(); } // Close RCT modal on outside click document.addEventListener("click", e=>{ const m=document.getElementById("rct-preview-modal"); if(m && e.target===m) m.style.display="none"; }); // ═══════════════════════════════════════════════════════════════════ // PHASE 7 — FINANCE MODULE // Pulls together incomeRecords, expenses and RECEIPTS into a single // cash position view, plus Accounts Receivable from INVOICES. // ═══════════════════════════════════════════════════════════════════ function computeFinance() { const methods = ["Cash","Bank Transfer","Mobile Money","Cheque"]; const byMethod = {}; methods.forEach(m => byMethod[m] = {in:0, out:0}); const bucket = m => byMethod[m] ? m : (byMethod["Cash"] ? "Cash" : (methods[0])); let cashIn = 0, cashOut = 0, whtTotal = 0; // incomeRecords is the single source of truth for money in — it already // includes manual entries, receipt collections, and quick invoice payments. // (RECEIPTS is NOT summed separately here, to avoid double counting.) incomeRecords.forEach(r => { const net = (r.netCash != null) ? r.netCash : (r.amount||0); const m = bucket(r.method); byMethod[m].in += net; cashIn += net; whtTotal += (r.whtAmount||0); }); expenses.forEach(x => { const m = bucket(x.method); byMethod[m].out += (x.amt||0); cashOut += (x.amt||0); }); PURCHASE_ORDERS.forEach(po => { if(!po.paid) return; const m = bucket(po.paidMethod); byMethod[m].out += (po.grand||0); cashOut += (po.grand||0); }); // Accounts Receivable — invoices not yet fully paid const openStatuses = ["Issued","Open","Overdue","Partially Paid"]; const arInvoices = INVOICES .map((inv, idx) => ({ inv, idx })) .filter(({inv}) => openStatuses.includes(inv.status)); const arTotal = arInvoices.reduce((s,{inv}) => s + Math.max((inv.grand||0) - (inv.paidAmount||0), 0), 0); const today = new Date().toISOString().slice(0,10); const overdueCount = INVOICES.filter(inv => { if(!openStatuses.includes(inv.status)) return false; return inv.due && inv.due < today; }).length; // Accounts Payable — purchase orders not yet paid (Draft excluded — not yet committed) const apPOs = PURCHASE_ORDERS .map((po, idx) => ({ po, idx })) .filter(({po}) => (po.status==="Sent" || po.status==="Received") && !po.paid); const apTotal = apPOs.reduce((s,{po}) => s + (po.grand||0), 0); return { byMethod, cashIn, cashOut, arInvoices, arTotal, overdueCount, apPOs, apTotal, whtTotal }; } function renderFinance() { const f = computeFinance(); const set = (id, v) => { const el = document.getElementById(id); if(el) el.textContent = v; }; set("fin-cashin", "UGX " + fmtMoney(f.cashIn)); set("fin-cashout", "UGX " + fmtMoney(f.cashOut)); set("fin-netcash", "UGX " + fmtMoney(f.cashIn - f.cashOut)); set("fin-ar", "UGX " + fmtMoney(f.arTotal)); set("fin-ar-sub", f.arInvoices.length + " unpaid invoice" + (f.arInvoices.length===1?"":"s")); set("fin-overdue", f.overdueCount); set("fin-ap", "UGX " + fmtMoney(f.apTotal)); set("fin-ap-sub", f.apPOs.length + " unpaid PO" + (f.apPOs.length===1?"":"s")); set("fin-wht", "UGX " + fmtMoney(f.whtTotal)); // Cash by method const mtbody = document.getElementById("fin-method-tbody"); if(mtbody) { mtbody.innerHTML = Object.keys(f.byMethod).map(m => { const row = f.byMethod[m]; const net = row.in - row.out; const netColor = net >= 0 ? "#15803d" : "#dc2626"; return ` ${m} UGX ${fmtMoney(row.in)} UGX ${fmtMoney(row.out)} UGX ${fmtMoney(net)} `; }).join(""); } // Accounts Receivable table const artbody = document.getElementById("fin-ar-tbody"); if(artbody) { if(!f.arInvoices.length) { artbody.innerHTML = ` 🎉 No outstanding invoices — all clients are paid up.`; } else { const today = new Date().toISOString().slice(0,10); const statusBg={Issued:"#dbeafe",Open:"#fef9c3","Partially Paid":"#ffedd5",Overdue:"#fee2e2"}; const statusFg={Issued:"#1d4ed8",Open:"#854d0e","Partially Paid":"#c2410c",Overdue:"#dc2626"}; artbody.innerHTML = f.arInvoices.map(({inv}) => { const bal = Math.max((inv.grand||0)-(inv.paidAmount||0), 0); const isOverdue = inv.due && inv.due < today; const sb = statusBg[isOverdue?"Overdue":inv.status] || "#f3f4f6"; const sf = statusFg[isOverdue?"Overdue":inv.status] || "#6b7280"; return ` ${inv.docNum} ${inv.clientName} ${inv.date||""} ${inv.due||""} ${inv.currency||"UGX"} ${fmtMoney(inv.grand||0)} ${inv.currency||"UGX"} ${fmtMoney(inv.paidAmount||0)} ${inv.currency||"UGX"} ${fmtMoney(bal)} ${isOverdue?"Overdue":inv.status} `; }).join(""); } } renderFinanceLedger(); renderFinanceAP(); renderFinanceWHT(); } function renderFinanceWHT() { const tbody = document.getElementById("fin-wht-tbody"); if(!tbody) return; const whtRecords = incomeRecords.filter(r => r.whtApplied); if(!whtRecords.length) { tbody.innerHTML = ` No Withholding Tax deductions recorded yet.`; return; } tbody.innerHTML = [...whtRecords].reverse().map(r => ` ${r.date||""} ${r.client||""} ${r.ref||"—"} UGX ${fmtMoney(r.amount)} UGX ${fmtMoney(r.whtAmount)} UGX ${fmtMoney(r.netCash)} `).join(""); } function exportWHTReport() { const whtRecords = incomeRecords.filter(r => r.whtApplied); if(!whtRecords.length) { alert("No WHT deductions to export."); return; } let csv = "Date,Client,Reference,Gross Amount (UGX),WHT 6% (UGX),Net Cash Received (UGX)\n"; whtRecords.forEach(r => { csv += `${r.date},"${r.client}",${r.ref||""},${Math.round(r.amount)},${Math.round(r.whtAmount)},${Math.round(r.netCash)}\n`; }); const blob = new Blob([csv], {type:"text/csv"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "RESTMED_WHT_Report_" + new Date().toISOString().slice(0,10) + ".csv"; a.click(); } function renderFinanceAP() { const f = computeFinance(); const aptbody = document.getElementById("fin-ap-tbody"); if(!aptbody) return; if(!f.apPOs.length) { aptbody.innerHTML = ` 🎉 No outstanding purchase orders — all suppliers are paid up.`; return; } const statusBg={Sent:"#dbeafe",Received:"#dcfce7"}; const statusFg={Sent:"#1d4ed8",Received:"#15803d"}; aptbody.innerHTML = f.apPOs.map(({po, idx}) => { const sb = statusBg[po.status] || "#f3f4f6"; const sf = statusFg[po.status] || "#6b7280"; return ` ${po.docNum} ${po.clientName} ${po.date||""} ${po.status} ${po.currency||"UGX"} ${fmtMoney(po.grand||0)} `; }).join(""); } function buildLedgerEntries() { const entries = []; const sourceType = { manual:"income", receipt:"receipt", invoice:"invoice" }; incomeRecords.forEach(r => { const net = (r.netCash != null) ? r.netCash : (r.amount||0); const whtNote = r.whtApplied ? (" (net of 6% WHT: UGX "+fmtMoney(r.whtAmount)+")") : ""; entries.push({ date: r.date, type: sourceType[r.source] || "income", desc: (serviceLabels[r.service] || r.desc || "Income") + whtNote, party: r.client || "—", method: r.method, in: net, out: 0 }); }); expenses.forEach(x => entries.push({ date: x.date, type: "expense", desc: x.desc || x.cat, party: x.payee || "—", method: x.method, in: 0, out: x.amt||0 })); PURCHASE_ORDERS.forEach(po => { if(!po.paid) return; entries.push({ date: po.paidDate || po.date, type: "po_payment", desc: "Payment for "+po.docNum, party: po.clientName || "—", method: po.paidMethod, in: 0, out: po.grand||0 }); }); entries.sort((a,b) => (a.date||"").localeCompare(b.date||"")); return entries.reverse(); } function renderFinanceLedger() { const tbody = document.getElementById("fin-ledger-tbody"); if(!tbody) return; const filter = (document.getElementById("fin-ledger-filter")||{}).value || "all"; const from = (document.getElementById("fin-ledger-from")||{}).value || ""; const to = (document.getElementById("fin-ledger-to")||{}).value || ""; let entries = buildLedgerEntries(); if(filter !== "all") entries = entries.filter(e => e.type === filter); if(from) entries = entries.filter(e => e.date && e.date >= from); if(to) entries = entries.filter(e => e.date && e.date <= to); if(!entries.length) { tbody.innerHTML = ` No transactions recorded yet.`; return; } const typeBadge = { income: `Income`, receipt: `Receipt`, invoice: `Invoice Payment`, expense: `Expense`, po_payment: `PO Payment` }; tbody.innerHTML = entries.map(e => ` ${e.date||""} ${typeBadge[e.type]||e.type} ${e.desc||""} ${e.party||""} ${e.method||""} ${e.in ? "UGX "+fmtMoney(e.in) : ""} ${e.out ? "UGX "+fmtMoney(e.out) : ""} `).join(""); } function exportFinanceLedger() { const filter = (document.getElementById("fin-ledger-filter")||{}).value || "all"; const from = (document.getElementById("fin-ledger-from")||{}).value || ""; const to = (document.getElementById("fin-ledger-to")||{}).value || ""; let entries = buildLedgerEntries(); if(filter !== "all") entries = entries.filter(e => e.type === filter); if(from) entries = entries.filter(e => e.date && e.date >= from); if(to) entries = entries.filter(e => e.date && e.date <= to); let csv = "Date,Type,Description,Party,Method,Money In (UGX),Money Out (UGX)\n"; entries.forEach(e => { csv += `${e.date||""},${e.type},"${(e.desc||"").replace(/"/g,'""')}","${(e.party||"").replace(/"/g,'""')}",${e.method||""},${e.in?Math.round(e.in):0},${e.out?Math.round(e.out):0}\n`; }); const blob = new Blob([csv], {type:"text/csv"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "RESTMED_Finance_Ledger_" + new Date().toISOString().slice(0,10) + ".csv"; a.click(); } // ══════════════════════════════════════════════════════════════════ // PHASE 8 — FINANCIAL STATEMENTS // Profit & Loss and Cash Flow are period-based (from/to filter). // Balance Sheet is always a point-in-time snapshot of the current // live position (reuses computeFinance() for cash/AR/AP). // ══════════════════════════════════════════════════════════════════ function computeStatements(from, to) { const inRange = d => !!d && (!from || d>=from) && (!to || d<=to); const beforeFrom = d => !!d && !!from && d { if(inRange(inv.date)) invoicedSales += inv.grand||0; }); incomeRecords.forEach(r => { if(inRange(r.date) && (r.source==="manual" || !r.source)) otherIncome += r.amount||0; }); const totalRevenue = invoicedSales + otherIncome; // ── COGS (estimated at current inventory cost) ── let cogs = 0; INVOICES.forEach(inv => { if(!inRange(inv.date)) return; (inv.items||[]).forEach(it => { const invItem = INVENTORY.find(i=>i.code===it.code); cogs += (invItem ? invItem.cost : 0) * (it.qty||0); }); }); const grossProfit = totalRevenue - cogs; // ── Operating expenses by category ── const expByCat = {}; let totalExpenses = 0; expenses.forEach(x => { if(!inRange(x.date)) return; totalExpenses += x.amt||0; const c = x.cat || "Other"; expByCat[c] = (expByCat[c]||0) + (x.amt||0); }); const netProfit = grossProfit - totalExpenses; const netMargin = totalRevenue > 0 ? (netProfit/totalRevenue*100) : 0; // ── Cash flow (period, net of WHT — actual cash movements) ── let cfInManual=0, cfInReceipts=0, cfInInvoice=0, cfOutExpenses=0, cfOutPO=0, cfWht=0; incomeRecords.forEach(r => { if(!inRange(r.date)) return; const net = (r.netCash != null) ? r.netCash : (r.amount||0); if(r.source==="receipt") cfInReceipts += net; else if(r.source==="invoice") cfInInvoice += net; else cfInManual += net; cfWht += (r.whtAmount||0); }); expenses.forEach(x => { if(inRange(x.date)) cfOutExpenses += x.amt||0; }); PURCHASE_ORDERS.forEach(po => { if(po.paid && inRange(po.paidDate)) cfOutPO += po.grand||0; }); const totalCashIn = cfInManual + cfInReceipts + cfInInvoice; const totalCashOut = cfOutExpenses + cfOutPO; const netCashFlow = totalCashIn - totalCashOut; // Opening cash = net of every cash movement strictly before "from" let openingCash = 0; if (from) { incomeRecords.forEach(r => { if(!beforeFrom(r.date)) return; openingCash += (r.netCash != null) ? r.netCash : (r.amount||0); }); expenses.forEach(x => { if(beforeFrom(x.date)) openingCash -= x.amt||0; }); PURCHASE_ORDERS.forEach(po => { if(po.paid && beforeFrom(po.paidDate)) openingCash -= po.grand||0; }); } const closingCash = openingCash + netCashFlow; // ── Balance sheet (current live snapshot) ── const fin = computeFinance(); const cashBalance = fin.cashIn - fin.cashOut; const arTotal = fin.arTotal; const apTotal = fin.apTotal; const inventoryValue = INVENTORY.reduce((s,i) => s + (i.stock||0)*(i.cost||0), 0); const totalAssets = cashBalance + arTotal + inventoryValue; const totalLiabilities = apTotal; const equity = totalAssets - totalLiabilities; return { invoicedSales, otherIncome, totalRevenue, cogs, grossProfit, expByCat, totalExpenses, netProfit, netMargin, cfInManual, cfInReceipts, cfInInvoice, totalCashIn, cfOutExpenses, cfOutPO, totalCashOut, cfWht, netCashFlow, openingCash, closingCash, cashBalance, arTotal, apTotal, inventoryValue, totalAssets, totalLiabilities, equity }; } function setStatementPeriod(preset) { const today = new Date(); const y = today.getFullYear(), m = today.getMonth(); const iso = d => d.toISOString().slice(0,10); let from = "", to = ""; if (preset === "month") { from = iso(new Date(y, m, 1)); to = iso(today); } else if (preset === "lastmonth") { from = iso(new Date(y, m-1, 1)); to = iso(new Date(y, m, 0)); } else if (preset === "year") { from = iso(new Date(y, 0, 1)); to = iso(today); } else if (preset === "all") { from = ""; to = ""; } document.getElementById("st-from").value = from; document.getElementById("st-to").value = to; renderStatements(); } function renderStatements() { const from = (document.getElementById("st-from")||{}).value || ""; const to = (document.getElementById("st-to")||{}).value || ""; const s = computeStatements(from, to); const set = (id,v) => { const e=document.getElementById(id); if(e) e.textContent=v; }; const money = v => "UGX " + fmtMoney(v); const periodLabel = (from||to) ? ("Period: "+(from||"the beginning")+" to "+(to||"today")) : "All time (no period filter set)"; set("st-pl-period", periodLabel); set("st-cf-period", periodLabel); set("st-bs-asof", "As of "+new Date().toISOString().slice(0,10)+" — current live position, not limited to the period above"); // P&L set("st-pl-invoiced", money(s.invoicedSales)); set("st-pl-other", money(s.otherIncome)); set("st-pl-revenue", money(s.totalRevenue)); set("st-pl-cogs", money(s.cogs)); set("st-pl-gross", money(s.grossProfit)); const expTbody = document.getElementById("st-pl-expenses-tbody"); if(expTbody) { const cats = Object.keys(s.expByCat); expTbody.innerHTML = cats.length ? cats.sort((a,b)=>s.expByCat[b]-s.expByCat[a]).map(c => `${c}${money(s.expByCat[c])}` ).join("") : `No expenses recorded in this period`; } set("st-pl-totalexp", money(s.totalExpenses)); set("st-pl-net", money(s.netProfit)); set("st-pl-margin", s.netMargin.toFixed(1)+"%"); const netEl = document.getElementById("st-pl-net"); if(netEl) netEl.parentElement.parentElement.style.background = s.netProfit>=0 ? "#1B4D1E" : "#7f1d1d"; // Balance Sheet set("st-bs-cash", money(s.cashBalance)); set("st-bs-ar", money(s.arTotal)); set("st-bs-inv", money(s.inventoryValue)); set("st-bs-assets", money(s.totalAssets)); set("st-bs-ap", money(s.apTotal)); set("st-bs-liab", money(s.totalLiabilities)); set("st-bs-equity", money(s.equity)); set("st-bs-total", money(s.totalLiabilities + s.equity)); // Cash Flow set("st-cf-opening", money(s.openingCash)); set("st-cf-in-income", money(s.cfInManual)); set("st-cf-in-receipts", money(s.cfInReceipts)); set("st-cf-in-invoice", money(s.cfInInvoice)); set("st-cf-wht", money(s.cfWht)); set("st-cf-totalin", money(s.totalCashIn)); set("st-cf-out-expenses",money(s.cfOutExpenses)); set("st-cf-out-po", money(s.cfOutPO)); set("st-cf-totalout", money(s.totalCashOut)); set("st-cf-net", money(s.netCashFlow)); set("st-cf-closing", money(s.closingCash)); } function printStatements() { const content = document.getElementById("statements-print-area"); if(!content) return; const from = (document.getElementById("st-from")||{}).value || ""; const to = (document.getElementById("st-to")||{}).value || ""; const w = window.open("", "_blank", "width=900,height=700"); w.document.write(`RESTMED Financial Statements

RESTMED LAB CONSULT LTD — Financial Statements

Period: ${from||"beginning"} to ${to||"today"} | Generated ${new Date().toISOString().slice(0,10)}

${content.innerHTML} `); w.document.close(); } // ══════════════════════════════════════════════════════════════════ // PHASE 9 — REPORTS & CHARTS // Dependency-free: charts are hand-built SVG/HTML so the file stays // fully offline and self-contained. // ══════════════════════════════════════════════════════════════════ const CHART_COLORS = ["#1B4D1E","#B8860B","#2563eb","#7c3aed","#dc2626","#0d9488","#ea580c","#4338ca"]; function getMonthlyRevExp(monthsBack) { const arr = []; const now = new Date(); for (let i = monthsBack-1; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth()-i, 1); const y = d.getFullYear(), m = d.getMonth(); const from = new Date(y,m,1).toISOString().slice(0,10); const to = new Date(y,m+1,0).toISOString().slice(0,10); const label = d.toLocaleString("en-US",{month:"short"}) + " " + y; let rev=0, exp=0; INVOICES.forEach(inv => { if(inv.date>=from && inv.date<=to) rev += inv.grand||0; }); incomeRecords.forEach(r => { if(r.date>=from && r.date<=to) rev += r.amount||0; }); expenses.forEach(x => { if(x.date>=from && x.date<=to) exp += x.amt||0; }); arr.push({label, revenue:rev, expense:exp}); } return arr; } function getIncomeByCategory() { const by = {}; incomeRecords.forEach(r => { const label = serviceLabels[r.service] || r.service || "Other"; by[label] = (by[label]||0) + (r.amount||0); }); INVOICES.forEach(inv => { by["Invoiced Sales"] = (by["Invoiced Sales"]||0) + (inv.grand||0); }); return Object.keys(by).map(k => ({label:k, value:by[k]})).filter(x=>x.value>0).sort((a,b)=>b.value-a.value); } function getTopClientsByRevenue(n) { const by = {}; INVOICES.forEach(inv => { by[inv.clientName] = (by[inv.clientName]||0) + (inv.grand||0); }); return Object.keys(by).map(k => ({label:k, value:by[k]})).sort((a,b)=>b.value-a.value).slice(0,n); } function getTopSuppliersByPurchase(n) { return [...SUPPLIERS] .filter(s => (s.totalOrdered||0) > 0) .sort((a,b) => (b.totalOrdered||0)-(a.totalOrdered||0)) .slice(0,n) .map(s => ({label:s.name, value:s.totalOrdered||0})); } function getLowStockItems() { return INVENTORY.filter(i => (i.stock||0) <= (i.reorder||0)); } // ── SVG grouped bar chart (Revenue vs Expenses by month) ──────────── function buildGroupedBarSVG(data) { const W=560, H=240, padL=55, padB=34, padT=14, padR=10; const chartW = W-padL-padR, chartH = H-padT-padB; const max = Math.max(1, ...data.map(d=>Math.max(d.revenue,d.expense))); const groupW = chartW/data.length; const barW = Math.min(26, groupW*0.32); const yTicks = 4; let gridLines = ""; for(let i=0;i<=yTicks;i++){ const y = padT + chartH - (chartH*i/yTicks); const val = Math.round(max*i/yTicks); gridLines += ` ${fmtMoney(val)}`; } let bars=""; data.forEach((d,i)=>{ const cx = padL + groupW*i + groupW/2; const revH = (d.revenue/max)*chartH, expH=(d.expense/max)*chartH; const revX = cx - barW - 2, expX = cx + 2; bars += ` ${d.label}`; }); return ` ${gridLines}${bars}
Revenue Expenses
`; } // ── SVG donut chart ─────────────────────────────────────────────── function buildDonutSVG(data) { if(!data.length) return `

No income recorded yet.

`; const total = data.reduce((s,d)=>s+d.value,0) || 1; const size=170, r=65, cx=size/2, cy=size/2, circ=2*Math.PI*r; let offset=0, segs=""; data.forEach((d,i)=>{ const frac = d.value/total; const dash = frac*circ; segs += ``; offset += dash; }); const legend = data.map((d,i)=>`
${d.label}${((d.value/total)*100).toFixed(0)}%
`).join(""); return `${segs} Total ${fmtMoney(total)}
${legend}
`; } // ── HTML horizontal bar list (top clients / suppliers) ────────────── function buildHBarList(data, emptyMsg) { if(!data.length) return `

${emptyMsg}

`; const max = Math.max(...data.map(d=>d.value)) || 1; return data.map((d,i)=>`
${i+1}. ${d.label} UGX ${fmtMoney(d.value)}
`).join(""); } function renderReports() { const set = (id,v) => { const e=document.getElementById(id); if(e) e.textContent=v; }; const money = v => "UGX " + fmtMoney(v); // YTD KPIs const yFrom = new Date().getFullYear()+"-01-01"; const yTo = new Date().toISOString().slice(0,10); const s = computeStatements(yFrom, yTo); set("rpt-revenue-ytd", money(s.totalRevenue)); set("rpt-expense-ytd", money(s.totalExpenses)); set("rpt-net-ytd", money(s.totalRevenue - s.totalExpenses)); const invValue = INVENTORY.reduce((sum,i)=>sum+(i.stock||0)*(i.cost||0),0); set("rpt-inv-value", money(invValue)); const lowStock = getLowStockItems(); set("rpt-lowstock-count", lowStock.length); const converted = QUOTATIONS.filter(q=>q.status==="Converted").length; const rate = QUOTATIONS.length ? Math.round(converted/QUOTATIONS.length*100) : 0; set("rpt-conv-rate", rate+"%"); set("rpt-conv-sub", converted+" of "+QUOTATIONS.length+" quotations"); // Charts const trendEl = document.getElementById("rpt-trend-chart"); if(trendEl) trendEl.innerHTML = buildGroupedBarSVG(getMonthlyRevExp(6)); const donutEl = document.getElementById("rpt-income-donut"); if(donutEl) donutEl.innerHTML = buildDonutSVG(getIncomeByCategory()); const clientsEl = document.getElementById("rpt-top-clients"); if(clientsEl) clientsEl.innerHTML = buildHBarList(getTopClientsByRevenue(5), "No invoiced sales yet."); const suppliersEl = document.getElementById("rpt-top-suppliers"); if(suppliersEl) suppliersEl.innerHTML = buildHBarList(getTopSuppliersByPurchase(5), "No purchase orders received yet."); // Low stock table const lsBody = document.getElementById("rpt-lowstock-tbody"); if(lsBody) { lsBody.innerHTML = lowStock.length ? lowStock.map(i => ` ${i.code} ${i.desc} ${i.cat} ${i.stock||0} ${i.reorder||0} UGX ${fmtMoney((i.stock||0)*(i.cost||0))} `).join("") : `🎉 All items are above their reorder level.`; } } function exportReportsCSV() { let csv = "RESTMED LAB CONSULT LTD — Business Report\nGenerated," + new Date().toISOString().slice(0,10) + "\n\n"; csv += "Monthly Revenue vs Expenses\nMonth,Revenue (UGX),Expenses (UGX)\n"; getMonthlyRevExp(6).forEach(m => { csv += `${m.label},${Math.round(m.revenue)},${Math.round(m.expense)}\n`; }); csv += "\nIncome by Category\nCategory,Amount (UGX)\n"; getIncomeByCategory().forEach(d => { csv += `"${d.label}",${Math.round(d.value)}\n`; }); csv += "\nTop Clients by Revenue\nClient,Total Invoiced (UGX)\n"; getTopClientsByRevenue(10).forEach(d => { csv += `"${d.label}",${Math.round(d.value)}\n`; }); csv += "\nTop Suppliers by Purchase Value\nSupplier,Total Ordered (UGX)\n"; getTopSuppliersByPurchase(10).forEach(d => { csv += `"${d.label}",${Math.round(d.value)}\n`; }); csv += "\nLow Stock Items\nCode,Item,Category,In Stock,Reorder Level,Value at Cost (UGX)\n"; getLowStockItems().forEach(i => { csv += `${i.code},"${i.desc}",${i.cat},${i.stock||0},${i.reorder||0},${Math.round((i.stock||0)*(i.cost||0))}\n`; }); const blob = new Blob([csv], {type:"text/csv"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "RESTMED_Business_Report_" + new Date().toISOString().slice(0,10) + ".csv"; a.click(); } // ── Hook into showSection ────────────────────────────────────────── const _p5origShow = showSection; showSection = function(name) { _p5origShow(name); if(name==="quotations") initDocForm("qt"); if(name==="invoices") initDocForm("inv"); if(name==="delivery") initDeliveryForm(); if(name==="receipts") initReceiptForm(); if(name==="finance") renderFinance(); if(name==="purchases") initPOForm(); if(name==="statements") { if(!document.getElementById("st-from").value && !document.getElementById("st-to").value) { setStatementPeriod("month"); } else { renderStatements(); } } if(name==="reports") renderReports(); if(name==="income") { updateIncomeKPIs(); renderIncomeTable(); updateIncomeChart(); } if(name==="expenses") { updateExpenseKPIs(); renderExpensesTable(); renderExpenseCategoryBreakdown(); } }; // Close modals on outside click document.addEventListener("click", e=>{ ["qt-preview-modal","inv-preview-modal","po-preview-modal"].forEach(id=>{ const m=document.getElementById(id); if(m && e.target===m) m.style.display="none"; }); // Close suggestion dropdowns document.querySelectorAll("[id^='qt-sug-'],[id^='inv-sug-'],[id^='po-sug-']").forEach(el=>{ if(!el.contains(e.target)) el.style.display="none"; }); }); // Clear form input errors on type document.querySelectorAll(".form-input").forEach(inp => { inp.addEventListener("input", () => { inp.classList.remove("error"); document.getElementById("login-error").classList.remove("show"); }); });