`); w.document.close(); } return (
βœ…

Bill Saved!

{bill.bill_no}
{bill.patient_name} | Total: β‚Ή{Number(bill.total).toFixed(2)} {bill.pending>0.01 && (Pending: β‚Ή{Number(bill.pending).toFixed(2)})}
); } function ServicesHistory({ user }) { const [bills, setBills] = useState([]); const [dateFilter, setDateFilter]= useState(today()); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(false); const [expanded, setExpanded] = useState(null); useEffect(() => { load(); }, [dateFilter]); useEffect(() => { const h = () => load(); window.addEventListener('service-bill-saved', h); return () => window.removeEventListener('service-bill-saved', h); }, [dateFilter]); async function load() { setLoading(true); const params = search ? `?search=${encodeURIComponent(search)}` : `?date=${dateFilter}`; const d = await api.get('/api/service-bills' + params); setBills(Array.isArray(d) ? d : []); setLoading(false); } const totalToday = bills.reduce((s,b) => s + Number(b.total||0), 0); return (
{setDateFilter(e.target.value);setSearch('');}} /> setSearch(e.target.value)} onKeyDown={e=>e.key==='Enter'&&load()} />
Bills
{bills.length}
Total Amount
β‚Ή{totalToday.toFixed(2)}
{loading ?
Loading…
: bills.length === 0 ?
No bills found.
:
{bills.map(b => (
setExpanded(expanded===b.id?null:b.id)}>
{b.bill_no} {b.patient_name} {b.patient_phone && {b.patient_phone}}
β‚Ή{Number(b.total).toFixed(2)}
{b.pending_amount > 0.01 &&
Pending β‚Ή{Number(b.pending_amount).toFixed(2)}
}
{b.payment_mode}
{expanded === b.id && (
{(b.items||[]).map((it,idx) => ( ))}
Service Category Amount
{it.name} {it.category} β‚Ή{Number(it.price).toFixed(2)}
{b.discount > 0 &&
Discount: -β‚Ή{Number(b.discount).toFixed(2)}
} {b.doctor &&
Doctor: {b.doctor}
}
)}
))}
}
); } function ServicesMaster({ user }) { const [services, setServices] = useState([]); const [form, setForm] = useState({ name:'', code:'', price:'', category:'General', description:'', unit:'' }); const [editId, setEditId] = useState(null); const [filter, setFilter] = useState(''); const CATS = ['General','OP','Ward','Procedure','Ambulance','Pathology']; useEffect(() => { load(); }, []); async function load() { const d = await api.get('/api/hospital-services'); setServices(Array.isArray(d) ? d : []); } async function save() { if (!form.name.trim()) { alert('Name required'); return; } if (editId) { await api.put('/api/hospital-services/' + editId, form); } else { await api.post('/api/hospital-services', form); } setForm({ name:'', code:'', price:'', category:'General', description:'', unit:'' }); setEditId(null); load(); } async function del(id) { if (!confirm('Delete this service?')) return; await api.del('/api/hospital-services/' + id); load(); } function startEdit(s) { setForm({ name:s.name, code:s.code||'', price:s.price||'', category:s.category||'General', description:s.description||'', unit:s.unit||'' }); setEditId(s.id); } const filtered = services.filter(s => !filter || s.category === filter || s.name.toLowerCase().includes(filter.toLowerCase())); const grouped = CATS.reduce((acc,c) => { acc[c] = filtered.filter(s=>s.category===c); return acc; }, {}); return (
{/* Form */}
{editId ? '✏️ Edit Service' : 'βž• Add New Service'}
setForm(f=>({...f,name:e.target.value}))} /> setForm(f=>({...f,code:e.target.value}))} /> setForm(f=>({...f,price:e.target.value}))} min="0" /> setForm(f=>({...f,unit:e.target.value}))} /> setForm(f=>({...f,description:e.target.value}))} />
{editId && }
{/* Filter */}
{CATS.map(c => services.filter(s=>s.category===c).length > 0 && ( ))}
{/* Table */} {Object.entries(grouped).filter(([,svcs])=>svcs.length>0).map(([cat,svcs]) => (
{cat} ({svcs.length})
{svcs.map(s => ( ))}
Name Code Price Unit
{s.name} {s.code||'β€”'} β‚Ή{Number(s.price).toFixed(2)} {s.unit||'β€”'}
))}
); } // ══════════════════════════════════════════════════════════════════ // END HOSPITAL SERVICES BILLING // ══════════════════════════════════════════════════════════════════ function LabPage({ settings, user }) { const { branchInfo } = React.useContext(BranchContext); const labName = branchInfo?.lab_name || (branchInfo?.name ? branchInfo.name + ' Diagnostics' : 'Lab Management'); const [tab, setTab] = useState('billing'); // Admin/SuperAdmin/AppRoles: full access const isAdminLevel = user.role === 'admin' || user?.is_super_admin || user?.is_app_role; const isLabUser = user.role === 'lab'; // Lab Admin/Manager designation also gets elevated access const labDesigAccess = getDeptAdminAccess(user); const isLabDesigAdmin = labDesigAccess.isDeptAdmin && labDesigAccess.deptRole === 'lab'; const isLabAdminAny = isAdminLevel || isLabDesigAdmin; // full lab admin: role OR designation const canBill = isAdminLevel || isLabUser || isLabDesigAdmin; // Tests edit: admin level + lab designation admin + lab user with can_edit flag const canEditTests = isLabAdminAny || (isLabUser && user?.can_edit); const tabs = [ { id: 'billing', label: '🧾 Billing', show: canBill }, { id: 'tests', label: 'πŸ”¬ Tests List', show: isLabAdminAny || isLabUser }, { id: 'history', label: 'πŸ“‹ History', show: isLabAdminAny || isLabUser }, { id: 'profit', label: 'πŸ“Š Profit & Charts', show: isLabAdminAny }, // lab designation admin can see profit ]; return (
πŸ§ͺ

{labName}

{branchInfo?.name &&
{branchInfo.name} Branch
}
{tabs.filter(t => t.show).map(t => ( ))}
{tab === 'billing' && canBill && } {tab === 'tests' && (isLabAdminAny||isLabUser) && } {tab === 'history' && (isLabAdminAny||isLabUser) && } {tab === 'profit' && isLabAdminAny && }
); } // ── LAB BILLING ─────────────────────────────────────────────────────────────── function LabBilling({ settings, user }) { const [patient, setPatient] = useState({ name: '', phone: '', age: '', gender: 'Male', doctor: '' }); const [patientLocked,setPatientLocked]= useState(false); // true once a patient is selected const [showNewPat, setShowNewPat] = useState(false); // new patient modal const [patSource, setPatSource] = useState('Internal'); // Internal = referred from OP/IP, External = walk-in const [patSearch, setPatSearch] = useState(''); // search existing patient masters const [patResults,setPatResults]= useState([]); const [branch, setBranch] = useState(user?.hospital_branch || ''); const [branches, setBranches] = useState([]); const [testQuery, setTestQuery] = useState(''); const [testDrop, setTestDrop] = useState([]); const [items, setItems] = useState([]); const [discount, setDiscount] = useState(''); const [payMode, setPayMode] = useState('Cash'); const [saving, setSaving] = useState(false); const [aiModal, setAiModal] = useState(false); const [savedBill, setSavedBill] = useState(null); const [labPaid, setLabPaid] = useState('0'); // default zero = full balance shown const [labPayRemarks, setLabPayRemarks] = useState(''); const dropRef = useRef(null); const debRef = useRef(null); const isAdminMgr = ['admin','manager'].includes(user?.role); const { branchInfo: labBranchInfo } = React.useContext(BranchContext); // Lab role and lab admin/manager designation can search patients across all branches const labUserDeptAccess = getDeptAdminAccess(user); const isLabUser = user?.role === 'lab' || (labUserDeptAccess.isDeptAdmin && labUserDeptAccess.deptRole === 'lab'); const patSearchBranch = isLabUser ? '' : branch; // no branch filter for lab users const labBranchInfo2 = labBranchInfo || {}; const discountMax = +(labBranchInfo2.lab_discount_max || settings?.lab_discount_max || 0); const discountValues = ((labBranchInfo2.lab_discount_values || settings?.lab_discount_values || '')).split(',').map(v=>v.trim()).filter(Boolean); // GST rates from branch settings β€” only apply if branch has a GST number (legal compliance) const labHasGST = !!(labBranchInfo2.gst_number); const labCgstRate = labHasGST ? +(labBranchInfo2.cgst_rate || 0) : 0; const labSgstRate = labHasGST ? +(labBranchInfo2.sgst_rate || 0) : 0; // Discount / price edit permission: admin/manager role + super admin + app roles + Lab Admin/Manager designation const labDeptAccess = getDeptAdminAccess(user); const canEditLabDiscount = isAdminMgr || user?.is_super_admin || user?.is_app_role || (labDeptAccess.isDeptAdmin && labDeptAccess.deptRole === 'lab'); // Price edit per test: same permission set const canEditTestPrice = canEditLabDiscount; useEffect(() => { api.get('/api/hospital-infos').then(d => { if (Array.isArray(d)) { const active = d.filter(b=>b.is_active); const _LABAPR = ['app_owner','app_developer','app_tester','app_designer']; if (user?.role === 'admin' || user?.is_super_admin || user?.is_app_role || _LABAPR.includes(user?.role)) { setBranches(active); } else { const userBranches = (user?.hospital_branch||'').split(',').map(s=>s.trim()).filter(Boolean); setBranches(userBranches.length ? active.filter(b => userBranches.includes(b.name)) : active); } } }); }, []); // Close test dropdown on outside click useEffect(() => { function h(e) { if (dropRef.current && !dropRef.current.contains(e.target)) setTestDrop([]); } document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, []); function handleTestSearch(e) { const q = e.target.value; setTestQuery(q); clearTimeout(debRef.current); if (!q.trim()) { setTestDrop([]); return; } debRef.current = setTimeout(async () => { const d = await api.get('/api/lab/tests?active_only=1&search=' + encodeURIComponent(q)); if (Array.isArray(d)) setTestDrop(d); }, 250); } function addTest(t) { if (items.find(i => i.id === t.id)) { setTestQuery(''); setTestDrop([]); return; } setItems(prev => [...prev, { id: t.id, name: t.name, code: t.code, price: t.price, unit: t.unit||'', min_value: t.min_value||'', max_value: t.max_value||'', normal_range: t.normal_range||'' }]); setTestQuery(''); setTestDrop([]); } function removeItem(id) { setItems(prev => prev.filter(i => i.id !== id)); } const subtotal = items.reduce((s, i) => s + Number(i.price || 0), 0); const disc = Number(discount) || 0; const afterDisc = Math.max(0, subtotal - disc); const cgstAmt = +(afterDisc * (labCgstRate / 100)).toFixed(2); const sgstAmt = +(afterDisc * (labSgstRate / 100)).toFixed(2); const total = +(afterDisc + cgstAmt + sgstAmt).toFixed(2); const [billDate, setBillDate] = useState(today()); const [billTime, setBillTime] = useState(nowTime ? nowTime() : new Date().toTimeString().slice(0,5)); async function saveBill() { if (!patient.name.trim()) { alert('Patient name required'); return; } if (!items.length) { alert('Add at least one test'); return; } setSaving(true); const paid = labPaid === '' ? total : +labPaid; // empty = full payment; 0 = nothing paid const payload = { patient_name: patient.name, patient_phone: patient.phone, patient_age: patient.age || '', patient_gender: patient.gender || 'Male', doctor: patient.doctor, items, discount: disc, payment_mode: payMode, total, branch, bill_date: billDate, bill_time: billTime, patient_source: patSource, paid_amount: paid, payment_remarks: labPayRemarks, cgst_rate: labCgstRate, sgst_rate: labSgstRate }; const d = await api.post('/api/lab/bills', payload); setSaving(false); if (d && (d.id || d.bill_no) && !d.error) { setSavedBill({ ...payload, bill_no: d.bill_no, id: d.id, date: billDate, created_by: user?.employeeId || user?.employee_id || '', created_by_name: user?.name || '' }); setItems([]); setDiscount('0'); setLabPaid('0'); setLabPayRemarks(''); setPatient({ name: '', phone: '', age: '', gender: 'Male', doctor: '' }); setPatientLocked(false); // Trigger history refresh via custom event window.dispatchEvent(new CustomEvent('lab-bill-saved')); } else { alert(d?.error || 'Save failed. Check console for details.'); console.error('Lab bill save failed:', d); } } function printLabBill(bill) { const b = { ...bill, net_total: bill.total }; let labBarUrl = ''; try { const c = document.createElement('canvas'); JsBarcode(c, b.bill_no||'LB', {format:'CODE128',width:1.5,height:28,displayValue:false,margin:2}); labBarUrl = c.toDataURL(); } catch(e) {} printHTML(buildBillHTML('lab', b, settings, null, labBarUrl)); } return (
{/* Branch + Patient */}
{branch && (() => { const bi = branches.find(b => b.name === branch); return bi ?
πŸ“ {bi.address||''} {bi.phone?`Β· πŸ“ž ${bi.phone}`:''}
: null; })()}
setBillDate(e.target.value)} />
setBillTime(e.target.value)} />

Patient Details

{/* Patient Source toggle */} {['Internal','External'].map(s=>( ))} {!patientLocked && } {patientLocked && }
{!patientLocked ? (
{ setPatient({ name: p.name, phone: p.phone||'', age: p.age?String(p.age):'', gender: p.gender||'Male', doctor: p.doctor||'' }); setPatientLocked(true); }} branch={patSearchBranch} placeholder={isLabUser ? "Search any patient (all branches)..." : "Search existing patient by name/phone..."} />
Search for an existing patient above, or click "Add New Patient".
) : ( /* Locked patient info β€” read-only display */
{patient.name}
{patient.phone && πŸ“ž {patient.phone}} {patient.age && Age: {patient.age}} {patient.gender && {patient.gender}}
βœ“ Patient Selected
{/* Referred By still editable */}
setPatient(v => ({ ...v, doctor: e.target.value }))} placeholder="Doctor / referrer" />
)}
{/* Test Search */}

Add Tests

{testDrop.length > 0 && (
{testDrop.map(t => (
addTest(t)}> {t.name} {t.code && {t.code}} {fmt(t.price)}
))}
)}
{/* Items Table */} {items.length > 0 && (
{canEditTestPrice && } {items.map((it, idx) => ( {canEditTestPrice && ( )} ))}
# Test Name Code Price (β‚Ή)Set PriceRemove
{idx + 1} {it.name} {it.code || 'β€”'} {fmt(it.price)} { const newPrice = +e.target.value || 0; setItems(prev => prev.map((i, i2) => i2 === idx ? { ...i, price: newPrice } : i)); }} title="Override price for this bill" />
)} {/* Totals */}
{!canEditLabDiscount ? (
No discount β€” contact Lab Admin
) : discountValues.length > 0 ? ( ) : ( {const v=+e.target.value; if(!discountMax||v<=discountMax) setDiscount(e.target.value); else setDiscount(String(discountMax));}} placeholder="0" /> )}
setPayMode(e.target.value)} options={['Cash','UPI','Card','Online','Credit']} />
Subtotal: {fmt(subtotal)}
{disc > 0 &&
Discount:- {fmt(disc)}
} {labCgstRate > 0 &&
CGST ({labCgstRate}%):+ {fmt(cgstAmt)}
} {labSgstRate > 0 &&
SGST ({labSgstRate}%):+ {fmt(sgstAmt)}
}
Final Total (incl. GST):{fmt(total)}
{/* Part Payment */} {(() => { // Default 0 = nothing paid = full balance due; empty = full payment; other value = partial const paidAmt = labPaid === '' ? total : +labPaid; const balanceDue = Math.max(0, total - paidAmt); const isPaid = balanceDue <= 0; return (
πŸ’³ Payment Details
setLabPaid(e.target.value)} placeholder="0.00" />
0 = unpaid (full balance) Β· Leave empty for full payment
β‚Ή{balanceDue.toFixed(2)}
{isPaid ? 'βœ“ Fully Paid' : 'Pending'}
β‚Ή{total.toFixed(2)}
setLabPayRemarks(e.target.value)} placeholder="UPI ref / cheque no." />
); })()} {/* Action Buttons */}
{savedBill && ( )}
{savedBill && (
βœ… Bill saved! Bill No: {savedBill.bill_no}
)}
{aiModal && setAiModal(false)} billId={savedBill?.id} />} {showNewPat && ( { setPatient({ name: p.name, phone: p.phone, age: p.age||'', gender: p.gender||'Male', doctor: '' }); setPatientLocked(true); setShowNewPat(false); }} onClose={() => setShowNewPat(false)} /> )}
); } // ── LAB AI ANALYSIS MODAL ───────────────────────────────────────────────────── function LabAIModal({ onClose, billId }) { const [text, setText] = useState(''); const [result, setResult] = useState(''); const [loading, setLoading] = useState(false); const [saved, setSaved] = useState(false); async function analyze() { if (!text.trim()) return; setLoading(true); const d = await api.post('/api/ai/analyze-lab', { results_text: text, bill_id: billId }); setResult(d.analysis || d.message || JSON.stringify(d)); setLoading(false); } async function saveAnalysis() { if (!result || !billId) return; await api.post('/api/lab/bills/' + billId + '/analysis', { analysis: result }); setSaved(true); } return (

Paste the test results / values below and click Analyze. The AI will interpret the findings.