diff --git a/public/App/MedicalStore/index.html b/public/App/MedicalStore/index.html index b5f1c67..58dda37 100644 --- a/public/App/MedicalStore/index.html +++ b/public/App/MedicalStore/index.html @@ -127,6 +127,65 @@ box-shadow: 0 10px 25px rgba(15, 23, 42, 0.25); } + /* Slide-in Checkout Desk Drawer */ + .checkout-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; + } + .checkout-drawer-overlay.open { + opacity: 1; + pointer-events: auto; + } + .checkout-drawer { + position: fixed; + top: 0; + right: 0; + width: 500px; + max-width: 96vw; + height: 100vh; + background: linear-gradient(165deg, #1e293b 0%, #0f172a 100%); + color: #ffffff; + 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); + } + .checkout-drawer.open { + transform: translateX(0); + } + .checkout-drawer-header { + padding: 16px 20px; + background: rgba(15, 23, 42, 0.85); + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + } + .checkout-drawer-body { + padding: 20px; + overflow-y: auto; + flex: 1; + } + .checkout-drawer-body::-webkit-scrollbar { + width: 6px; + } + .checkout-drawer-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + } + .btn-primary-custom { background: linear-gradient(135deg, #0891b2 0%, #0284c7 100%); border: none; @@ -746,6 +805,7 @@ function PosCounter({ store, notify }) { const [lastInvoice, setLastInvoice] = useState(null); const [printFormat, setPrintFormat] = useState('thermal'); // 'thermal', 'a5', 'a4' const [isProcessing, setIsProcessing] = useState(false); + const [isCheckoutOpen, setIsCheckoutOpen] = useState(false); // B2C Sales Return & Same-Invoice Exchange State const [returnCart, setReturnCart] = useState([]); @@ -1498,6 +1558,7 @@ function PosCounter({ store, notify }) { : `Invoice ${data.invoice_no} generated successfully! Net Amount: ₹${data.net_amount}`; notify(msg, 'success'); fetchInvoiceDetails(data.sale_id); + setIsCheckoutOpen(false); setCart([]); setReturnCart([]); setWholeDiscountVal(0); @@ -1535,12 +1596,13 @@ function PosCounter({ store, notify }) { // Global Keyboard Shortcuts (Desktop Pharmacy Standard) useEffect(() => { const handleGlobalKeyDown = (e) => { - // Dismiss Modals on Escape - if (showFhirModal || showReturnModal) { + // Dismiss Modals or Checkout Drawer on Escape + if (showFhirModal || showReturnModal || isCheckoutOpen) { if (e.key === 'Escape') { e.preventDefault(); - setShowFhirModal(false); - setShowReturnModal(false); + if (showFhirModal) setShowFhirModal(false); + else if (showReturnModal) setShowReturnModal(false); + else if (isCheckoutOpen) setIsCheckoutOpen(false); } return; } @@ -1569,32 +1631,43 @@ function PosCounter({ store, notify }) { return; } - // F7: Jump to Whole Bill Discount + // F7: Jump to Whole Bill Discount (opens Checkout Drawer if closed) if (e.key === 'F7') { e.preventDefault(); - if (wholeDiscountRef.current) { - wholeDiscountRef.current.focus(); - wholeDiscountRef.current.select(); + setIsCheckoutOpen(true); + setTimeout(() => { + if (wholeDiscountRef.current) { + wholeDiscountRef.current.focus(); + wholeDiscountRef.current.select(); + } + }, 150); + return; + } + + // F8: Open Checkout Drawer or Cycle Payment Mode + if (e.key === 'F8') { + e.preventDefault(); + if (!isCheckoutOpen) { + setIsCheckoutOpen(true); + } else { + const modes = ['Cash', 'UPI', 'Card', 'Mixed']; + if (selectedPatient?.has_active_ipd) modes.push('IPD_Credit'); + const curIdx = modes.indexOf(paymentMode); + const nextMode = modes[(curIdx + 1) % modes.length]; + setPaymentMode(nextMode); } return; } - // F8: Cycle Payment Mode - if (e.key === 'F8') { - e.preventDefault(); - const modes = ['Cash', 'UPI', 'Card', 'Mixed']; - if (selectedPatient?.has_active_ipd) modes.push('IPD_Credit'); - const curIdx = modes.indexOf(paymentMode); - const nextMode = modes[(curIdx + 1) % modes.length]; - setPaymentMode(nextMode); - return; - } - - // F9 or Ctrl+Enter or Alt+S: Complete Sale & Print Bill + // F9 or Ctrl+Enter or Alt+S: Open Checkout or Complete Sale & Print Bill if (e.key === 'F9' || (e.ctrlKey && e.key === 'Enter') || (e.altKey && (e.key === 's' || e.key === 'S'))) { e.preventDefault(); - if (!isProcessing && (cart.length > 0 || returnCart.length > 0)) { - processSale(); + if (!isCheckoutOpen) { + setIsCheckoutOpen(true); + } else { + if (!isProcessing && (cart.length > 0 || returnCart.length > 0)) { + processSale(); + } } return; } @@ -1602,12 +1675,13 @@ function PosCounter({ store, notify }) { window.addEventListener('keydown', handleGlobalKeyDown); return () => window.removeEventListener('keydown', handleGlobalKeyDown); - }, [showFhirModal, showReturnModal, paymentMode, selectedPatient, isProcessing, cart, returnCart, splitRemaining, splitCashNum, splitUpiNum, splitCardNum, refundPayable, refundMode]); + }, [showFhirModal, showReturnModal, isCheckoutOpen, paymentMode, selectedPatient, isProcessing, cart, returnCart, splitRemaining, splitCashNum, splitUpiNum, splitCardNum, refundPayable, refundMode]); return ( -
- {/* Left Column: Patient Selector & Cart Grid */} -
+
+
+ {/* Full-Width Workspace: Patient Selector, Medicine Lookup & Cart Grid */} +
{/* Patient Information Panel */}
@@ -1917,6 +1991,17 @@ function PosCounter({ store, notify }) { )}
+ {(cart.length > 0 || returnCart.length > 0) && ( + + )}
-
-
- {/* Right Column: Checkout, Totals & Multi-Format Printing */} -
-
-
- Checkout Desk - {store?.store_code || ''} -
- - {/* Whole Bill Discount Controller */} -
-
- -
- - + {/* Cart Footer Bar: Live Totals & Checkout Trigger */} +
+
+
+ + {cart.length + returnCart.length} Line Items + + + Gross: ₹{grossTotal.toFixed(2)} + + {itemDiscountTotal > 0 && ( + + Item Disc: -₹{itemDiscountTotal.toFixed(2)} + + )} + {wholeDiscountAmt > 0 && ( + + Bill Disc: -₹{wholeDiscountAmt.toFixed(2)} + + )} + {returnTotal > 0 && ( + + Return Credit: -₹{returnTotal.toFixed(2)} + + )}
-
- - {wholeDiscountType === 'pct' ? '%' : '₹'} - - setWholeDiscountVal(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - if (!isProcessing && cart.length > 0) { - processSale(); - } - } +
+
+
+ {refundPayable > 0 ? 'Customer Refund' : 'Net Amount'} +
+
0 ? 'text-danger' : 'text-success'}`} style={{ lineHeight: 1.1 }}> + ₹{refundPayable > 0 ? refundPayable.toFixed(2) : netPayable.toFixed(2)} +
+
+ + - )} -
- - {/* 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 */} -
- -
- - - -
-
- -
{/* Print Preview Component for Thermal 80mm, A5, and A4 */} @@ -3420,8 +3039,563 @@ function PosCounter({ store, notify }) {
)} +
+
- {/* ABDM FHIR R4 Bundle Modal */} + {/* 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 */} {showFhirModal && (
@@ -3838,7 +4012,6 @@ function PosCounter({ store, notify }) {
)} -
); }