+ {/* Draft Restored Banner */}
+ {isRestoredBannerVisible && (
+
+ )}
+
+ {/* Editing Invoice Banner */}
+ {editingInvoiceBanner && (
+
+ )}
+
{/* Patient Information Panel */}
@@ -2253,18 +2557,36 @@ function PosCounter({ store, notify }) {
{patientType}
+ {draftStatus === 'saved' && (
+
+ Draft Auto-Saved ({cart.length} items)
+
+ )}
+
+
+ {(cart.length > 0 || selectedPatient) && (
+
+ )}
+ {lastInvoice && (
+
+ )}
- {lastInvoice && (
-
- )}
{selectedPatient && (
+ );
+ }
+
+ const [invoices, setInvoices] = useState([]);
+ const [stats, setStats] = useState({ today_count: 0, today_amount: 0, total_count: 0, total_amount: 0, total_returns: 0 });
+ const [pagination, setPagination] = useState({ current_page: 1, per_page: 25, total_records: 0, total_pages: 1 });
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Filters
+ const [searchQuery, setSearchQuery] = useState('');
+ const [datePreset, setDatePreset] = useState('all'); // 'today', 'yesterday', 'last7', 'month', 'all'
+ const [fromDate, setFromDate] = useState('');
+ const [toDate, setToDate] = useState('');
+ const [paymentMode, setPaymentMode] = useState('ALL');
+ const [page, setPage] = useState(1);
+ const [limit, setLimit] = useState(25);
+
+ // Selected invoice for View/Print Drawer
+ const [viewInvoice, setViewInvoice] = useState(null);
+ const [isPrintOpen, setIsPrintOpen] = useState(false);
+ const [printFormat, setPrintFormat] = useState('thermal'); // 'thermal', 'a5', 'a4'
+ const [isLoadingView, setIsLoadingView] = useState(false);
+
+ // Quick Edit Details Modal state
+ const [editModalSale, setEditModalSale] = useState(null);
+ const [editForm, setEditForm] = useState({
+ patient_name: '',
+ patient_mobile: '',
+ patient_address: '',
+ age: '',
+ gender: '',
+ doctor_name: '',
+ doctor_reg_no: '',
+ payment_mode: 'Cash',
+ payment_reference: '',
+ bank_name: '',
+ upi_ref_no: '',
+ card_ref_no: '',
+ remarks: ''
+ });
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ const loadInvoices = async (targetPage = page, customSearch = searchQuery) => {
+ setIsLoading(true);
+ try {
+ const params = new URLSearchParams({
+ store_id: store.store_id || 0,
+ search: customSearch !== undefined ? customSearch : searchQuery,
+ from_date: fromDate || '',
+ to_date: toDate || '',
+ payment_mode: paymentMode || 'ALL',
+ page: targetPage,
+ limit: limit
+ });
+ const res = await fetch(API_BASE + 'sales/list?' + params.toString());
+ const data = await res.json();
+ if (data.status) {
+ setInvoices(data.sales || []);
+ if (data.stats) setStats(data.stats);
+ if (data.pagination) setPagination(data.pagination);
+ setPage(targetPage);
+ } else {
+ notify(data.message || 'Failed to load invoices.', 'error');
+ }
+ } catch (e) {
+ notify('Error fetching sales list: ' + e.message, 'error');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadInvoices(1);
+ }, [store?.store_id, fromDate, toDate, paymentMode, limit]);
+
+ const handleDatePreset = (preset) => {
+ setDatePreset(preset);
+ const today = new Date();
+ const formatDate = (d) => d.toISOString().split('T')[0];
+
+ if (preset === 'today') {
+ const todayStr = formatDate(today);
+ setFromDate(todayStr);
+ setToDate(todayStr);
+ } else if (preset === 'yesterday') {
+ const yest = new Date(today);
+ yest.setDate(yest.getDate() - 1);
+ const yestStr = formatDate(yest);
+ setFromDate(yestStr);
+ setToDate(yestStr);
+ } else if (preset === 'last7') {
+ const past7 = new Date(today);
+ past7.setDate(past7.getDate() - 7);
+ setFromDate(formatDate(past7));
+ setToDate(formatDate(today));
+ } else if (preset === 'month') {
+ const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
+ setFromDate(formatDate(firstDay));
+ setToDate(formatDate(today));
+ } else if (preset === 'all') {
+ setFromDate('');
+ setToDate('');
+ }
+ };
+
+ const handleSearchSubmit = (e) => {
+ if (e) e.preventDefault();
+ loadInvoices(1, searchQuery);
+ };
+
+ const clearSearch = () => {
+ setSearchQuery('');
+ loadInvoices(1, '');
+ };
+
+ // Open View / Print Drawer
+ const openPrintDrawer = async (saleId) => {
+ setIsLoadingView(true);
+ try {
+ const res = await fetch(API_BASE + 'sales/invoice/' + saleId);
+ const data = await res.json();
+ if (data.status) {
+ setViewInvoice(data);
+ setIsPrintOpen(true);
+ } else {
+ notify(data.message || 'Invoice details not found.', 'error');
+ }
+ } catch (e) {
+ notify('Error loading invoice: ' + e.message, 'error');
+ } finally {
+ setIsLoadingView(false);
+ }
+ };
+
+ // Open Edit Details Modal
+ const openEditModal = (sale) => {
+ setEditModalSale(sale);
+ setEditForm({
+ patient_name: sale.patient_name || '',
+ patient_mobile: sale.patient_mobile || '',
+ patient_address: sale.patient_address || '',
+ age: sale.age || '',
+ gender: sale.gender || '',
+ doctor_name: sale.doctor_name || '',
+ doctor_reg_no: sale.doctor_reg_no || '',
+ payment_mode: sale.payment_mode || 'Cash',
+ payment_reference: sale.payment_reference || '',
+ bank_name: sale.bank_name || '',
+ upi_ref_no: sale.upi_ref_no || '',
+ card_ref_no: sale.card_ref_no || '',
+ remarks: sale.reconciled_notes || ''
+ });
+ };
+
+ const handleSaveEdit = async (e) => {
+ e.preventDefault();
+ if (!editModalSale) return;
+ setIsUpdating(true);
+ try {
+ const res = await fetch(API_BASE + 'sales/update', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ sale_id: editModalSale.sale_id,
+ ...editForm
+ })
+ });
+ const data = await res.json();
+ if (data.status) {
+ notify(`Invoice {editModalSale.invoice_no} updated successfully!`, 'success');
+ setEditModalSale(null);
+ loadInvoices(page);
+ } else {
+ notify(data.message || 'Failed to update invoice.', 'error');
+ }
+ } catch (err) {
+ notify('Error saving invoice update: ' + err.message, 'error');
+ } finally {
+ setIsUpdating(false);
+ }
+ };
+
+ // Load into POS Counter Desk for direct re-billing / line items editing
+ const handleEditInPos = async (saleId) => {
+ setIsLoadingView(true);
+ try {
+ const res = await fetch(API_BASE + 'sales/invoice/' + saleId);
+ const data = await res.json();
+ if (data.status && data.sale) {
+ if (onEditInPos) {
+ onEditInPos(data);
+ }
+ } else {
+ notify(data.message || 'Failed to load invoice for POS edit.', 'error');
+ }
+ } catch (e) {
+ notify('Error: ' + e.message, 'error');
+ } finally {
+ setIsLoadingView(false);
+ }
+ };
+
+ const printThermalReceipt = () => {
+ if (!viewInvoice || !viewInvoice.sale) return;
+ const txt = generateThermalReceiptText(viewInvoice);
+ const win = window.open('', '_blank', 'width=420,height=650');
+ if (!win) {
+ notify('Pop-up blocked. Please allow pop-ups to print thermal receipt.', 'warning');
+ return;
+ }
+ try {
+ win.document.title = 'Thermal Receipt - ' + (viewInvoice.sale ? viewInvoice.sale.invoice_no : '');
+ const style = win.document.createElement('style');
+ style.textContent = '@page { size: 80mm auto; margin: 0; } html, body { margin: 0; padding: 0; background: #fff; } body { padding: 4mm 2mm; font-family: Courier, monospace; font-size: 11px; line-height: 1.25; color: #000; } pre { margin: 0; font-family: inherit; font-size: inherit; white-space: pre-wrap; word-break: break-all; }';
+ win.document.head.appendChild(style);
+ const pre = win.document.createElement('pre');
+ pre.textContent = txt;
+ win.document.body.appendChild(pre);
+ setTimeout(() => {
+ try {
+ win.focus();
+ win.print();
+ } catch (pe) {
+ console.error('Print trigger error', pe);
+ }
+ }, 350);
+ } catch (e) {
+ console.error('Error writing thermal window:', e);
+ }
+ };
+
+ const printPdf = (format = 'a5') => {
+ if (!viewInvoice || !viewInvoice.sale) return;
+ const saleId = viewInvoice.sale.sale_id;
+ const url = `{API_BASE}sales/invoice-pdf/{saleId}?format={format}&view=1`;
+ const printWindow = window.open(url, '_blank');
+ if (!printWindow) {
+ notify('Popup was blocked by browser. Please allow popups for PDF printing.', 'error');
+ }
+ };
+
+ const downloadPdf = (format = 'a5') => {
+ if (!viewInvoice || !viewInvoice.sale) return;
+ const saleId = viewInvoice.sale.sale_id;
+ const url = `{API_BASE}sales/invoice-pdf/{saleId}?format={format}&download=1`;
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `Invoice_{viewInvoice.sale.invoice_no.replace(/[^a-zA-Z0-9_-]/g, '_')}_{format.toUpperCase()}.pdf`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ };
+
+ const generateThermalReceiptText = (inv) => {
+ if (!inv || !inv.sale) return '';
+ const sale = inv.sale;
+ const items = inv.items || [];
+ const returnItems = inv.return_items || [];
+ const LINE_WIDTH = 48;
+ const padR = (str, len) => String(str || '').slice(0, len).padEnd(len, ' ');
+ const padL = (str, len) => String(str || '').slice(0, len).padStart(len, ' ');
+ const center = (str) => {
+ const s = String(str || '').trim();
+ if (s.length >= LINE_WIDTH) return s.slice(0, LINE_WIDTH);
+ const pad = Math.floor((LINE_WIDTH - s.length) / 2);
+ return ' '.repeat(pad) + s;
+ };
+ const rule = (char = '=') => char.repeat(LINE_WIDTH);
+
+ let lines = [];
+ lines.push(center(inv.store?.store_name || 'HOSPITAL PHARMACY'));
+ lines.push(center(`{inv.store?.building_name || 'Main Hospital'} - {inv.store?.floor_no || 'Ground Floor'}`));
+ if (inv.store?.phone) lines.push(center(`Ph: {inv.store.phone}`));
+ if (inv.store?.gstin) lines.push(center(`GSTIN: {inv.store.gstin}`));
+ if (inv.store?.drug_license_no_20b) lines.push(center(`DL: {inv.store.drug_license_no_20b} / {inv.store?.drug_license_no_21b || ''}`));
+ lines.push(rule('='));
+ lines.push(center('TAX INVOICE / CASH MEMO'));
+ lines.push(rule('-'));
+ lines.push(padR(`Inv No : {sale.invoice_no}`, 28) + padL(`Date: {String(sale.sale_date).slice(0, 10)}`, 20));
+ lines.push(padR(`Patient: {sale.patient_name || 'Walk-in Customer'}`, 32) + padL(sale.uhid ? `UHID: {sale.uhid}` : '', 16));
+ if (sale.doctor_name) lines.push(`Doctor : {sale.doctor_name}`);
+ lines.push(rule('-'));
+ lines.push(padR('ITEM', 22) + padR('BATCH', 8) + padL('QTY', 5) + padL('RATE', 6) + padL('TOTAL', 7));
+ lines.push(rule('-'));
+
+ items.forEach(it => {
+ const name = (it.item_name || 'Item').slice(0, 21);
+ const batch = (it.batch_no || '').slice(0, 7);
+ const qty = String(it.qty || 1);
+ const rate = Number(it.unit_price || it.effective_rate || 0).toFixed(2);
+ const total = Number(it.total_amount || 0).toFixed(2);
+ lines.push(padR(name, 22) + padR(batch, 8) + padL(qty, 5) + padL(rate, 6) + padL(total, 7));
+ });
+
+ if (returnItems.length > 0) {
+ lines.push(rule('-'));
+ lines.push(center('RETURNED / EXCHANGED ITEMS'));
+ returnItems.forEach(it => {
+ const name = ('RET: ' + (it.item_name || 'Item')).slice(0, 21);
+ const batch = (it.batch_no || '').slice(0, 7);
+ const qty = String(it.qty || 1);
+ const rate = Number(it.unit_price || 0).toFixed(2);
+ const total = ('-' + Number(it.total_amount || 0).toFixed(2));
+ lines.push(padR(name, 22) + padR(batch, 8) + padL(qty, 5) + padL(rate, 6) + padL(total, 7));
+ });
+ }
+
+ lines.push(rule('='));
+ lines.push(padR('Gross Total:', 36) + padL(`Rs. {Number(sale.gross_amount).toFixed(2)}`, 12));
+ if (Number(sale.discount_amount) > 0) {
+ lines.push(padR('Discount Allowed:', 36) + padL(`-Rs. {Number(sale.discount_amount).toFixed(2)}`, 12));
+ }
+ if (Number(sale.return_amount) > 0) {
+ lines.push(padR('Less Return Value:', 36) + padL(`-Rs. {Number(sale.return_amount).toFixed(2)}`, 12));
+ }
+ lines.push(padR('NET AMOUNT PAYABLE:', 34) + padL(`Rs. {Number(sale.net_amount).toFixed(2)}`, 14));
+ lines.push(rule('-'));
+ lines.push(padR(`Paid by: {sale.payment_mode || 'Cash'}`, 30) + padL(`Status: PAID`, 18));
+ lines.push(rule('='));
+ lines.push(center('Thank you for your visit! Get well soon!'));
+ lines.push(center('Medicines cannot be returned without original bill.'));
+ return lines.join('n');
+ };
+
+ return (
+
+ {/* Header and Control Bar */}
+
+
+
+
+ Invoices & Sales Register
+
+ {pagination.total_records || 0} Bills
+
+
+
+ Showing all sales across pharmacy desk ordered Latest to Oldest. Search by bill #, patient, mobile, or doctor.
+
+
+
+
+ {onEditInPos && (
+
+ )}
+
+
+
+ {/* Quick Date Presets Bar */}
+
+ Quick Filter:
+
+
+
+
+
+
+
+ {/* KPI Metrics Cards */}
+
+
+
+
+ Today's Bills
+
+
+
{stats.today_count || 0}
+
₹{Number(stats.today_amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
+
+
+
+
+
+ Total Invoices
+
+
+
{stats.total_count || 0}
+
₹{Number(stats.total_amount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
+
+
+
+
+
+ Returns / Refunds
+
+
+
₹{Number(stats.total_returns || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
+
Total customer returns
+
+
+
+
+
+ Net Realized
+
+
+
+ ₹{Number((stats.total_amount || 0) - (stats.total_returns || 0)).toLocaleString('en-IN', { minimumFractionDigits: 2 })}
+
+
Net pharmacy collection
+
+
+
+
+ {/* Filters Row */}
+
+
+ {/* Invoices List Table */}
+
+
+
+
+
+ | Invoice No |
+ Date & Time |
+ Patient Details |
+ Doctor |
+ Items Dispensed |
+ Payment |
+ Net Total |
+ Status |
+ Actions |
+
+
+
+ {isLoading ? (
+
+ |
+
+ Loading invoices...
+ |
+
+ ) : invoices.length === 0 ? (
+
+
+
+ No invoices found
+ Try changing your search query or date range filters.
+ |
+
+ ) : (
+ invoices.map((inv) => {
+ const hasReturns = Number(inv.return_amount) > 0;
+ return (
+
+ |
+ openPrintDrawer(inv.sale_id)}
+ title="Click to View / Print Invoice"
+ style={{ cursor: 'pointer' }}
+ >
+ {inv.invoice_no}
+
+ {inv.abdm_care_context_ref && (
+
+ ABDM
+
+ )}
+ |
+
+
+ {String(inv.sale_date).slice(0, 10)}
+
+
+ {String(inv.sale_date).slice(11, 16)}
+
+ |
+
+
+ {inv.patient_name || 'Walk-in Customer'}
+
+
+ {inv.uhid && (
+
+ {inv.uhid}
+
+ )}
+ {inv.patient_mobile && (
+
+ {inv.patient_mobile}
+
+ )}
+
+ {inv.patient_type || 'Walk-in'}
+
+
+ |
+
+
+ {inv.doctor_name || 'Self / Walk-in'}
+
+ {inv.doctor_reg_no && (
+ Reg: {inv.doctor_reg_no}
+ )}
+ |
+
+
+
+ {inv.items_count || 0} items
+
+
+
+ {inv.items_summary || 'N/A'}
+
+ |
+
+
+ {inv.payment_mode || 'Cash'}
+
+ |
+
+
+ ₹{Number(inv.net_amount).toFixed(2)}
+
+ {Number(inv.discount_amount) > 0 && (
+
+ Disc: -₹{Number(inv.discount_amount).toFixed(2)}
+
+ )}
+ {hasReturns && (
+
+ Ret: -₹{Number(inv.return_amount).toFixed(2)}
+
+ )}
+ |
+
+ {hasReturns ? (
+
+ Exchange
+
+ ) : (
+
+ Paid
+
+ )}
+ |
+
+
+
+
+
+
+ |
+
+ );
+ })
+ )}
+
+
+
+
+ {/* Pagination Footer */}
+ {pagination.total_pages > 1 && (
+
+
+ Showing page {pagination.current_page} of {pagination.total_pages} ({pagination.total_records} total invoices)
+
+
+
+
+ {pagination.current_page} / {pagination.total_pages}
+
+
+
+
+ )}
+
+
+ {/* Quick Edit Details Modal */}
+ {editModalSale && (
+
+
+
+
+
+
+ Edit Invoice Details: {editModalSale.invoice_no}
+
+
+
+
+
+
+
+ )}
+
+ {/* Slide-in Print Drawer for Invoices View */}
+
setIsPrintOpen(false)}
+ />
+
+ {viewInvoice && (
+ <>
+
+
+
+
+
Invoice & Print Desk
+
+ Bill: {viewInvoice.sale.invoice_no}
+
+
+
+
+
+
+
+
+
+
+
+ {printFormat === 'thermal' ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ {printFormat === 'thermal' && (
+
+
+ Thermal 80mm ESC/POS Text
+
+
+
+ {generateThermalReceiptText(viewInvoice)}
+
+
+ )}
+
+ {printFormat === 'a5' && (
+
+
+
+
+ A5 Landscape PDF (210mm × 148mm): Native vector document rendered by mPDF.
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {printFormat === 'a4' && (
+
+
+
+
+ A4 Vector PDF (210mm × 297mm): Native vector document rendered by mPDF.
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ >
+ )}
+
+
+ );
+}
+
// =========================================================================
// 2. LIVE INVENTORY & BATCH MATRIX VIEW
// =========================================================================