diff --git a/public/App/MedicalStore/index.html b/public/App/MedicalStore/index.html
index 58dda37..54798b8 100644
--- a/public/App/MedicalStore/index.html
+++ b/public/App/MedicalStore/index.html
@@ -186,6 +186,75 @@
border-radius: 4px;
}
+ /* Slide-in Bill Print Drawer */
+ .print-drawer-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(15, 23, 42, 0.65);
+ backdrop-filter: blur(4px);
+ z-index: 1045;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.25s ease;
+ }
+ .print-drawer-overlay.open {
+ opacity: 1;
+ pointer-events: auto;
+ }
+ .print-drawer {
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 720px;
+ max-width: 96vw;
+ height: 100vh;
+ background: #f1f5f9;
+ color: #1e293b;
+ z-index: 1050;
+ box-shadow: -12px 0 40px rgba(0, 0, 0, 0.55);
+ display: flex;
+ flex-direction: column;
+ transform: translateX(105%);
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
+ }
+ .print-drawer.open {
+ transform: translateX(0);
+ }
+ .print-drawer-header {
+ padding: 14px 20px;
+ background: #0f172a;
+ color: #ffffff;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-shrink: 0;
+ }
+ .print-drawer-body {
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+ background: #cbd5e1;
+ }
+ .print-drawer-body::-webkit-scrollbar {
+ width: 6px;
+ }
+ .print-drawer-body::-webkit-scrollbar-thumb {
+ background: rgba(100, 116, 139, 0.4);
+ border-radius: 4px;
+ }
+ .print-paper-card {
+ background: #ffffff;
+ border-radius: 8px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
+ padding: 24px;
+ margin: 0 auto;
+ max-width: 100%;
+ }
+
.btn-primary-custom {
background: linear-gradient(135deg, #0891b2 0%, #0284c7 100%);
border: none;
@@ -806,6 +875,7 @@ function PosCounter({ store, notify }) {
const [printFormat, setPrintFormat] = useState('thermal'); // 'thermal', 'a5', 'a4'
const [isProcessing, setIsProcessing] = useState(false);
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
+ const [isPrintOpen, setIsPrintOpen] = useState(false);
// B2C Sales Return & Same-Invoice Exchange State
const [returnCart, setReturnCart] = useState([]);
@@ -1587,6 +1657,7 @@ function PosCounter({ store, notify }) {
const data = await res.json();
if (data.status) {
setLastInvoice(data);
+ setIsPrintOpen(true);
}
} catch (e) {
console.error(e);
@@ -1596,13 +1667,14 @@ function PosCounter({ store, notify }) {
// Global Keyboard Shortcuts (Desktop Pharmacy Standard)
useEffect(() => {
const handleGlobalKeyDown = (e) => {
- // Dismiss Modals or Checkout Drawer on Escape
- if (showFhirModal || showReturnModal || isCheckoutOpen) {
+ // Dismiss Modals or Drawers on Escape
+ if (showFhirModal || showReturnModal || isCheckoutOpen || isPrintOpen) {
if (e.key === 'Escape') {
e.preventDefault();
if (showFhirModal) setShowFhirModal(false);
else if (showReturnModal) setShowReturnModal(false);
else if (isCheckoutOpen) setIsCheckoutOpen(false);
+ else if (isPrintOpen) setIsPrintOpen(false);
}
return;
}
@@ -1675,7 +1747,7 @@ function PosCounter({ store, notify }) {
window.addEventListener('keydown', handleGlobalKeyDown);
return () => window.removeEventListener('keydown', handleGlobalKeyDown);
- }, [showFhirModal, showReturnModal, isCheckoutOpen, paymentMode, selectedPatient, isProcessing, cart, returnCart, splitRemaining, splitCashNum, splitUpiNum, splitCardNum, refundPayable, refundMode]);
+ }, [showFhirModal, showReturnModal, isCheckoutOpen, isPrintOpen, paymentMode, selectedPatient, isProcessing, cart, returnCart, splitRemaining, splitCashNum, splitUpiNum, splitCardNum, refundPayable, refundMode]);
return (
@@ -1691,6 +1763,17 @@ function PosCounter({ store, notify }) {
{patientType}
+ {lastInvoice && (
+
+ )}
{selectedPatient && (
{(selectedPatient.has_active_opd || selectedPatient.has_active_ipd) && (
@@ -2398,36 +2481,618 @@ function PosCounter({ store, notify }) {
- {/* Print Preview Component for Thermal 80mm, A5, and A4 */}
- {lastInvoice && (
-
-
-
- Bill: {lastInvoice.sale.invoice_no}
- {lastInvoice.sale.abdm_care_context_ref && (
-
- {lastInvoice.sale.abdm_care_context_ref}
-
- )}
-
-
+
+
+
+ {/* Slide-in Checkout Desk Drawer & Backdrop */}
+
setIsCheckoutOpen(false)}
+ />
+
+
+
+
+
+
Checkout Desk
+
+ Counter: {store?.store_code || ''} • {store?.store_name}
+
+
+
+
+
+
+
+ Checkout Desk
+ {store?.store_code || ''}
+
+
+ {/* Whole Bill Discount Controller */}
+
+
+
+
-
-
-
-
-
+
+
+ {wholeDiscountType === 'pct' ? '%' : '₹'}
+
+ setWholeDiscountVal(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ if (!isProcessing && cart.length > 0) {
+ processSale();
+ }
+ }
+ }}
+ />
+ {Number(wholeDiscountVal) > 0 && (
+ setWholeDiscountVal(0)}
+ >
+
+
+ )}
+
+
+ {/* Quick Preset Buttons */}
+
+ Presets:
+ {wholeDiscountType === 'pct' ? (
+ [5, 10, 15, 20].map(p => (
+ setWholeDiscountVal(p)}
+ >
+ {p}%
+
+ ))
+ ) : (
+ [20, 50, 100, 200].map(v => (
+ setWholeDiscountVal(v)}
+ >
+ ₹{v}
+
+ ))
+ )}
+ {Number(wholeDiscountVal) > 0 && (
+ setWholeDiscountVal(0)}
+ >
+ Clear
+
+ )}
+
+
+
+ {/* Breakdown */}
+
+ Gross Items Total:
+ ₹{grossTotal.toFixed(2)}
+
+
+ {itemDiscountTotal > 0 && (
+
+ Item-Level Discounts:
+ -₹{itemDiscountTotal.toFixed(2)}
+
+ )}
+
+ {wholeDiscountAmt > 0 && (
+
+ Whole Bill Discount ({wholeDiscountType === 'pct' ? `${wholeDiscountVal}%` : `₹${wholeDiscountVal}`}):
+ -₹{wholeDiscountAmt.toFixed(2)}
+
+ )}
+
+ {discountTotal > 0 && (
+
+ Total Discounts:
+ -₹{discountTotal.toFixed(2)}
+
+ )}
+
+ {returnTotal > 0 && (
+
+ Less Return Credit:
+ -₹{returnTotal.toFixed(2)}
+
+ )}
+
+
+ Round Off:
+ ₹{roundOff}
+
+
+
+ {refundPayable > 0 ? (
+
+
+ REFUND TO PATIENT:
+ ₹{refundPayable.toLocaleString('en-IN')}
+
+
+ Return credit exceeds new items. Refund this net difference from counter cash drawer or UPI.
+
+
+
+
+ setRefundMode('Cash')}
+ >
+ Cash Refund
+
+ setRefundMode('UPI')}
+ >
+ UPI / Bank Refund
+
+
+ {refundMode === 'UPI' && (
+
setRefundRefNo(e.target.value)}
+ />
+ )}
+
+
+ ) : (
+
+ Net Payable:
+ ₹{netPayable.toLocaleString('en-IN')}
+
+ )}
+
+ {/* Payment Mode Selection (Only when patient owes money) */}
+ {refundPayable <= 0 && (
+
+
+
+ {paymentMode === 'Mixed' && (
+ Partial / Split Mode
+ )}
+
+
+
+ setPaymentMode('Cash')}
+ >
+ Cash
+
+
+
+ setPaymentMode('UPI')}
+ >
+ UPI / QR
+
+
+
+ {
+ setPaymentMode('Mixed');
+ if (!splitCash && !splitUpi) {
+ setSplitCash('');
+ setSplitUpi('');
+ }
+ }}
+ title="Partial payment by Cash and UPI/Bank"
+ >
+ Split / Mixed
+
+
+
+ setPaymentMode('Card')}
+ >
+ Card Swipe
+
+
+
+ setPaymentMode('IPD_Credit')}
+ title="Charge to IPD Patient Bill"
+ >
+ IPD Bill Post
+
+
+
+
+ {paymentMode === 'IPD_Credit' && (
+
+
+ Medicines will be charged directly to the patient's IPD Hospital Account and settled at discharge.
+
+ )}
+
+ {/* Mixed / Split Payment Breakdown Panel */}
+ {paymentMode === 'Mixed' && (
+
+
+ Split Payment Allocation
+ Total: ₹{netPayable}
+
+
+
+
+
+
+ ₹
+ setSplitCash(e.target.value)}
+ step="any"
+ />
+
+
+
+
+
+ ₹
+ setSplitUpi(e.target.value)}
+ step="any"
+ />
+
+
+
+
+ {/* UTR & Bank Details for the UPI portion */}
+ {Number(splitUpi) > 0 && (
+
+ )}
+
+ {/* Optional Card portion */}
+
+
+
Card Swiped (if any):
+
+ ₹
+ setSplitCard(e.target.value)}
+ step="any"
+ />
+
+
+ {Number(splitCard) > 0 && (
+
setCardRefNo(e.target.value)}
+ />
+ )}
+
+
+ {/* Live Balance Summary & Autofill helpers */}
+
+
+ Allocated: ₹{totalSplitAllocated.toFixed(2)}
+ Bill Net: ₹{netPayable}
+
+ {Math.abs(splitRemaining) <= 0.01 ? (
+
+ Balanced! Ready to print bill.
+
+ ) : splitRemaining > 0 ? (
+
+
+ Balance Remaining: ₹{splitRemaining.toFixed(2)}
+
+
+ setSplitUpi(String(((Number(splitUpi) || 0) + splitRemaining).toFixed(2)))}
+ >
+ + ₹{splitRemaining.toFixed(2)} to UPI
+
+ setSplitCash(String(((Number(splitCash) || 0) + splitRemaining).toFixed(2)))}
+ >
+ + ₹{splitRemaining.toFixed(2)} to Cash
+
+
+
+ ) : (
+
+ Overallocated by ₹{Math.abs(splitRemaining).toFixed(2)}
+
+ )}
+
+
+ )}
+
+ {/* Single UPI Mode Details */}
+ {paymentMode === 'UPI' && (
+
+ {store?.upi_id && (
+
+ Store UPI VPA: {store?.upi_id}
+
+ )}
+
+
+ Accountant will match this UTR against the hospital bank statement.
+
+
+ )}
+
+ {/* Single Card Mode Details */}
+ {paymentMode === 'Card' && (
+
+ )}
+
+ )}
+
+ {/* Print Format Selector */}
+
+
+
+ setPrintFormat('thermal')}
+ >
+ Thermal 80mm (1-5 Items)
+
+ setPrintFormat('a5')}
+ >
+ A5 Invoice (OPD)
+
+ setPrintFormat('a4')}
+ >
+ A4 Tax Bill (IPD)
+
+
+
+
+
0 ? 'btn-warning text-dark fw-bold' : 'btn-success-custom'} w-100 py-2.5 fs-6`}
+ onClick={processSale}
+ disabled={isProcessing || (cart.length === 0 && returnCart.length === 0) || (refundPayable === 0 && paymentMode === 'Mixed' && Math.abs(splitRemaining) > 0.01)}
+ >
+ {isProcessing ? (
+ Generating Bill & Credit Note...
+ ) : (refundPayable === 0 && paymentMode === 'Mixed' && Math.abs(splitRemaining) > 0.01) ? (
+ Allocate Balance (₹{splitRemaining > 0 ? splitRemaining.toFixed(2) + ' left' : Math.abs(splitRemaining).toFixed(2) + ' over'})
+ ) : refundPayable > 0 ? (
+ Process Return & Refund ₹{refundPayable}
+ ) : returnCart.length > 0 ? (
+ Complete Exchange Bill & Print ({printFormat.toUpperCase()})
+ ) : (
+ Complete Sale & Print ({printFormat.toUpperCase()})
+ )}
+
+
+
+
+ {/* Slide-in Bill Print Drawer & Backdrop */}
+
setIsPrintOpen(false)}
+ />
+
+ {lastInvoice && (
+ <>
+
+
+
+
+
Invoice & Print Desk
+
+ Bill: {lastInvoice.sale.invoice_no}
+ {lastInvoice.sale.abdm_care_context_ref && (
+
+ ABDM Linked
+
+ )}
+
+
+
+
+
+
viewAbdmBundle(lastInvoice.sale.sale_id)}
+ title="View official ABDM FHIR R4 MedicationDispense JSON Document Bundle"
+ style={{ fontSize: '0.72rem' }}
+ >
+ FHIR R4
+
+
+ setPrintFormat('thermal')}>80mm
+ setPrintFormat('a5')}>A5
+ setPrintFormat('a4')}>A4
+
+
window.print()} title="Print Bill">
+ Print
+
+
setIsPrintOpen(false)}
+ title="Close Print Drawer (Esc)"
+ >
+
+ Esc
+
+
+
+
+
+
+
{/* Format 1: 80mm Thermal Slip (Single or 1-5 Items) */}
{printFormat === 'thermal' && (
@@ -3037,562 +3702,24 @@ function PosCounter({ store, notify }) {
)}
-
- )}
-
-
-
- {/* Slide-in Checkout Desk Drawer & Backdrop */}
-
setIsCheckoutOpen(false)}
- />
-
-
-
-
-
-
Checkout Desk
-
- Counter: {store?.store_code || ''} • {store?.store_name}
-
-
-
-
setIsCheckoutOpen(false)}
- title="Close Checkout Drawer (Esc)"
- >
-
- Close
- Esc
-
-
-
-
- Checkout Desk
- {store?.store_code || ''}
-
-
- {/* Whole Bill Discount Controller */}
-
-
-
-
- setWholeDiscountType('pct')}
- >
- % Percent
-
- setWholeDiscountType('val')}
- >
- ₹ Fixed Value
-
-
-
-
-
-
- {wholeDiscountType === 'pct' ? '%' : '₹'}
-
- setWholeDiscountVal(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- if (!isProcessing && cart.length > 0) {
- processSale();
- }
- }
- }}
- />
- {Number(wholeDiscountVal) > 0 && (
- setWholeDiscountVal(0)}
- >
-
-
- )}
-
-
- {/* Quick Preset Buttons */}
-
- Presets:
- {wholeDiscountType === 'pct' ? (
- [5, 10, 15, 20].map(p => (
- setWholeDiscountVal(p)}
- >
- {p}%
-
- ))
- ) : (
- [20, 50, 100, 200].map(v => (
- setWholeDiscountVal(v)}
- >
- ₹{v}
-
- ))
- )}
- {Number(wholeDiscountVal) > 0 && (
- setWholeDiscountVal(0)}
- >
- Clear
-
- )}
- {/* Breakdown */}
-
- Gross Items Total:
- ₹{grossTotal.toFixed(2)}
-
-
- {itemDiscountTotal > 0 && (
-
- Item-Level Discounts:
- -₹{itemDiscountTotal.toFixed(2)}
-
- )}
-
- {wholeDiscountAmt > 0 && (
-
- Whole Bill Discount ({wholeDiscountType === 'pct' ? `${wholeDiscountVal}%` : `₹${wholeDiscountVal}`}):
- -₹{wholeDiscountAmt.toFixed(2)}
-
- )}
-
- {discountTotal > 0 && (
-
- Total Discounts:
- -₹{discountTotal.toFixed(2)}
-
- )}
-
- {returnTotal > 0 && (
-
- Less Return Credit:
- -₹{returnTotal.toFixed(2)}
-
- )}
-
-
- Round Off:
- ₹{roundOff}
-
-
-
- {refundPayable > 0 ? (
-
-
- REFUND TO PATIENT:
- ₹{refundPayable.toLocaleString('en-IN')}
-
-
- Return credit exceeds new items. Refund this net difference from counter cash drawer or UPI.
-
-
-
-
- setRefundMode('Cash')}
- >
- Cash Refund
-
- setRefundMode('UPI')}
- >
- UPI / Bank Refund
-
-
- {refundMode === 'UPI' && (
-
setRefundRefNo(e.target.value)}
- />
- )}
-
-
- ) : (
-
- Net Payable:
- ₹{netPayable.toLocaleString('en-IN')}
-
- )}
-
- {/* Payment Mode Selection (Only when patient owes money) */}
- {refundPayable <= 0 && (
-
-
-
- {paymentMode === 'Mixed' && (
- Partial / Split Mode
- )}
-
-
-
-
setPaymentMode('Cash')}
- >
- Cash
+
+
+ Format: {printFormat} • Bill No: {lastInvoice.sale.invoice_no}
+
+
+ setIsPrintOpen(false)}>
+ Done / Close (Esc)
-
-
- setPaymentMode('UPI')}
- >
- UPI / QR
-
-
-
- {
- setPaymentMode('Mixed');
- if (!splitCash && !splitUpi) {
- setSplitCash('');
- setSplitUpi('');
- }
- }}
- title="Partial payment by Cash and UPI/Bank"
- >
- Split / Mixed
-
-
-
- setPaymentMode('Card')}
- >
- Card Swipe
-
-
-
- setPaymentMode('IPD_Credit')}
- title="Charge to IPD Patient Bill"
- >
- IPD Bill Post
+ window.print()}>
+ Print Bill ({printFormat.toUpperCase()})
-
- {paymentMode === 'IPD_Credit' && (
-
-
- Medicines will be charged directly to the patient's IPD Hospital Account and settled at discharge.
-
- )}
-
- {/* Mixed / Split Payment Breakdown Panel */}
- {paymentMode === 'Mixed' && (
-
-
- Split Payment Allocation
- Total: ₹{netPayable}
-
-
-
-
-
-
- ₹
- setSplitCash(e.target.value)}
- step="any"
- />
-
-
-
-
-
- ₹
- setSplitUpi(e.target.value)}
- step="any"
- />
-
-
-
-
- {/* UTR & Bank Details for the UPI portion */}
- {Number(splitUpi) > 0 && (
-
- )}
-
- {/* Optional Card portion */}
-
-
-
Card Swiped (if any):
-
- ₹
- setSplitCard(e.target.value)}
- step="any"
- />
-
-
- {Number(splitCard) > 0 && (
-
setCardRefNo(e.target.value)}
- />
- )}
-
-
- {/* Live Balance Summary & Autofill helpers */}
-
-
- Allocated: ₹{totalSplitAllocated.toFixed(2)}
- Bill Net: ₹{netPayable}
-
- {Math.abs(splitRemaining) <= 0.01 ? (
-
- Balanced! Ready to print bill.
-
- ) : splitRemaining > 0 ? (
-
-
- Balance Remaining: ₹{splitRemaining.toFixed(2)}
-
-
- setSplitUpi(String(((Number(splitUpi) || 0) + splitRemaining).toFixed(2)))}
- >
- + ₹{splitRemaining.toFixed(2)} to UPI
-
- setSplitCash(String(((Number(splitCash) || 0) + splitRemaining).toFixed(2)))}
- >
- + ₹{splitRemaining.toFixed(2)} to Cash
-
-
-
- ) : (
-
- Overallocated by ₹{Math.abs(splitRemaining).toFixed(2)}
-
- )}
-
-
- )}
-
- {/* Single UPI Mode Details */}
- {paymentMode === 'UPI' && (
-
- {store?.upi_id && (
-
- Store UPI VPA: {store?.upi_id}
-
- )}
-
-
- Accountant will match this UTR against the hospital bank statement.
-
-
- )}
-
- {/* Single Card Mode Details */}
- {paymentMode === 'Card' && (
-
- )}
-
- )}
-
- {/* Print Format Selector */}
-
-
-
- setPrintFormat('thermal')}
- >
- Thermal 80mm (1-5 Items)
-
- setPrintFormat('a5')}
- >
- A5 Invoice (OPD)
-
- setPrintFormat('a4')}
- >
- A4 Tax Bill (IPD)
-
-
-
-
-
0 ? 'btn-warning text-dark fw-bold' : 'btn-success-custom'} w-100 py-2.5 fs-6`}
- onClick={processSale}
- disabled={isProcessing || (cart.length === 0 && returnCart.length === 0) || (refundPayable === 0 && paymentMode === 'Mixed' && Math.abs(splitRemaining) > 0.01)}
- >
- {isProcessing ? (
- Generating Bill & Credit Note...
- ) : (refundPayable === 0 && paymentMode === 'Mixed' && Math.abs(splitRemaining) > 0.01) ? (
- Allocate Balance (₹{splitRemaining > 0 ? splitRemaining.toFixed(2) + ' left' : Math.abs(splitRemaining).toFixed(2) + ' over'})
- ) : refundPayable > 0 ? (
- Process Return & Refund ₹{refundPayable}
- ) : returnCart.length > 0 ? (
- Complete Exchange Bill & Print ({printFormat.toUpperCase()})
- ) : (
- Complete Sale & Print ({printFormat.toUpperCase()})
- )}
-
-
+ >
+ )}
{/* ABDM FHIR R4 Bundle Modal */}