setSelectedRequest(req)}
+ >
+ |
+
+
+ {req.pickData.requestedAt ? new Date(req.pickData.requestedAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' }) : '—'}
+
+
+ {req.pickData.requestedAt ? new Date(req.pickData.requestedAt).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }) : ''}
+
+
|
-
-
- {req.locationname}
-
+ |
+
+
+ {req.locationname}
+
|
-
+ |
- 
-
- {req.product?.name}
- {req.product?.sku}
+
+ 
+
+
+ {req.product?.name}
+ {req.product?.sku}
|
-
- {req.pickData.qty || '—'}
+ |
+
+ {req.pickData.qty || '—'}
+
|
-
-
+ |
+
+
+ {req.pickData.status || '—'}
+
|
-
-
+ |
+
{req.pickData.resolvedAt ? new Date(req.pickData.resolvedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'}
|
-
+ |
{isPending ? (
-
+
) : (
-
+
Processed
)}
diff --git a/src/components/PosView.tsx b/src/components/PosView.tsx
new file mode 100644
index 0000000..1f362e6
--- /dev/null
+++ b/src/components/PosView.tsx
@@ -0,0 +1,1280 @@
+import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
+import { Search, Plus, Minus, Trash2, Receipt, X, Tag, QrCode, CreditCard, PieChart, Barcode, ShoppingCart, User, RefreshCcw, Save, Download, ChevronRight, Zap, Wallet, Smartphone, AlertCircle } from 'lucide-react';
+import { useFiestaStockStatement, FIESTA_TENANT_ID } from '../services/fiestaQueries';
+import { num as fnum, str as fstr, type Row } from '../services/fiestaApi';
+
+interface PosViewProps {
+ locationid?: number;
+ tenantId?: number;
+}
+
+interface CartItem {
+ id: string;
+ name: string;
+ sku: string;
+ price: number;
+ qty: number;
+ maxQty: number;
+ discountPerItem: number;
+ couponCode?: string;
+ unit: string;
+ isRefund?: boolean;
+ promoDiscount?: number;
+ hasBogo?: boolean;
+}
+
+export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: PosViewProps) {
+ const stockQ = useFiestaStockStatement({ tenantid: tenantId, locationid: locationid ?? 0, pagesize: 200 });
+
+ const [search, setSearch] = useState('');
+ const [cart, setCart] = useState([]);
+
+ const [showPayment, setShowPayment] = useState(false);
+ const [showReceipt, setShowReceipt] = useState(false);
+ const [showReport, setShowReport] = useState(false);
+ const [showManualSearch, setShowManualSearch] = useState(false);
+ const [payMethod, setPayMethod] = useState<'cash' | 'upi' | 'card' | 'store_credit'>('cash');
+ const [cashReceived, setCashReceived] = useState('');
+
+ const [discountPromptItem, setDiscountPromptItem] = useState(null);
+ const [couponCodeInput, setCouponCodeInput] = useState('');
+ const [discountAmountInput, setDiscountAmountInput] = useState('');
+ const couponInputRef = useRef(null);
+ const searchInputRef = useRef(null);
+
+ const [dailyReport, setDailyReport] = useState({ sales: 0, tx: 0, gst: 0, cash: 0, upi: 0, card: 0, products: {} as Record });
+
+ const [isRefundMode, setIsRefundMode] = useState(false);
+
+ const [customer, setCustomer] = useState<{ phone: string; points: number; name: string } | null>(null);
+ const [showCustomerPrompt, setShowCustomerPrompt] = useState(false);
+ const [customerPhoneInput, setCustomerPhoneInput] = useState('');
+ const [customerNameInput, setCustomerNameInput] = useState('');
+ const [isNewCustomer, setIsNewCustomer] = useState(false);
+
+ const [parkedCarts, setParkedCarts] = useState<{ id: string; cart: CartItem[]; time: number; customer: any }[]>([]);
+ const [showParkedPrompt, setShowParkedPrompt] = useState(false);
+
+ const inventory = useMemo(() => {
+ return (stockQ.data ?? []).map((r: Row, i: number) => {
+ const isWeighable = i % 4 === 0;
+ return {
+ id: fstr(r.productid),
+ name: fstr(r.productname) || 'Unnamed product',
+ sku: fstr(r.sku) || `SKU-${fstr(r.productid)}`,
+ closing: fnum(r.closing) ?? 0,
+ price: Math.floor(Math.random() * 500) + 50,
+ unit: isWeighable ? 'kg' : 'pc',
+ };
+ });
+ }, [stockQ.data]);
+
+ const handleProductScanned = useCallback(
+ (product: any) => {
+ if (product.closing <= 0 && !isRefundMode) return;
+ setCart((prev) => {
+ const existing = prev.find((item) => item.id === product.id && !!item.isRefund === isRefundMode);
+ if (existing) {
+ if (!isRefundMode && existing.qty >= product.closing) return prev;
+ return prev.map((item) =>
+ item.id === product.id && !!item.isRefund === isRefundMode ? { ...item, qty: item.qty + 1 } : item
+ );
+ }
+ return [
+ {
+ id: product.id,
+ name: product.name,
+ sku: product.sku,
+ price: product.price,
+ qty: product.unit === 'kg' ? 1.0 : 1,
+ maxQty: product.closing,
+ discountPerItem: 0,
+ unit: product.unit,
+ isRefund: isRefundMode,
+ },
+ ...prev,
+ ];
+ });
+ setSearch('');
+ },
+ [isRefundMode]
+ );
+
+ const updateCartQty = (id: string, newQty: number) => {
+ setCart((prev) =>
+ prev
+ .map((item) => {
+ if (item.id === id) {
+ if (newQty <= 0) return { ...item, qty: 0 };
+ if (newQty > item.maxQty) return item;
+ return { ...item, qty: newQty };
+ }
+ return item;
+ })
+ .filter((item) => item.qty > 0)
+ );
+ };
+
+ const removeCartItem = (id: string) => {
+ setCart((prev) => prev.filter((item) => item.id !== id));
+ };
+
+ const clearCart = () => setCart([]);
+
+ const applyDiscountToItem = () => {
+ if (!discountPromptItem) return;
+ const amt = parseFloat(discountAmountInput) || 0;
+ const code = couponCodeInput.trim().toUpperCase() || undefined;
+ setCart((prev) =>
+ prev.map((item) => (item.id === discountPromptItem ? { ...item, discountPerItem: amt, couponCode: code } : item))
+ );
+ setDiscountPromptItem(null);
+ setCouponCodeInput('');
+ setDiscountAmountInput('');
+ setTimeout(() => searchInputRef.current?.focus(), 100);
+ };
+
+ const removeCoupon = (id: string) => {
+ setCart((prev) =>
+ prev.map((item) => (item.id === id ? { ...item, discountPerItem: 0, couponCode: undefined } : item))
+ );
+ };
+
+ const cartWithPromos = useMemo(() => {
+ return cart.map((item) => {
+ if (item.isRefund) return { ...item, promoDiscount: 0, hasBogo: false };
+ let promoDiscount = 0;
+ let hasBogo = false;
+ if (item.unit !== 'kg' && item.qty >= 3) {
+ const freeItems = Math.floor(item.qty / 3);
+ promoDiscount = freeItems * item.price;
+ hasBogo = true;
+ }
+ return { ...item, promoDiscount, hasBogo };
+ });
+ }, [cart]);
+
+ const subtotal = cartWithPromos.reduce((sum, item) => sum + item.price * item.qty * (item.isRefund ? -1 : 1), 0);
+ const totalDiscount = cartWithPromos.reduce(
+ (sum, item) => sum + (item.discountPerItem * item.qty + (item.promoDiscount || 0)),
+ 0
+ );
+ const taxableAmount = Math.max(0, subtotal - totalDiscount);
+ const cgst = taxableAmount * 0.09;
+ const sgst = taxableAmount * 0.09;
+ const packagingCharge = cart.length > 0 && subtotal > 0 ? 10 : 0;
+ const grandTotal = subtotal < 0 ? subtotal : taxableAmount + cgst + sgst + packagingCharge;
+
+ const parkCart = () => {
+ if (cart.length === 0) return;
+ setParkedCarts((prev) => [
+ ...prev,
+ { id: `CART-${Date.now().toString().slice(-4)}`, cart, time: Date.now(), customer },
+ ]);
+ setCart([]);
+ setCustomer(null);
+ setIsRefundMode(false);
+ };
+
+ const retrieveCart = (pc: any) => {
+ setCart(pc.cart);
+ setCustomer(pc.customer);
+ setParkedCarts((prev) => prev.filter((p) => p.id !== pc.id));
+ setShowParkedPrompt(false);
+ };
+
+ useEffect(() => {
+ let barcode = '';
+ let lastKeyTime = Date.now();
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (
+ showPayment ||
+ showReceipt ||
+ showReport ||
+ discountPromptItem ||
+ showManualSearch ||
+ showCustomerPrompt ||
+ showParkedPrompt
+ )
+ return;
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
+ const currentTime = Date.now();
+ if (currentTime - lastKeyTime > 100) barcode = '';
+ lastKeyTime = currentTime;
+ if (e.key === 'Enter') {
+ if (barcode.length > 0) {
+ const product = inventory.find((p) => p.sku === barcode || p.id === barcode);
+ if (product) handleProductScanned(product);
+ barcode = '';
+ e.preventDefault();
+ }
+ } else if (e.key.length === 1) {
+ barcode += e.key;
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [
+ inventory,
+ handleProductScanned,
+ showPayment,
+ showReceipt,
+ showReport,
+ discountPromptItem,
+ showManualSearch,
+ showCustomerPrompt,
+ showParkedPrompt,
+ ]);
+
+ const handleSearchKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ const val = search.trim();
+ if (val) {
+ const product = inventory.find(
+ (p) => p.sku === val || p.id === val || p.name.toLowerCase() === val.toLowerCase()
+ );
+ if (product) {
+ handleProductScanned(product);
+ } else {
+ alert('Product not found in inventory.');
+ }
+ }
+ }
+ };
+
+ const getTodayKey = () => new Date().toISOString().split('T')[0];
+
+ const checkDailyReset = useCallback(() => {
+ const savedDate = localStorage.getItem('nearly_pos_date');
+ const today = getTodayKey();
+ if (savedDate !== today) {
+ localStorage.setItem('nearly_pos_date', today);
+ const initialReport = { sales: 0, tx: 0, gst: 0, cash: 0, upi: 0, card: 0, products: {} };
+ localStorage.setItem('nearly_pos_report', JSON.stringify(initialReport));
+ setDailyReport(initialReport);
+ } else {
+ const rep = localStorage.getItem('nearly_pos_report');
+ if (rep) setDailyReport(JSON.parse(rep));
+ }
+ }, []);
+
+ useEffect(() => {
+ checkDailyReset();
+ }, [checkDailyReset]);
+
+ const recordSale = () => {
+ checkDailyReset();
+ const rep = JSON.parse(localStorage.getItem('nearly_pos_report') || '{}');
+ rep.sales = (rep.sales || 0) + grandTotal;
+ rep.tx = (rep.tx || 0) + 1;
+ rep.gst = (rep.gst || 0) + cgst + sgst;
+ rep[payMethod] = (rep[payMethod] || 0) + grandTotal;
+ if (!rep.products) rep.products = {};
+ cart.forEach((item) => {
+ rep.products[item.name] = (rep.products[item.name] || 0) + item.qty;
+ });
+ localStorage.setItem('nearly_pos_report', JSON.stringify(rep));
+ setDailyReport(rep);
+ };
+
+ const handleCheckoutBtn = () => {
+ if (cart.length === 0) return;
+ setCashReceived('');
+ setPayMethod(grandTotal < 0 ? 'store_credit' : 'cash');
+ setShowPayment(true);
+ };
+
+ const confirmPayment = () => {
+ recordSale();
+ setShowPayment(false);
+ setShowReceipt(true);
+ };
+
+ const resetPos = () => {
+ setCart([]);
+ setCustomer(null);
+ setIsRefundMode(false);
+ setShowReceipt(false);
+ setSearch('');
+ setTimeout(() => searchInputRef.current?.focus(), 100);
+ };
+
+ const changeAmt = Math.max(0, (parseFloat(cashReceived) || 0) - grandTotal);
+
+ const checkCustomerPhone = () => {
+ if (customerPhoneInput.length >= 10) {
+ const db = JSON.parse(localStorage.getItem('nearly_pos_customers') || '{}');
+ if (db[customerPhoneInput]) {
+ // Existing customer
+ setCustomer({
+ phone: customerPhoneInput,
+ name: db[customerPhoneInput].name,
+ points: db[customerPhoneInput].points || Math.floor(Math.random() * 500) + 50,
+ });
+ closeCustomerPrompt();
+ } else {
+ // New customer
+ setIsNewCustomer(true);
+ }
+ }
+ };
+
+ const saveNewCustomer = () => {
+ if (customerNameInput.trim()) {
+ const db = JSON.parse(localStorage.getItem('nearly_pos_customers') || '{}');
+ const newCustomer = {
+ name: customerNameInput.trim(),
+ points: 50, // Starting points
+ };
+ db[customerPhoneInput] = newCustomer;
+ localStorage.setItem('nearly_pos_customers', JSON.stringify(db));
+
+ setCustomer({
+ phone: customerPhoneInput,
+ name: newCustomer.name,
+ points: newCustomer.points,
+ });
+ closeCustomerPrompt();
+ }
+ };
+
+ const closeCustomerPrompt = () => {
+ setShowCustomerPrompt(false);
+ setCustomerPhoneInput('');
+ setCustomerNameInput('');
+ setIsNewCustomer(false);
+ };
+
+ const cashDenominations = [10, 20, 50, 100, 200, 500, 2000];
+ const now = new Date();
+ const timeString = now.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' });
+ const dateString = now.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
+
+ return (
+
+
+
+ {/* ── LEFT PANEL: Cart Items ─────────────────────────────────────── */}
+
+
+ {/* Action Bar */}
+
+
+
+
+ POS Terminal
+
+ {isRefundMode && (
+
+ Refund Mode
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {/* Scan Bar */}
+
+
+
+
+
+ setSearch(e.target.value)}
+ onKeyDown={handleSearchKeyDown}
+ className="flex-1 bg-transparent border-none outline-none font-bold text-xl text-slate-800 placeholder:font-normal placeholder:text-slate-400"
+ autoFocus
+ />
+ {search && (
+
+ )}
+ {!search && (
+
+ Press Enter
+
+ )}
+
+
+
+ {/* Cart Table */}
+
+ {stockQ.isLoading ? (
+
+
+ Initializing inventory…
+
+ ) : cart.length === 0 ? (
+
+
+
+
+
+ Cart is empty
+ Scan a barcode or browse products to add items
+
+
+
+ ) : (
+
+
+
+ | Product |
+ Qty |
+ Rate |
+ Disc. |
+ Amount |
+ |
+
+
+
+ {cartWithPromos.map((item, idx) => (
+
+
+
+
+ {idx + 1}
+
+
+
+ {item.isRefund && (
+
+ Refund
+
+ )}
+ {item.name}
+
+ {item.sku}
+
+
+ |
+
+
+
+
+ updateCartQty(item.id, parseFloat(e.target.value) || 0)}
+ className="w-12 text-center text-sm font-bold text-slate-900 bg-transparent border-none outline-none hide-spin-button"
+ />
+ {item.unit}
+
+
+
+ |
+
+ ₹{item.price.toFixed(2)}
+ /{item.unit}
+ |
+
+ {item.discountPerItem > 0 || item.hasBogo ? (
+
+ {item.discountPerItem > 0 && (
+
+ -₹{(item.discountPerItem * item.qty).toFixed(2)}
+
+ )}
+ {item.hasBogo && (
+
+ -₹{(item.promoDiscount || 0).toFixed(2)}
+
+ )}
+
+ {item.discountPerItem > 0 && (
+
+ {item.couponCode || 'MANUAL'}
+
+
+ )}
+ {item.hasBogo && (
+
+ BOGO
+
+ )}
+
+
+ ) : (
+ —
+ )}
+ |
+
+ {item.isRefund ? '-' : ''}₹
+ {(
+ item.price * item.qty -
+ item.discountPerItem * item.qty -
+ (item.promoDiscount || 0)
+ ).toFixed(2)}
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+ )}
+
+
+ {/* Cart Footer Summary (visible on mobile) */}
+ {cart.length > 0 && (
+
+ {cart.length} item{cart.length !== 1 ? 's' : ''}
+ ₹{grandTotal.toFixed(2)}
+
+ )}
+
+
+ {/* ── RIGHT PANEL: Billing ───────────────────────────────────────── */}
+
+
+ {/* Panel Header */}
+
+
+
+ Checkout
+ {dateString} · {timeString}
+
+
+ {customer ? (
+
+
+
+ {customer.name || customer.phone}
+
+
+
+ {customer.points} pts
+
+
+ ) : (
+
+ )}
+
+
+
+ {/* Item Count */}
+
+
+ {cart.length} item{cart.length !== 1 ? 's' : ''} in cart
+
+
+
+ {/* Order Breakdown */}
+
+
+
+ Subtotal
+ ₹{subtotal.toFixed(2)}
+
+
+ {totalDiscount > 0 && (
+
+
+
+ Discounts
+
+ -₹{totalDiscount.toFixed(2)}
+
+ )}
+
+
+ CGST (9%)
+ ₹{cgst.toFixed(2)}
+
+
+ SGST (9%)
+ ₹{sgst.toFixed(2)}
+
+
+ Packaging
+ ₹{packagingCharge.toFixed(2)}
+
+
+ {cart.length === 0 && (
+
+ Add items to see totals
+
+ )}
+
+
+ {/* Grand Total + Pay */}
+
+
+
+ Grand Total
+ ₹{grandTotal.toFixed(2)}
+
+ {grandTotal < 0 && (
+
+ Refund Due
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* ── MODAL: Manual Product Search ──────────────────────────────── */}
+ {showManualSearch && (
+
+
+
+
+ Product Search
+
+
+
+
+ setSearch(e.target.value)}
+ autoFocus
+ className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm font-medium text-slate-800 outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400/20 transition-all"
+ />
+
+
+ {inventory
+ .filter(
+ (p) =>
+ !search ||
+ p.name.toLowerCase().includes(search.toLowerCase()) ||
+ p.sku.toLowerCase().includes(search.toLowerCase())
+ )
+ .map((product) => (
+
+ ))}
+
+
+
+ )}
+
+ {/* ── MODAL: Customer CRM ───────────────────────────────────────── */}
+ {showCustomerPrompt && (
+
+
+
+
+ {isNewCustomer ? 'New Customer' : 'Attach Customer'}
+ {isNewCustomer ? "Please enter the customer's name." : 'Enter mobile number to retrieve loyalty profile.'}
+
+ setCustomerPhoneInput(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && !isNewCustomer && checkCustomerPhone()}
+ disabled={isNewCustomer}
+ className={`w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl font-bold text-slate-800 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 transition-all ${isNewCustomer ? 'opacity-50 mb-3' : 'mb-4'}`}
+ autoFocus={!isNewCustomer}
+ />
+
+ {isNewCustomer && (
+ setCustomerNameInput(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && saveNewCustomer()}
+ className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl font-bold text-slate-800 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 transition-all mb-4"
+ autoFocus
+ />
+ )}
+
+ {isNewCustomer ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+ {/* ── MODAL: Parked Carts ───────────────────────────────────────── */}
+ {showParkedPrompt && (
+
+
+
+
+ Parked Sales
+
+
+
+
+ {parkedCarts.length === 0 ? (
+
+ ) : (
+ parkedCarts.map((pc) => (
+
+
+ {pc.id}
+
+ {new Date(pc.time).toLocaleTimeString()} · {pc.cart.length} items · {pc.customer ? pc.customer.phone : 'Guest'}
+
+
+
+
+ ))
+ )}
+
+
+
+ )}
+
+ {/* ── MODAL: Discount ───────────────────────────────────────────── */}
+ {discountPromptItem && (
+
+
+
+
+
+
+
+
+
+ Apply Discount
+ Enter a manual discount or attach a coupon code.
+
+
+
+
+
+ )}
+
+ {/* ── MODAL: Payment ────────────────────────────────────────────── */}
+ {showPayment && (
+
+
+ {/* Header */}
+
+
+
+ {grandTotal < 0 ? 'Refund Due' : 'Amount Due'}
+
+ ₹{Math.abs(grandTotal).toFixed(2)}
+
+
+
+
+
+ {/* Payment Method Tabs */}
+
+ Payment Method
+
+ {(grandTotal >= 0 ? ['cash', 'upi', 'card'] : ['cash', 'store_credit']).map((m) => {
+ const icons: Record = {
+ cash: ,
+ upi: ,
+ card: ,
+ store_credit: ,
+ };
+ const labels: Record = {
+ cash: 'Cash', upi: 'UPI', card: 'Card', store_credit: 'Store Credit',
+ };
+ return (
+
+ );
+ })}
+
+
+
+ {/* Cash Payment */}
+ {payMethod === 'cash' && grandTotal >= 0 && (
+
+
+
+ setCashReceived(e.target.value)}
+ className="w-full px-4 py-3 bg-slate-50 border-2 border-slate-200 rounded-xl font-black text-2xl text-slate-900 outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 transition-all text-right"
+ placeholder="0.00"
+ autoFocus
+ />
+
+ {/* Quick denominations */}
+
+ Quick Select
+
+ {cashDenominations
+ .filter((d) => d >= grandTotal * 0.5)
+ .slice(0, 6)
+ .map((d) => (
+
+ ))}
+
+
+
+ {cashReceived && parseFloat(cashReceived) >= grandTotal && (
+
+ Change Due
+ ₹{changeAmt.toFixed(2)}
+
+ )}
+
+ )}
+
+ {/* Cash Refund */}
+ {payMethod === 'cash' && grandTotal < 0 && (
+
+ Return Cash to Customer
+ ₹{Math.abs(grandTotal).toFixed(2)}
+
+ )}
+
+ {/* Store Credit */}
+ {payMethod === 'store_credit' && grandTotal < 0 && (
+
+ {customer ? (
+
+
+ Credit to: {customer.phone}
+
+ ) : (
+
+
+ Attach a customer to issue Store Credit
+
+
+ )}
+
+ )}
+
+ {/* UPI */}
+ {payMethod === 'upi' && grandTotal >= 0 && (
+
+
+
+
+ Scan QR Code to Pay
+ Awaiting payment confirmation…
+
+ )}
+
+ {/* Card */}
+ {payMethod === 'card' && grandTotal >= 0 && (
+
+
+ Tap, swipe or insert card
+ Waiting for terminal…
+
+ )}
+
+
+
+
+
+ )}
+
+ {/* ── MODAL: Receipt ────────────────────────────────────────────── */}
+ {showReceipt && (
+
+
+
+
+ Receipt
+
+
+
+
+
+
+ Departmental Store
+ GSTIN: 29ABCDE1234F1Z5
+ {new Date().toLocaleString('en-IN')}
+
+
+
+ {cartWithPromos.map((item) => (
+
+
+ {item.isRefund && REFUND} {item.name}
+
+
+ {item.qty} {item.unit} × ₹{item.price.toFixed(2)}
+ {item.isRefund ? '-' : ''}₹{(item.price * item.qty).toFixed(2)}
+
+ {(item.discountPerItem > 0 || item.hasBogo) && (
+
+ Discount {item.couponCode ? `(${item.couponCode})` : item.hasBogo ? '(BOGO)' : ''}
+ -₹{((item.discountPerItem * item.qty) + (item.promoDiscount || 0)).toFixed(2)}
+
+ )}
+
+ ))}
+
+
+
+ Subtotal₹{subtotal.toFixed(2)}
+ Discounts-₹{totalDiscount.toFixed(2)}
+ CGST (9%)₹{cgst.toFixed(2)}
+ SGST (9%)₹{sgst.toFixed(2)}
+ Packaging₹{packagingCharge.toFixed(2)}
+
+ Grand Total₹{grandTotal.toFixed(2)}
+
+
+ {grandTotal < 0 ? 'Refund via' : 'Paid via'}
+ {payMethod.replace('_', ' ')}
+
+
+
+ Thank you for shopping! 🛍
+
+
+
+
+
+
+
+
+ )}
+
+ {/* ── MODAL: Daily Report ───────────────────────────────────────── */}
+ {showReport && (
+
+
+
+
+ Daily Sales Report
+
+
+
+
+
+
+
+ Total Revenue
+ ₹{dailyReport.sales.toFixed(2)}
+
+
+ Transactions
+ {dailyReport.tx}
+
+
+ GST
+ ₹{dailyReport.gst.toFixed(2)}
+
+
+ Avg. Bill
+
+ ₹{dailyReport.tx > 0 ? (dailyReport.sales / dailyReport.tx).toFixed(0) : '0'}
+
+
+
+
+
+ Payment Split
+ {dailyReport.sales > 0 ? (
+ <>
+
+ {dailyReport.cash > 0 && (
+
+ )}
+ {dailyReport.upi > 0 && (
+
+ )}
+ {dailyReport.card > 0 && (
+
+ )}
+
+
+ {[
+ { key: 'cash', label: 'Cash', color: 'bg-emerald-500', val: dailyReport.cash },
+ { key: 'upi', label: 'UPI', color: 'bg-blue-500', val: dailyReport.upi },
+ { key: 'card', label: 'Card', color: 'bg-amber-500', val: dailyReport.card },
+ ].map((p) => (
+
+
+ {p.label}
+ ₹{p.val.toFixed(0)}
+
+ ))}
+
+ >
+ ) : (
+
+ No transactions recorded today.
+
+ )}
+
+
+
+ Top Products
+ {Object.keys(dailyReport.products || {}).length > 0 ? (
+
+ {Object.entries(dailyReport.products)
+ .sort(([, a], [, b]) => (b as number) - (a as number))
+ .map(([name, qty]) => (
+
+ {name}
+
+ {Number(qty).toFixed(2)} sold
+
+
+ ))}
+
+ ) : (
+
+ No products sold yet.
+
+ )}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/StoreCatalogView.tsx b/src/components/StoreCatalogView.tsx
index c1f6daf..6648c50 100644
--- a/src/components/StoreCatalogView.tsx
+++ b/src/components/StoreCatalogView.tsx
@@ -540,40 +540,45 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
sub="Browse the catalogue and add items to your store to request stock."
/>
) : (
-
+
-
+
-
+
{['Requested At', 'Product', 'Qty', 'Status', 'Resolved At'].map((h, i) => (
- | {h} |
+ {h} |
))}
-
+
{Object.entries(picks).map(([pid, data]: [string, any]) => {
const prod = products.find(p => p.id === pid);
const isApproved = data.status === 'Approved';
const isRejected = data.status === 'Rejected';
- const color = isApproved ? '#10b981' : isRejected ? '#f43f5e' : data.status === 'Cancelled' ? '#94a3b8' : '#f59e0b';
- const DIVIDER_C = '#f1f5f9';
+ const isCancelled = data.status === 'Cancelled';
return (
- { e.currentTarget.style.background = SURFACE_ALT; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
- |
-
- {data.requestedAt ? new Date(data.requestedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'}
-
+ |
+ |
+
+
+ {data.requestedAt ? new Date(data.requestedAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' }) : '—'}
+
+
+ {data.requestedAt ? new Date(data.requestedAt).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }) : ''}
+
+
|
-
+ |
{prod ? (
<>
- 
-
- {prod.name}
- {prod.sku}
+
+ 
+
+
+ {prod.name}
+ {prod.sku}
>
) : (
@@ -581,14 +586,29 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
)}
|
-
- {data.qty || '—'}
+ |
+
+ {data.qty || '—'}
+
|
-
-
+ |
+
+
+ {data.status || '—'}
+
|
-
-
+ |
+
{data.resolvedAt ? new Date(data.resolvedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'}
|
diff --git a/src/components/UserStorePage.tsx b/src/components/UserStorePage.tsx
index 1308990..02831aa 100644
--- a/src/components/UserStorePage.tsx
+++ b/src/components/UserStorePage.tsx
@@ -34,6 +34,7 @@ import StoreCatalogView from './StoreCatalogView';
import DispatchHubView from './DispatchHubView';
import DeliveryReportsView from './DeliveryReportsView';
import StoreQRView from './StoreQRView';
+import PosView from './PosView';
import UserStoreSidebar, { type UserNavItem } from './UserStoreSidebar';
import ComparisonModal from './ComparisonModal';
interface UserStorePageProps {
@@ -48,6 +49,7 @@ interface UserStorePageProps {
const NAV_ITEMS: UserNavItem[] = [
{ id: 'console', label: 'Store Console', icon: LayoutDashboard },
{ id: 'inventory', label: 'Products', icon: Layers },
+ { id: 'pos', label: 'POS Terminal', icon: ShoppingBag },
{ id: 'customers', label: 'Customers', icon: Users },
{ id: 'dispatch', label: 'Dispatch', icon: Route },
{ id: 'reports', label: 'Reports', icon: ClipboardList },
@@ -191,6 +193,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
// Logistics console — scoped to this user's store. These views own their
// loading/error states, so they don't need the store-console load gating below.
+ if (activeSection === 'pos') return ;
if (activeSection === 'dispatch') return ;
if (activeSection === 'reports') return ;
// Inventory & Catalog is its own page: the manager-curated catalog the user
@@ -278,6 +281,8 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
? storeName
: activeSection === 'inventory'
? 'Products'
+ : activeSection === 'pos'
+ ? 'POS Terminal'
: activeSection === 'account'
? 'My Account'
: activeSection.charAt(0).toUpperCase() + activeSection.slice(1),
@@ -285,6 +290,8 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
? LayoutDashboard
: activeSection === 'inventory'
? Layers
+ : activeSection === 'pos'
+ ? ShoppingBag
: activeSection === 'customers'
? Users
: activeSection === 'dispatch'
@@ -307,10 +314,10 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
className={`flex-1 min-w-0 transition-all duration-300 ${
// Dispatch is a full-bleed cockpit — fill the area exactly (no page
// padding) so it sits flush under the header. Other pages stay padded.
- activeSection === 'dispatch' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]'
+ activeSection === 'dispatch' || activeSection === 'pos' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]'
} ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'}`}
>
- {activeSection === 'dispatch' ? (
+ {activeSection === 'dispatch' || activeSection === 'pos' ? (
{renderSection()}
) : (
|