(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 ``;
}).join('');
if (unassignedBills.length > 0) {
html += ``;
}
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 = `
${billsHtml || '
No bills.
'}
${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);