// Dealer Credits (branch portal, #dealer-credits): what the branch owes each dealer from credit purchases and // opening credit, and paying it off day by day. A payment settles the dealer's bills oldest first. // SalesServer: GET /api/dealer-ledger, GET /api/dealer-ledger/:id, POST /api/dealer-ledger/:id/pay. const DLC_METHODS = ['Cash', 'Gpay', 'Card', 'Bank Transfer', 'Cheque']; const DLC_ICON = { Cash: '💵', Gpay: '📱', Card: '💳', 'Bank Transfer': '🏦', Cheque: '🧾' }; // Which bills a payment would settle (oldest first, same as the server) function dlcAllocate(bills, amount) { let left = Math.round((Number(amount) || 0) * 100) / 100; const out = []; for (const b of bills) { if (left <= 0) break; if (!(b.outstanding > 0)) continue; const part = Math.min(b.outstanding, left); out.push({ label: b.label, amount: part, clears: part >= b.outstanding }); left = Math.round((left - part) * 100) / 100; } return out; } function DealerCredits({ salesUrl, token }) { const UI = window.DealerUI; const authToken = token || localStorage.getItem('branch_token') || ''; const headers = { Authorization: 'Bearer ' + authToken, 'Content-Type': 'application/json' }; const [dealers, setDealers] = React.useState([]); const [totals, setTotals] = React.useState(null); const [loading, setLoading] = React.useState(true); const [loadError, setLoadError] = React.useState(''); const [query, setQuery] = React.useState(''); const [onlyDues, setOnlyDues] = React.useState(true); const [selectedId, setSelectedId] = React.useState(() => { try { const id = sessionStorage.getItem('dealerCredits.selected') || ''; sessionStorage.removeItem('dealerCredits.selected'); return id; } catch (e) { return ''; } }); const [showDetail, setShowDetail] = React.useState(!!selectedId); // narrow screens: list ↔ detail const [ledger, setLedger] = React.useState(null); const [ledgerLoading, setLedgerLoading] = React.useState(false); const [tab, setTab] = React.useState('bills'); const [pay, setPay] = React.useState({ amount: '', method: 'Cash', paidOn: UI.todayInput(), note: '' }); const [paying, setPaying] = React.useState(false); const [payError, setPayError] = React.useState(''); const [toast, setToast] = React.useState(null); const flash = (type, text) => { setToast({ type, text }); setTimeout(() => setToast(null), 3500); }; const loadSummary = React.useCallback(async () => { setLoadError(''); try { const res = await fetch(salesUrl + '/api/dealer-ledger', { headers }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.message || 'Could not load dealer credits'); const list = Array.isArray(d.dealers) ? d.dealers : []; setDealers(list); setTotals(d.totals || null); // Opened from a dealer that is settled: show all dealers so it stays visible setSelectedId(cur => { const pick = cur && list.find(x => String(x._id) === String(cur)); if (pick && !(pick.outstanding > 0)) setOnlyDues(false); if (pick) return cur; const first = list.filter(x => x.outstanding > 0).sort((a, b) => b.outstanding - a.outstanding)[0] || list[0]; return first ? String(first._id) : ''; }); } catch (e) { setLoadError(e.message); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [salesUrl, authToken]); const loadLedger = React.useCallback(async (id) => { if (!id) { setLedger(null); return; } setLedgerLoading(true); try { const res = await fetch(salesUrl + '/api/dealer-ledger/' + encodeURIComponent(id), { headers }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.message || 'Could not load this dealer'); setLedger(d); } catch (e) { setLedger(null); flash('err', e.message); } finally { setLedgerLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [salesUrl, authToken]); React.useEffect(() => { loadSummary(); }, [loadSummary]); React.useEffect(() => { loadLedger(selectedId); setPay(p => ({ ...p, amount: '', note: '' })); setPayError(''); }, [selectedId, loadLedger]); const list = React.useMemo(() => { const q = query.trim().toLowerCase(); return dealers .filter(d => (!onlyDues || d.outstanding > 0 || String(d._id) === String(selectedId))) .filter(d => !q || [d.supplierName, d.agencyName, d.phoneNumber].some(v => String(v || '').toLowerCase().includes(q))) .sort((a, b) => b.outstanding - a.outstanding || UI.dealerName(a).localeCompare(UI.dealerName(b))); }, [dealers, query, onlyDues, selectedId]); const due = ledger ? ledger.totals.outstanding : 0; const amountNum = Number(pay.amount) || 0; const allocation = ledger ? dlcAllocate(ledger.bills, Math.min(amountNum, due)) : []; const submitPay = async (e) => { e.preventDefault(); setPayError(''); if (!(amountNum > 0)) { setPayError('Enter the amount paid'); return; } if (amountNum > due + 0.001) { setPayError(`That is more than the ${UI.money(due)} due`); return; } if (!pay.paidOn) { setPayError('Pick the payment date'); return; } setPaying(true); try { const [y, m, d] = pay.paidOn.split('-').map(Number); const now = new Date(); const paidOn = new Date(y, m - 1, d, now.getHours(), now.getMinutes(), now.getSeconds()); const res = await fetch(salesUrl + '/api/dealer-ledger/' + encodeURIComponent(selectedId) + '/pay', { method: 'POST', headers, body: JSON.stringify({ amount: amountNum, method: pay.method, note: pay.note.trim(), paidOn: (paidOn > now ? now : paidOn).toISOString() }) }); const out = await res.json().catch(() => ({})); if (!res.ok || !out.success) throw new Error(out.message || 'Payment not saved'); flash('ok', `${UI.money(amountNum)} paid to ${UI.dealerName(ledger.dealer)}${out.outstanding > 0 ? ` — ${UI.money(out.outstanding)} still due` : ' — fully settled'}`); setPay(p => ({ ...p, amount: '', note: '' })); setTab('payments'); await Promise.all([loadLedger(selectedId), loadSummary()]); } catch (err) { setPayError(err.message); } finally { setPaying(false); } }; const select = (id) => { setSelectedId(String(id)); setShowDetail(true); setTab('bills'); }; // Payments grouped by day, newest first const paymentDays = React.useMemo(() => { const groups = []; (ledger ? ledger.payments : []).forEach(p => { const key = UI.formatDate(p.paidOn); let g = groups.find(x => x.key === key); if (!g) { g = { key, total: 0, items: [] }; groups.push(g); } g.total += p.amount; g.items.push(p); }); return groups; }, [ledger]); const dealer = ledger && ledger.dealer; const paidPct = ledger && ledger.totals.totalCredit > 0 ? Math.min(100, (ledger.totals.totalPaid / ledger.totals.totalCredit) * 100) : 0; return (
Track what you owe each dealer and settle it day by day. Payments clear the oldest bills first.
| Bill | Date | Amount | Paid | Due | Status |
|---|---|---|---|---|---|
| {b.kind === 'opening' ? '📌 ' : '🧾 '}{b.label} | {UI.formatDate(b.billDate)} | {UI.money(b.totalAmount)} | {UI.money(b.paidAmount)} | {UI.money(b.outstanding)} | {b.outstanding <= 0 ? Settled : b.paidAmount > 0 ? Part paid : Unpaid} |
| Date | Entry | Credit | Paid | Balance |
|---|---|---|---|---|
| {UI.formatDate(s.date)} | {s.label} | {s.credit ? UI.money(s.credit) : ''} | {s.paid ? UI.money(s.paid) : ''} | {UI.money(s.balance)} |