// 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 (

Dealer Credits

Track what you owe each dealer and settle it day by day. Payments clear the oldest bills first.

Total due
{totals ? UI.money(totals.outstanding) : '—'}
{totals ? `${totals.dealersWithDues} dealer${totals.dealersWithDues === 1 ? '' : 's'} to pay` : ' '}
Paid today
{totals ? UI.money(totals.paidToday) : '—'}
All dealers
Total paid
{totals ? UI.money(totals.totalPaid) : '—'}
Since the start
Credit taken
{totals ? UI.money(totals.totalCredit) : '—'}
Purchases + opening credit
{loading ? (
⏳
Loading dealer credits…
) : loadError ? (
⚠️
{loadError}
) : dealers.length === 0 ? (
💳
No dealers yet Add a dealer first. Credit purchases and opening credit will show up here.
) : (
{/* Dealer list */}
setQuery(e.target.value)} placeholder="Search dealer" aria-label="Search dealer" />
{list.length === 0 ? (
🎉
{onlyDues ? 'Nothing due' : 'No match'}{onlyDues ? 'Every dealer is fully paid.' : 'Try another search.'}
) : list.map(d => ( ))}
{/* Dealer detail */}
{!dealer ? (
👈
{ledgerLoading ? 'Loading…' : 'Choose a dealer'}
) : ( <>
{UI.initials(UI.dealerName(dealer))}
{UI.dealerName(dealer)}
{[dealer.supplierName && dealer.agencyName ? dealer.agencyName : '', dealer.phoneNumber, dealer.gstNumber ? 'GSTIN ' + dealer.gstNumber : ''].filter(Boolean).join(' · ') || 'No contact details'}
{dealer.phoneNumber ? 📞 Call : null}
Credit taken
{UI.money(ledger.totals.totalCredit)}
Paid
{UI.money(ledger.totals.totalPaid)}
Due now
0 ? 'var(--dl-wa)' : 'var(--dl-ok)' }}>{UI.money(due)}
{Math.round(paidPct)}% of all credit paid
Record a payment
{due > 0 ? {UI.money(due)} due : ✓ Fully settled}
{due > 0 ? ( <>
₹ { setPay(p => ({ ...p, amount: e.target.value })); setPayError(''); }} className={payError && /amount|more than/i.test(payError) ? 'bad' : ''} />
setPay(p => ({ ...p, paidOn: e.target.value }))} />
{DLC_METHODS.map(m => ( ))}
setPay(p => ({ ...p, note: e.target.value }))} />
{amountNum > 0 && amountNum <= due + 0.001 && allocation.length > 0 && (
This payment settles
{allocation.map((a, i) => (
{a.label} {a.clears ? cleared : part} {UI.money(a.amount)}
))}
Still due after this{UI.money(Math.max(0, due - amountNum))}
)} {payError &&
{payError}
}
) : (
Nothing is due to this dealer. New credit purchases from Purchase Inventory will appear here.
)}
{[['bills', `Bills (${ledger.bills.length})`], ['payments', `Payments (${ledger.payments.length})`], ['statement', 'Statement']].map(([k, l]) => ( ))}
{tab === 'bills' && (ledger.bills.length === 0 ? (
🧾
No credit billsBuy stock on credit from this dealer in Purchase Inventory, or add opening credit when creating a dealer.
) : (
{ledger.bills.map(b => ( ))}
BillDateAmountPaidDueStatus
{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}
))} {tab === 'payments' && (paymentDays.length === 0 ? (
💸
No payments yetPayments you record will be listed here day by day.
) : paymentDays.map(g => (
{g.key}Paid {UI.money(g.total)}
{g.items.map(p => (
{p.method || 'Payment'}{UI.money(p.amount)}
{p.note ?
{p.note}
: null}
{p.allocations.map(a => `${a.label} ${UI.money(a.amount)}`).join(' · ')}
))}
)))} {tab === 'statement' && (ledger.statement.length === 0 ? (
📄
No entries yet
) : (
{ledger.statement.map((s, i) => ( ))}
DateEntryCreditPaidBalance
{UI.formatDate(s.date)} {s.label} {s.credit ? UI.money(s.credit) : ''} {s.paid ? UI.money(s.paid) : ''} {UI.money(s.balance)}
))}
)}
)} {toast &&
{toast.text}
}
); } window.DealerCredits = DealerCredits;