(function (app) { const { state } = app; // --- Providers --- app.loadBills = async function () { const personParam = state.selectedPersonId ? `?personId=${state.selectedPersonId}` : ''; const [providersRes, summaryRes] = await Promise.all([ fetch(`${app.API}/providers${personParam}`), fetch(`${app.API}/bills/summary${personParam}`) ]); if (providersRes.ok) { state.currentProviders = await providersRes.json(); } if (summaryRes.ok) { const summary = await summaryRes.json(); app.loadBillSummary(summary); } await app.renderProviders(); }; app.loadBillSummary = function (summary) { const oopEl = document.getElementById('summaryBillOop'); const chargedEl = document.getElementById('summaryBillCharged'); const dueEl = document.getElementById('summaryBillDue'); if (oopEl) oopEl.textContent = app.formatCurrency(summary.totalPaid); if (chargedEl) chargedEl.textContent = 'of ' + app.formatCurrency(summary.totalCharged) + ' charged'; if (dueEl) { if (summary.totalDue > 0) { dueEl.textContent = app.formatCurrency(summary.totalDue) + ' due'; dueEl.style.display = ''; } else { dueEl.textContent = ''; dueEl.style.display = 'none'; } } const container = document.getElementById('billSummarySection'); if (!container) return; if (summary.totalCharged === 0) { container.innerHTML = ''; return; } const yearRows = summary.byYear.map(y => `
${y.year} ${app.formatCurrency(y.total)} (${y.count} bill${y.count !== 1 ? 's' : ''})
` ).join(''); const providerRows = summary.byProvider.map(p => `
${app.escapeHtml(p.providerName)} ${app.formatCurrency(p.total)} (${p.count})
` ).join(''); container.innerHTML = `
Charged by Year
${yearRows || '
No data
'}
Charged by Provider
${providerRows || '
No data
'}
`; }; app.renderProviders = async function () { const container = document.getElementById('providersList'); if (!container) return; // Fetch unassigned bills const unassignedBillParam = state.selectedPersonId ? `?personId=${state.selectedPersonId}` : ''; const unassignedRes = await fetch(`${app.API}/bills${unassignedBillParam}`); let unassignedBills = []; if (unassignedRes.ok) { const allBills = await unassignedRes.json(); unassignedBills = allBills.filter(b => !b.providerId); } if (state.currentProviders.length === 0 && unassignedBills.length === 0) { container.innerHTML = '
No billing providers yet. Add a provider to start tracking bills and payments.
'; return; } let html = state.currentProviders.map(p => { const statusClass = p.balance <= 0 ? 'paid' : p.totalPaid > 0 ? 'partial' : 'unpaid'; const statusLabel = p.balance <= 0 ? 'Paid' : p.totalPaid > 0 ? 'Partial' : 'Unpaid'; return `
${app.personBadge(p.personId)}${app.escapeHtml(p.name)} ${statusLabel}
${app.formatCurrency(p.totalCharged)} charged · ${app.formatCurrency(p.totalPaid)} paid · ${app.formatCurrency(p.balance)} balance · ${p.billCount} bill${p.billCount !== 1 ? 's' : ''}
${p.notes ? `
${app.escapeHtml(p.notes)}
` : ''}
`; }).join(''); if (unassignedBills.length > 0) { html += `
Bills without a provider
${unassignedBills.length} bill${unassignedBills.length !== 1 ? 's' : ''}
`; } container.innerHTML = html; }; app.toggleProvider = async function (providerId) { const section = document.getElementById(`provider-details-${providerId}`); const expandBtn = document.getElementById(`provider-expand-${providerId}`); if (section.style.display === 'none') { section.style.display = ''; expandBtn.innerHTML = '▼'; await app.loadProviderDetails(providerId); } else { section.style.display = 'none'; expandBtn.innerHTML = '▶'; } }; app.toggleUnassigned = async function () { const section = document.getElementById('provider-details-unassigned'); const expandBtn = document.getElementById('provider-expand-unassigned'); if (section.style.display === 'none') { section.style.display = ''; expandBtn.innerHTML = '▼'; await app.loadUnassignedBills(); } else { section.style.display = 'none'; expandBtn.innerHTML = '▶'; } }; app.loadProviderDetails = async function (providerId) { const section = document.getElementById(`provider-details-${providerId}`); if (!section) return; const billsParam = state.selectedPersonId ? `personId=${state.selectedPersonId}&` : ''; const [billsRes, paymentsRes] = await Promise.all([ fetch(`${app.API}/bills?${billsParam}providerId=${providerId}`), fetch(`${app.API}/providers/${providerId}/payments`) ]); let bills = []; let payments = []; if (billsRes.ok) bills = await billsRes.json(); if (paymentsRes.ok) payments = await paymentsRes.json(); const categoryOptions = ` `; const docOptions = state.currentDocuments.map(d => { const label = d.title || d.fileName || `Document #${d.id}`; return ``; }).join(''); const showForms = state.selectedPersonId ? '' : 'style="display:none"'; const billsHtml = bills.map(b => { const meta = []; if (b.billDate) meta.push(app.formatDate(b.billDate)); if (b.category) meta.push(app.formatLabel(b.category)); if (b.doctorName) meta.push('Dr. ' + b.doctorName); const docNames = b.documentNames ? `
Docs: ${app.escapeHtml(b.documentNames)}
` : ''; return `
${app.escapeHtml(b.summary || 'Bill')} ${app.formatCurrency(b.totalAmount)}
${meta.map(m => app.escapeHtml(m)).join(' · ')}
${docNames ? `
${docNames}
` : ''}
`; }).join(''); const paymentsHtml = payments.map(p => { const meta = []; if (p.paymentDate) meta.push(app.formatDate(p.paymentDate)); if (p.description) meta.push(p.description); return `
${app.formatCurrency(p.amount)} ${meta.length ? `${meta.map(m => app.escapeHtml(m)).join(' · ')}` : ''}
`; }).join(''); section.innerHTML = `
Bills
${billsHtml || '
No bills.
'}
Payments
${paymentsHtml || '
No payments recorded.
'}
`; }; app.loadUnassignedBills = async function () { const section = document.getElementById('provider-details-unassigned'); if (!section) return; const unassignedParam = state.selectedPersonId ? `?personId=${state.selectedPersonId}` : ''; const res = await fetch(`${app.API}/bills${unassignedParam}`); if (!res.ok) return; const allBills = await res.json(); const bills = allBills.filter(b => !b.providerId); const providerOptions = state.currentProviders.map(p => `` ).join(''); const billsHtml = bills.map(b => { const meta = []; if (b.billDate) meta.push(app.formatDate(b.billDate)); if (b.category) meta.push(app.formatLabel(b.category)); return `
${app.escapeHtml(b.summary || 'Bill')} ${app.formatCurrency(b.totalAmount)}
${meta.map(m => app.escapeHtml(m)).join(' · ')}
`; }).join(''); section.innerHTML = `
${billsHtml}
`; }; // --- Actions --- app.addProvider = async function () { const name = document.getElementById('newProviderName').value.trim(); if (!name) { alert('Provider name is required'); return; } const res = await fetch(`${app.API}/providers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ personId: state.selectedPersonId, name, notes: document.getElementById('newProviderNotes').value.trim() || null }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to add provider'); return; } document.getElementById('newProviderName').value = ''; document.getElementById('newProviderNotes').value = ''; await app.loadBills(); }; app.deleteProvider = async function (id) { if (!confirm('Delete this provider and unlink all its bills?')) return; const res = await fetch(`${app.API}/providers/${id}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to delete'); return; } await app.loadBills(); }; app.addBill = async function (providerId) { const amountStr = document.getElementById(`newBillAmount-${providerId}`).value; if (!amountStr || parseFloat(amountStr) <= 0) { alert('Amount must be greater than 0'); return; } const res = await fetch(`${app.API}/bills`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ personId: state.selectedPersonId, totalAmount: parseFloat(amountStr), summary: document.getElementById(`newBillSummary-${providerId}`).value.trim() || null, category: document.getElementById(`newBillCategory-${providerId}`).value || null, billDate: document.getElementById(`newBillDate-${providerId}`).value || null, providerId }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to add bill'); return; } document.getElementById(`newBillAmount-${providerId}`).value = ''; document.getElementById(`newBillSummary-${providerId}`).value = ''; document.getElementById(`newBillCategory-${providerId}`).value = ''; document.getElementById(`newBillDate-${providerId}`).value = ''; await app.refreshProvider(providerId); }; app.deleteBill = async function (id, providerId) { if (!confirm('Delete this bill and all its charges?')) return; const res = await fetch(`${app.API}/bills/${id}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to delete'); return; } if (providerId) { await app.refreshProvider(providerId); } else { await app.loadBills(); } }; app.addProviderPayment = async function (providerId) { const amountStr = document.getElementById(`provPayAmount-${providerId}`).value; if (!amountStr || parseFloat(amountStr) <= 0) { alert('Amount must be greater than 0'); return; } const res = await fetch(`${app.API}/providers/${providerId}/payments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: parseFloat(amountStr), paymentDate: document.getElementById(`provPayDate-${providerId}`).value || null, description: document.getElementById(`provPayDesc-${providerId}`).value.trim() || null }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to add payment'); return; } document.getElementById(`provPayAmount-${providerId}`).value = ''; document.getElementById(`provPayDate-${providerId}`).value = ''; document.getElementById(`provPayDesc-${providerId}`).value = ''; await app.refreshProvider(providerId); }; app.deleteProviderPayment = async function (id, providerId) { if (!confirm('Delete this payment?')) return; const res = await fetch(`${app.API}/provider-payments/${id}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to delete'); return; } await app.refreshProvider(providerId); }; app.toggleBillCharges = async function (billId) { const section = document.getElementById(`bill-charges-${billId}`); const expandBtn = document.getElementById(`bill-expand-${billId}`); if (section.style.display === 'none') { section.style.display = ''; expandBtn.innerHTML = '▼'; await app.loadCharges(billId); } else { section.style.display = 'none'; expandBtn.innerHTML = '▶'; } }; app.loadCharges = async function (billId) { const res = await fetch(`${app.API}/bills/${billId}/charges`); if (!res.ok) return; const charges = await res.json(); const container = document.getElementById(`chargeList-${billId}`); if (charges.length === 0) { container.innerHTML = '
No line items.
'; return; } container.innerHTML = charges.map(c => `
${app.escapeHtml(c.description)} ${app.formatCurrency(c.amount)}
` ).join(''); }; app.addCharge = async function (billId, providerId) { const desc = document.getElementById(`chargeDesc-${billId}`).value.trim(); const amountStr = document.getElementById(`chargeAmount-${billId}`).value; if (!desc) { alert('Description is required'); return; } if (!amountStr || parseFloat(amountStr) <= 0) { alert('Amount must be greater than 0'); return; } const res = await fetch(`${app.API}/bills/${billId}/charges`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: desc, amount: parseFloat(amountStr) }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to add charge'); return; } document.getElementById(`chargeDesc-${billId}`).value = ''; document.getElementById(`chargeAmount-${billId}`).value = ''; await app.loadCharges(billId); }; app.deleteCharge = async function (id, billId) { if (!confirm('Delete this charge?')) return; const res = await fetch(`${app.API}/bill-charges/${id}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to delete'); return; } await app.loadCharges(billId); }; app.linkDocumentToBill = async function (billId, providerId) { const select = document.getElementById(`linkDoc-${billId}`); const docId = select.value; if (!docId) { alert('Select a document to link'); return; } const res = await fetch(`${app.API}/bills/${billId}/documents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documentId: parseInt(docId) }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to link document'); return; } select.value = ''; await app.refreshProvider(providerId); }; app.assignBillToProvider = async function (billId) { const select = document.getElementById(`assignProvider-${billId}`); const providerId = select.value; if (!providerId) { alert('Select a provider'); return; } // Get current bill details first const assignParam = state.selectedPersonId ? `?personId=${state.selectedPersonId}` : ''; const getRes = await fetch(`${app.API}/bills${assignParam}`); if (!getRes.ok) return; const allBills = await getRes.json(); const bill = allBills.find(b => b.id === billId); if (!bill) return; const res = await fetch(`${app.API}/bills/${billId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ totalAmount: bill.totalAmount, summary: bill.summary, category: bill.category, billDate: bill.billDate, doctorId: bill.doctorId, providerId: parseInt(providerId) }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.error || 'Failed to assign bill'); return; } await app.loadBills(); }; app.refreshProvider = async function (providerId) { // Refresh the summary and provider header, then reload details const refreshParam = state.selectedPersonId ? `?personId=${state.selectedPersonId}` : ''; const [providersRes, summaryRes] = await Promise.all([ fetch(`${app.API}/providers${refreshParam}`), fetch(`${app.API}/bills/summary${refreshParam}`) ]); if (providersRes.ok) { state.currentProviders = await providersRes.json(); } if (summaryRes.ok) { const summary = await summaryRes.json(); app.loadBillSummary(summary); } // Update the provider header info const provider = state.currentProviders.find(p => p.id === providerId); if (provider) { const headerInfo = document.querySelector(`#provider-${providerId} .prescription-meta`); if (headerInfo) { headerInfo.textContent = ''; headerInfo.innerHTML = `${app.formatCurrency(provider.totalCharged)} charged · ${app.formatCurrency(provider.totalPaid)} paid · ${app.formatCurrency(provider.balance)} balance · ${provider.billCount} bill${provider.billCount !== 1 ? 's' : ''}`; } const statusBadge = document.querySelector(`#provider-${providerId} .bill-status`); if (statusBadge) { const statusClass = provider.balance <= 0 ? 'paid' : provider.totalPaid > 0 ? 'partial' : 'unpaid'; const statusLabel = provider.balance <= 0 ? 'Paid' : provider.totalPaid > 0 ? 'Partial' : 'Unpaid'; statusBadge.className = `bill-status ${statusClass}`; statusBadge.textContent = statusLabel; } } // Reload provider details if expanded const section = document.getElementById(`provider-details-${providerId}`); if (section && section.style.display !== 'none') { await app.loadProviderDetails(providerId); } }; // --- Window bindings --- window.medDocsAddProvider = () => app.addProvider(); window.medDocsDeleteProvider = (id) => app.deleteProvider(id); window.medDocsToggleProvider = (id) => app.toggleProvider(id); window.medDocsToggleUnassigned = () => app.toggleUnassigned(); window.medDocsAddBill = (providerId) => app.addBill(providerId); window.medDocsDeleteBill = (id, providerId) => app.deleteBill(id, providerId); window.medDocsToggleBillCharges = (billId) => app.toggleBillCharges(billId); window.medDocsAddCharge = (billId, providerId) => app.addCharge(billId, providerId); window.medDocsDeleteCharge = (id, billId) => app.deleteCharge(id, billId); window.medDocsAddProviderPayment = (providerId) => app.addProviderPayment(providerId); window.medDocsDeleteProviderPayment = (id, providerId) => app.deleteProviderPayment(id, providerId); window.medDocsLinkDocument = (billId, providerId) => app.linkDocumentToBill(billId, providerId); window.medDocsAssignBillToProvider = (billId) => app.assignBillToProvider(billId); })(MedDocs);