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 && ( + + )} +
+ + {/* Quick Preset Buttons */} +
+ Presets: + {wholeDiscountType === 'pct' ? ( + [5, 10, 15, 20].map(p => ( + + )) + ) : ( + [20, 50, 100, 200].map(v => ( + + )) + )} + {Number(wholeDiscountVal) > 0 && ( + + )} +
+
+ + {/* 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. + +
+ +
+ + +
+ {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 + )} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + {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 && ( +
+
+
+ + setUpiRefNo(e.target.value)} + /> + Used by Accountant for statement audit +
+
+ + setBankName(e.target.value)} + /> + + +
+
+
+ )} + + {/* 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)} +
+
+ + +
+
+ ) : ( +
+ Overallocated by ₹{Math.abs(splitRemaining).toFixed(2)} +
+ )} +
+
+ )} + + {/* Single UPI Mode Details */} + {paymentMode === 'UPI' && ( +
+ {store?.upi_id && ( +
+ Store UPI VPA: {store?.upi_id} +
+ )} +
+
+ + setUpiRefNo(e.target.value)} + /> +
+
+ + setBankName(e.target.value)} + /> +
+
+ + Accountant will match this UTR against the hospital bank statement. + +
+ )} + + {/* Single Card Mode Details */} + {paymentMode === 'Card' && ( +
+
+
+ + setCardRefNo(e.target.value)} + /> +
+
+ + setBankName(e.target.value)} + /> +
+
+
+ )} +
+ )} + + {/* Print Format Selector */} +
+ +
+ + + +
+
+ + +
+
+ + {/* 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 + + )} + +
+
+ +
+ +
+ + + +
+ + +
+
+ +
+
+
{/* 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} - -
-
- -
-
-
- 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 && ( - - )} -
- - {/* Quick Preset Buttons */} -
- Presets: - {wholeDiscountType === 'pct' ? ( - [5, 10, 15, 20].map(p => ( - - )) - ) : ( - [20, 50, 100, 200].map(v => ( - - )) - )} - {Number(wholeDiscountVal) > 0 && ( - - )}
- {/* 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. - -
- -
- - -
- {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 - )} -
-
-
- -
-
- -
-
- -
-
- -
-
-
- - {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 && ( -
-
-
- - setUpiRefNo(e.target.value)} - /> - Used by Accountant for statement audit -
-
- - setBankName(e.target.value)} - /> - - -
-
-
- )} - - {/* 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)} -
-
- - -
-
- ) : ( -
- Overallocated by ₹{Math.abs(splitRemaining).toFixed(2)} -
- )} -
-
- )} - - {/* Single UPI Mode Details */} - {paymentMode === 'UPI' && ( -
- {store?.upi_id && ( -
- Store UPI VPA: {store?.upi_id} -
- )} -
-
- - setUpiRefNo(e.target.value)} - /> -
-
- - setBankName(e.target.value)} - /> -
-
- - Accountant will match this UTR against the hospital bank statement. - -
- )} - - {/* Single Card Mode Details */} - {paymentMode === 'Card' && ( -
-
-
- - setCardRefNo(e.target.value)} - /> -
-
- - setBankName(e.target.value)} - /> -
-
-
- )} -
- )} - - {/* Print Format Selector */} -
- -
- - - -
-
- - -
+ + )}
{/* ABDM FHIR R4 Bundle Modal */}