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 => (
- 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')}
+ title="Open Checkout Desk to review totals, select payment method and complete sale (F8 / F9)"
>
- 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
+
+ Checkout
+ F8 / F9
+
-
- {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()})
- )}
-
{/* 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}
+
+
+
+
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
+
+
+
+ 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()})
+ )}
+
+
+
+
+ {/* ABDM FHIR R4 Bundle Modal */}
{showFhirModal && (
@@ -3838,7 +4012,6 @@ function PosCounter({ store, notify }) {
)}
-
);
}