conversion done with astryx
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
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 { Search, Plus, 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';
|
||||
import { useNotify } from './Toasts';
|
||||
import { Dialog } from '@astryxdesign/core/Dialog';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { IconButton } from '@astryxdesign/core/IconButton';
|
||||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||||
import { NumberInput } from '@astryxdesign/core/NumberInput';
|
||||
import { Badge } from '@astryxdesign/core/Badge';
|
||||
import { Spinner } from '@astryxdesign/core/Spinner';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
|
||||
import {
|
||||
Table, TableHeader, TableHeaderCell, TableBody, TableRow, TableCell,
|
||||
} from '@astryxdesign/core/Table';
|
||||
|
||||
/**
|
||||
* PHASE 6 — the one modal shell for this screen.
|
||||
@@ -407,6 +418,14 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
.hide-spin-button { -moz-appearance: textfield; }
|
||||
.pos-scan-glow { box-shadow: 0 0 0 3px rgba(16,185,129,0.15), 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.pos-scan-glow-red { box-shadow: 0 0 0 3px rgba(239,68,68,0.15), 0 1px 3px rgba(0,0,0,0.08); }
|
||||
/* The scan field sits inside its own glowing frame, so the input's own
|
||||
chrome is removed and its type size restored. A cashier aims a
|
||||
scanner at this from arm's length — the size is the affordance. */
|
||||
.pos-scan-field input {
|
||||
font-size: 20px; font-weight: 700; background: transparent;
|
||||
border: none; box-shadow: none; height: auto; padding-left: 0;
|
||||
}
|
||||
.pos-scan-field input::placeholder { font-weight: 400; }
|
||||
@keyframes scan-pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
|
||||
.scan-pulse { animation: scan-pulse 1.8s ease-in-out infinite; }
|
||||
`}</style>
|
||||
@@ -427,43 +446,34 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* PHASE 6 — the till's action bar. Refund mode used to restyle every
|
||||
button inline (white-on-red); that state now reads from the Refund
|
||||
button's own variant plus the banner beside the title, so the other
|
||||
four stop changing colour for a mode they do not control. */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => setShowManualSearch(true)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded-lg border transition-colors ${isRefundMode ? 'bg-white/10 hover:bg-white/20 text-white border-white/15' : 'bg-slate-50 hover:bg-slate-100 text-slate-700 border-slate-200'}`}
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" /> Browse
|
||||
</button>
|
||||
<button
|
||||
<Button label="Browse" variant="secondary" size="sm" icon={<Search className="w-3.5 h-3.5" />} onClick={() => setShowManualSearch(true)} />
|
||||
<Button
|
||||
label="Refund"
|
||||
variant={isRefundMode ? 'destructive' : 'secondary'}
|
||||
size="sm"
|
||||
icon={<RefreshCcw className="w-3.5 h-3.5" />}
|
||||
onClick={() => setIsRefundMode(!isRefundMode)}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded-lg border transition-colors ${isRefundMode ? 'bg-white text-red-600 border-white' : 'bg-slate-50 hover:bg-slate-100 text-slate-700 border-slate-200'}`}
|
||||
>
|
||||
<RefreshCcw className="w-3.5 h-3.5" /> Refund
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<Button
|
||||
label="Park"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={<Save className="w-3.5 h-3.5" />}
|
||||
isDisabled={cart.length === 0}
|
||||
onClick={parkCart}
|
||||
disabled={cart.length === 0}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded-lg border transition-colors disabled:opacity-40 ${isRefundMode ? 'bg-white/10 hover:bg-white/20 text-white border-white/15' : 'bg-slate-50 hover:bg-slate-100 text-slate-700 border-slate-200'}`}
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" /> Park
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowParkedPrompt(true)}
|
||||
className={`relative flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded-lg border transition-colors ${isRefundMode ? 'bg-white/10 hover:bg-white/20 text-white border-white/15' : 'bg-slate-50 hover:bg-slate-100 text-slate-700 border-slate-200'}`}
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" /> Retrieve
|
||||
{parkedCarts.length > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-amber-400 text-slate-900 rounded-full text-[9px] font-black flex items-center justify-center">
|
||||
{parkedCarts.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { checkDailyReset(); setShowReport(true); }}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold rounded-lg border transition-colors ${isRefundMode ? 'bg-white/10 hover:bg-white/20 text-white border-white/15' : 'bg-slate-50 hover:bg-slate-100 text-slate-700 border-slate-200'}`}
|
||||
>
|
||||
<PieChart className="w-3.5 h-3.5" /> Report
|
||||
</button>
|
||||
/>
|
||||
{/* The parked count was a floating dot pinned to the button corner,
|
||||
outside its label — nothing announced it. */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button label="Retrieve" variant="secondary" size="sm" icon={<Download className="w-3.5 h-3.5" />} onClick={() => setShowParkedPrompt(true)} />
|
||||
{parkedCarts.length > 0 && <Badge variant="warning" label={String(parkedCarts.length)} />}
|
||||
</div>
|
||||
<Button label="Report" variant="secondary" size="sm" icon={<PieChart className="w-3.5 h-3.5" />} onClick={() => { checkDailyReset(); setShowReport(true); }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -473,21 +483,26 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<div className={`scan-pulse ${isRefundMode ? 'text-red-400' : 'text-emerald-500'}`}>
|
||||
<Barcode className="w-7 h-7" />
|
||||
</div>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder={isRefundMode ? 'Scan item to refund…' : 'Scan barcode or type SKU…'}
|
||||
value={search}
|
||||
onChange={(e) => 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 && (
|
||||
<button onClick={() => setSearch('')} className="text-slate-400 hover:text-slate-600">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{/* The scan field keeps its oversized type deliberately — an
|
||||
operator aims a scanner at this from arm's length and the size
|
||||
is the affordance, so `pos-scan-field` restores the 20px bold
|
||||
face that TextInput's `lg` does not reach. What it gains is a
|
||||
real label (it had none at all) and a clear button that is now
|
||||
part of the control rather than a bare <button> beside it. */}
|
||||
<div className="flex-1 pos-scan-field">
|
||||
<TextInput
|
||||
ref={searchInputRef}
|
||||
label={isRefundMode ? 'Scan item to refund' : 'Scan barcode or type SKU'}
|
||||
isLabelHidden
|
||||
size="lg"
|
||||
placeholder={isRefundMode ? 'Scan item to refund…' : 'Scan barcode or type SKU…'}
|
||||
value={search}
|
||||
onChange={(v) => setSearch(v)}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
hasClear
|
||||
hasAutoFocus
|
||||
/>
|
||||
</div>
|
||||
{!search && (
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-widest hidden md:block">
|
||||
Press Enter
|
||||
@@ -499,95 +514,78 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
{/* Cart Table */}
|
||||
<div className="flex-1 overflow-y-auto bg-white">
|
||||
{stockQ.isLoading ? (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-3 text-slate-400">
|
||||
<div className="w-8 h-8 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-sm font-semibold">Initializing inventory…</p>
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<Spinner size="xl" label="Initializing inventory…" />
|
||||
</div>
|
||||
) : cart.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-5 text-slate-300 select-none">
|
||||
<div className="w-24 h-24 rounded-3xl bg-slate-100 flex items-center justify-center">
|
||||
<ShoppingCart className="w-12 h-12 text-slate-300" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="font-bold text-slate-400 text-lg">Cart is empty</p>
|
||||
<p className="text-sm text-slate-400 mt-1">Scan a barcode or browse products to add items</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowManualSearch(true)}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-slate-900 text-white text-sm font-bold rounded-xl hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<Search className="w-4 h-4" /> Browse Products
|
||||
</button>
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
icon={<ShoppingCart className="w-12 h-12" />}
|
||||
title="Cart is empty"
|
||||
description="Scan a barcode or browse products to add items"
|
||||
actions={<Button label="Browse Products" icon={<Search className="w-4 h-4" />} onClick={() => setShowManualSearch(true)} />}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full overflow-x-auto custom-scrollbar">
|
||||
<table className="w-full text-left border-collapse min-w-[500px]">
|
||||
<thead className="sticky top-0 z-10 bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="px-5 py-3 text-[11px] font-bold uppercase tracking-wider text-slate-500">Product</th>
|
||||
<th className="px-4 py-3 text-[11px] font-bold uppercase tracking-wider text-slate-500 text-center">Qty</th>
|
||||
<th className="px-4 py-3 text-[11px] font-bold uppercase tracking-wider text-slate-500 text-right">Rate</th>
|
||||
<th className="px-4 py-3 text-[11px] font-bold uppercase tracking-wider text-slate-500 text-right">Disc.</th>
|
||||
<th className="px-4 py-3 text-[11px] font-bold uppercase tracking-wider text-slate-500 text-right">Amount</th>
|
||||
<th className="px-5 py-3 text-[11px] font-bold uppercase tracking-wider text-slate-500 text-center w-20"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Table style={{ minWidth: 500 }}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Product</TableHeaderCell>
|
||||
<TableHeaderCell>Qty</TableHeaderCell>
|
||||
<TableHeaderCell>Rate</TableHeaderCell>
|
||||
<TableHeaderCell>Disc.</TableHeaderCell>
|
||||
<TableHeaderCell>Amount</TableHeaderCell>
|
||||
<TableHeaderCell><span className="sr-only">Row actions</span></TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{cartWithPromos.map((item, idx) => (
|
||||
<tr
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className={`group border-b border-slate-100 hover:bg-slate-50/80 transition-colors ${item.isRefund ? 'bg-red-50/50' : ''}`}
|
||||
style={item.isRefund ? { background: 'rgba(244,63,94,0.06)' } : undefined}
|
||||
>
|
||||
<td className="px-5 py-3.5">
|
||||
<TableCell>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<span className="text-[10px] font-black text-slate-500">{idx + 1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.isRefund && (
|
||||
<span className="px-1.5 py-0.5 bg-red-100 text-red-600 text-[9px] uppercase font-black rounded tracking-wider">
|
||||
Refund
|
||||
</span>
|
||||
)}
|
||||
<span className="font-bold text-slate-900 text-[14px] leading-snug">{item.name}</span>
|
||||
{item.isRefund && <Badge variant="error" label="Refund" />}
|
||||
<span className="font-bold text-[14px] leading-snug">{item.name}</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-400 font-mono mt-0.5">{item.sku}</div>
|
||||
<div className="text-[11px] font-mono mt-0.5 opacity-50">{item.sku}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-center">
|
||||
<div className="inline-flex items-center bg-slate-100 rounded-lg overflow-hidden border border-slate-200">
|
||||
<button
|
||||
onClick={() => updateCartQty(item.id, item.qty - 1)}
|
||||
className="w-8 h-8 flex items-center justify-center text-slate-500 hover:bg-red-50 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<Minus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div className="flex items-center gap-0.5 px-1">
|
||||
<input
|
||||
type="number"
|
||||
step={item.unit === 'kg' ? '0.001' : '1'}
|
||||
value={item.qty}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-[10px] font-bold text-slate-400">{item.unit}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => updateCartQty(item.id, item.qty + 1)}
|
||||
disabled={item.qty >= item.maxQty}
|
||||
className="w-8 h-8 flex items-center justify-center text-slate-500 hover:bg-emerald-50 hover:text-emerald-600 transition-colors disabled:opacity-30"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{/* The −/+ pair plus a bare number box is one NumberInput
|
||||
now: it brings its own steppers, so the two icon-only
|
||||
buttons (neither of which had an accessible name) are
|
||||
gone, and `max` finally expresses the stock ceiling that
|
||||
only the + button used to enforce. */}
|
||||
<div className="flex items-center gap-1 w-28">
|
||||
<NumberInput
|
||||
label={`Quantity for ${item.name}`}
|
||||
isLabelHidden
|
||||
size="sm"
|
||||
value={item.qty}
|
||||
onChange={(v) => updateCartQty(item.id, v ?? 0)}
|
||||
step={item.unit === 'kg' ? 0.001 : 1}
|
||||
min={0}
|
||||
max={item.maxQty}
|
||||
/>
|
||||
<span className="text-[10px] font-bold shrink-0 opacity-50">{item.unit}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-right">
|
||||
<span className="font-bold text-slate-700 text-sm">₹{item.price.toFixed(2)}</span>
|
||||
<span className="block text-[10px] text-slate-400">/{item.unit}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-right">
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-right">
|
||||
<span className="font-bold text-sm">₹{item.price.toFixed(2)}</span>
|
||||
<span className="block text-[10px] opacity-50">/{item.unit}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.discountPerItem > 0 || item.hasBogo ? (
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{item.discountPerItem > 0 && (
|
||||
@@ -600,60 +598,66 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
-₹{(item.promoDiscount || 0).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex gap-1 flex-wrap justify-end">
|
||||
<div className="flex gap-1 flex-wrap justify-end items-center">
|
||||
{item.discountPerItem > 0 && (
|
||||
<span className="flex items-center gap-0.5 text-[9px] font-black text-emerald-700 bg-emerald-100 px-1.5 py-0.5 rounded-md uppercase tracking-wide">
|
||||
{item.couponCode || 'MANUAL'}
|
||||
<button onClick={() => removeCoupon(item.id)} className="ml-0.5 hover:text-red-500">
|
||||
<X className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{item.hasBogo && (
|
||||
<span className="text-[9px] font-black text-amber-700 bg-amber-100 px-1.5 py-0.5 rounded-md uppercase tracking-wide">
|
||||
BOGO
|
||||
</span>
|
||||
<>
|
||||
<Badge variant="success" label={item.couponCode || 'MANUAL'} />
|
||||
<IconButton
|
||||
label={`Remove discount from ${item.name}`}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon={<X className="w-3 h-3" />}
|
||||
onClick={() => removeCoupon(item.id)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{item.hasBogo && <Badge variant="warning" label="BOGO" />}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-300 text-sm">—</span>
|
||||
<span className="text-sm block text-right opacity-30">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={`px-4 py-3.5 text-right font-bold text-[15px] ${item.isRefund ? 'text-red-600' : 'text-slate-900'}`}>
|
||||
{item.isRefund ? '-' : ''}₹
|
||||
{(
|
||||
item.price * item.qty -
|
||||
item.discountPerItem * item.qty -
|
||||
(item.promoDiscount || 0)
|
||||
).toFixed(2)}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-center">
|
||||
<div className="flex items-center justify-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className={`text-right font-bold text-[15px] ${item.isRefund ? 'text-red-600' : ''}`}>
|
||||
{item.isRefund ? '-' : ''}₹
|
||||
{(
|
||||
item.price * item.qty -
|
||||
item.discountPerItem * item.qty -
|
||||
(item.promoDiscount || 0)
|
||||
).toFixed(2)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{/* The `opacity-0 group-hover` reveal is gone. On a till
|
||||
these two are the only way to fix a mis-scan, and they
|
||||
were invisible until the mouse happened to be over the
|
||||
row — while still being in the tab order. */}
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<IconButton
|
||||
label={`Apply discount to ${item.name}`}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={<Tag className="w-3.5 h-3.5" />}
|
||||
onClick={() => {
|
||||
setDiscountPromptItem(item.id);
|
||||
setCouponCodeInput(item.couponCode || '');
|
||||
setDiscountAmountInput(item.discountPerItem > 0 ? item.discountPerItem.toString() : '');
|
||||
}}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-lg bg-blue-50 text-blue-500 hover:bg-blue-100 transition-colors"
|
||||
title="Apply Discount"
|
||||
>
|
||||
<Tag className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<IconButton
|
||||
label={`Remove ${item.name} from cart`}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
icon={<Trash2 className="w-3.5 h-3.5" />}
|
||||
onClick={() => removeCartItem(item.id)}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-lg bg-red-50 text-red-500 hover:bg-red-100 transition-colors"
|
||||
title="Remove"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -683,21 +687,26 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<div className="flex items-center gap-1.5 justify-end">
|
||||
<User className="w-3 h-3 text-blue-500" />
|
||||
<span className="text-xs font-bold text-slate-900">{customer.name || customer.phone}</span>
|
||||
<button onClick={() => setCustomer(null)} className="text-slate-400 hover:text-slate-600 ml-1">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
<IconButton
|
||||
label="Detach customer from this sale"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon={<X className="w-3 h-3" />}
|
||||
onClick={() => setCustomer(null)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] font-bold text-amber-500 mt-0.5 flex items-center justify-end gap-1">
|
||||
<Zap className="w-2.5 h-2.5" /> {customer.points} pts
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
label="Customer"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={<Plus className="w-3.5 h-3.5" />}
|
||||
onClick={() => setShowCustomerPrompt(true)}
|
||||
className="flex items-center gap-1.5 bg-slate-50 hover:bg-slate-100 border border-slate-200 text-slate-700 text-xs font-bold px-3 py-2 rounded-xl transition-colors"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> Customer
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -761,14 +770,19 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
{/* PAY keeps its own emerald face and full width: it is the one
|
||||
control the whole screen exists to reach, and it is hit hundreds of
|
||||
times a shift. Everything around it is Astryx; this stays a
|
||||
deliberate exception, marked as one. */}
|
||||
<div className="flex gap-2 pos-pay-row">
|
||||
<IconButton
|
||||
label="Clear cart"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
icon={<Trash2 className="w-5 h-5" />}
|
||||
isDisabled={cart.length === 0}
|
||||
onClick={clearCart}
|
||||
disabled={cart.length === 0}
|
||||
className="w-12 h-12 flex items-center justify-center bg-white border border-slate-200 hover:bg-red-50 hover:border-red-200 text-slate-500 hover:text-red-500 rounded-xl transition-all disabled:opacity-40"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
/>
|
||||
<button
|
||||
onClick={handleCheckoutBtn}
|
||||
disabled={cart.length === 0 || !customer}
|
||||
@@ -787,18 +801,18 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<h3 className="font-bold text-slate-900 flex items-center gap-2">
|
||||
<Search className="w-5 h-5 text-indigo-500" /> Product Search
|
||||
</h3>
|
||||
<button onClick={() => setShowManualSearch(false)} className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-400">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<IconButton label="Close product search" variant="ghost" size="sm" icon={<X className="w-4 h-4" />} onClick={() => setShowManualSearch(false)} />
|
||||
</div>
|
||||
<div className="px-5 py-3 border-b border-slate-100">
|
||||
<input
|
||||
type="text"
|
||||
<TextInput
|
||||
label="Search products"
|
||||
isLabelHidden
|
||||
placeholder="Search by name, SKU…"
|
||||
value={search}
|
||||
onChange={(e) => 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"
|
||||
onChange={(v) => setSearch(v)}
|
||||
startIcon={<Search className="w-4 h-4" />}
|
||||
hasClear
|
||||
hasAutoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
@@ -810,6 +824,10 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
p.sku.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
.map((product) => (
|
||||
// Stays a native button: it is a full-width result row whose
|
||||
// whole surface is the hit target, which Button's fixed
|
||||
// padding and centred label cannot express. It already had a
|
||||
// disabled state and a text label, so nothing is lost.
|
||||
<button
|
||||
key={product.id}
|
||||
disabled={product.closing <= 0}
|
||||
@@ -840,52 +858,52 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center">
|
||||
<User className="w-6 h-6 text-blue-500" />
|
||||
</div>
|
||||
<button onClick={closeCustomerPrompt} className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-400">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<IconButton label="Close customer panel" variant="ghost" size="sm" icon={<X className="w-4 h-4" />} onClick={closeCustomerPrompt} />
|
||||
</div>
|
||||
<h3 className="font-bold text-xl text-slate-900 mb-1">{isNewCustomer ? 'New Customer' : 'Attach Customer'}</h3>
|
||||
<p className="text-sm text-slate-500 mb-5">{isNewCustomer ? "Please enter the customer's name." : 'Enter mobile number to retrieve loyalty profile.'}</p>
|
||||
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="e.g. 9876543210"
|
||||
value={customerPhoneInput}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
{/* Was `type="tel"` with no filtering, so letters reached a field
|
||||
the Find button gates on `.length >= 10` — "abcdefghij" opened
|
||||
a lookup. Astryx has no tel type; digits-only in the handler is
|
||||
the stricter fix anyway. */}
|
||||
<div className="mb-4">
|
||||
<TextInput
|
||||
label="Customer mobile number"
|
||||
placeholder="e.g. 9876543210"
|
||||
value={customerPhoneInput}
|
||||
onChange={(v) => setCustomerPhoneInput(v.replace(/\D/g, '').slice(0, 10))}
|
||||
onEnter={() => { if (!isNewCustomer) checkCustomerPhone(); }}
|
||||
isDisabled={isNewCustomer}
|
||||
hasAutoFocus={!isNewCustomer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isNewCustomer && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Customer Name"
|
||||
value={customerNameInput}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<TextInput
|
||||
label="Customer name"
|
||||
placeholder="Customer Name"
|
||||
value={customerNameInput}
|
||||
onChange={(v) => setCustomerNameInput(v)}
|
||||
onEnter={saveNewCustomer}
|
||||
hasAutoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isNewCustomer ? (
|
||||
<button
|
||||
<Button
|
||||
label="Save & Attach"
|
||||
isDisabled={!customerNameInput.trim()}
|
||||
onClick={saveNewCustomer}
|
||||
disabled={!customerNameInput.trim()}
|
||||
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-200 disabled:text-slate-400 text-white rounded-xl font-bold transition-colors"
|
||||
>
|
||||
Save & Attach
|
||||
</button>
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
label="Find & Attach"
|
||||
isDisabled={customerPhoneInput.length < 10}
|
||||
onClick={checkCustomerPhone}
|
||||
disabled={customerPhoneInput.length < 10}
|
||||
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-200 disabled:text-slate-400 text-white rounded-xl font-bold transition-colors"
|
||||
>
|
||||
Find & Attach
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PosModal>
|
||||
@@ -898,16 +916,11 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<h3 className="font-bold text-slate-900 flex items-center gap-2">
|
||||
<Download className="w-5 h-5 text-blue-500" /> Parked Sales
|
||||
</h3>
|
||||
<button onClick={() => setShowParkedPrompt(false)} className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-400">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<IconButton label="Close parked sales" variant="ghost" size="sm" icon={<X className="w-4 h-4" />} onClick={() => setShowParkedPrompt(false)} />
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
{parkedCarts.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<Save className="w-10 h-10 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm font-medium">No parked sales</p>
|
||||
</div>
|
||||
<EmptyState isCompact icon={<Save className="w-10 h-10" />} title="No parked sales" />
|
||||
) : (
|
||||
parkedCarts.map((pc) => (
|
||||
<div key={pc.id} className="bg-slate-50 border border-slate-200 rounded-xl p-4 flex justify-between items-center hover:border-blue-200 transition-colors">
|
||||
@@ -917,12 +930,7 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
{new Date(pc.time).toLocaleTimeString()} · {pc.cart.length} items · {pc.customer ? pc.customer.phone : 'Guest'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => retrieveCart(pc)}
|
||||
className="px-4 py-2 bg-blue-600 text-white text-sm font-bold rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Retrieve
|
||||
</button>
|
||||
<Button label="Retrieve" size="sm" onClick={() => retrieveCart(pc)} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -938,47 +946,28 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<div className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center">
|
||||
<Tag className="w-6 h-6 text-emerald-600" />
|
||||
</div>
|
||||
<button onClick={() => setDiscountPromptItem(null)} className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-400">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<IconButton label="Close discount panel" variant="ghost" size="sm" icon={<X className="w-4 h-4" />} onClick={() => setDiscountPromptItem(null)} />
|
||||
</div>
|
||||
<h3 className="font-bold text-xl text-slate-900 mb-1">Apply Discount</h3>
|
||||
<p className="text-sm text-slate-500 mb-5">Enter a manual discount or attach a coupon code.</p>
|
||||
<div className="space-y-3 mb-5">
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-1.5">
|
||||
Discount Amount (₹)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0.00"
|
||||
value={discountAmountInput}
|
||||
onChange={(e) => setDiscountAmountInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && applyDiscountToItem()}
|
||||
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-emerald-400 focus:ring-2 focus:ring-emerald-400/20 transition-all"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-1.5">
|
||||
Coupon Code (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. OFF10"
|
||||
value={couponCodeInput}
|
||||
onChange={(e) => setCouponCodeInput(e.target.value.toUpperCase())}
|
||||
onKeyDown={(e) => e.key === 'Enter' && applyDiscountToItem()}
|
||||
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl font-bold text-slate-800 uppercase outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<NumberInput
|
||||
label="Discount Amount (₹)"
|
||||
value={discountAmountInput === '' ? null : Number(discountAmountInput)}
|
||||
onChange={(v) => setDiscountAmountInput(v == null ? '' : String(v))}
|
||||
min={0}
|
||||
step={0.01}
|
||||
/>
|
||||
<TextInput
|
||||
label="Coupon Code"
|
||||
isOptional
|
||||
placeholder="e.g. OFF10"
|
||||
value={couponCodeInput}
|
||||
onChange={(v) => setCouponCodeInput(v.toUpperCase())}
|
||||
onEnter={applyDiscountToItem}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={applyDiscountToItem}
|
||||
className="w-full py-3 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl font-bold transition-colors"
|
||||
>
|
||||
Confirm Discount
|
||||
</button>
|
||||
<Button label="Confirm Discount" onClick={applyDiscountToItem} />
|
||||
</div>
|
||||
</PosModal>
|
||||
)}
|
||||
@@ -994,16 +983,27 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
</p>
|
||||
<p className="text-4xl font-black tracking-tight text-slate-900">₹{Math.abs(grandTotal).toFixed(2)}</p>
|
||||
</div>
|
||||
<button onClick={() => setShowPayment(false)} className="w-8 h-8 flex items-center justify-center rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-500 mt-1">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<IconButton label="Close payment panel" variant="ghost" size="sm" icon={<X className="w-4 h-4" />} onClick={() => setShowPayment(false)} />
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-5">
|
||||
{/* Payment Method Tabs */}
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-2.5">Payment Method</p>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{/* SegmentedControl, not three buttons and not SelectableCard.
|
||||
This is a single-select, and as plain buttons the active
|
||||
method lived only in border colour. SelectableCard was tried
|
||||
first and rejected: verified in Chrome, it renders a
|
||||
focusable div carrying only `aria-label` — no role, no
|
||||
aria-checked — and it drops a `role`/`aria-checked` you pass
|
||||
it, so the choice stayed silent. SegmentedControl emits a
|
||||
real radiogroup with arrow-key navigation. */}
|
||||
<SegmentedControl
|
||||
label="Payment method"
|
||||
layout="fill"
|
||||
value={payMethod}
|
||||
onChange={(v) => setPayMethod(v as any)}
|
||||
>
|
||||
{(grandTotal >= 0 ? ['cash', 'upi', 'card'] : ['cash', 'store_credit']).map((m) => {
|
||||
const icons: Record<string, React.ReactNode> = {
|
||||
cash: <Wallet className="w-4 h-4" />,
|
||||
@@ -1014,38 +1014,22 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
const labels: Record<string, string> = {
|
||||
cash: 'Cash', upi: 'UPI', card: 'Card', store_credit: 'Store Credit',
|
||||
};
|
||||
return (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setPayMethod(m as any)}
|
||||
className={`flex flex-col items-center gap-1.5 py-3 rounded-xl border-2 font-bold text-sm transition-all ${
|
||||
payMethod === m
|
||||
? 'border-emerald-500 bg-emerald-50 text-emerald-700'
|
||||
: 'border-slate-200 bg-slate-50 text-slate-500 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{icons[m]}
|
||||
<span className="text-[11px]">{labels[m]}</span>
|
||||
</button>
|
||||
);
|
||||
return <SegmentedControlItem key={m} value={m} label={labels[m]} icon={icons[m]} />;
|
||||
})}
|
||||
</div>
|
||||
</SegmentedControl>
|
||||
</div>
|
||||
|
||||
{/* Cash Payment */}
|
||||
{payMethod === 'cash' && grandTotal >= 0 && (
|
||||
<div className="space-y-4 animate-in slide-in-from-bottom-2 duration-200">
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-2">
|
||||
Cash Received
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={cashReceived}
|
||||
onChange={(e) => 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
|
||||
<div className="pos-cash-field">
|
||||
<NumberInput
|
||||
label="Cash Received"
|
||||
value={cashReceived === '' ? null : Number(cashReceived)}
|
||||
onChange={(v) => setCashReceived(v == null ? '' : String(v))}
|
||||
min={0}
|
||||
step={0.01}
|
||||
hasAutoFocus
|
||||
/>
|
||||
</div>
|
||||
{/* Quick denominations */}
|
||||
@@ -1056,24 +1040,20 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
.filter((d) => d >= grandTotal * 0.5)
|
||||
.slice(0, 6)
|
||||
.map((d) => (
|
||||
<button
|
||||
<Button
|
||||
key={d}
|
||||
label={`₹${d}`}
|
||||
size="sm"
|
||||
variant={parseFloat(cashReceived) === d ? 'primary' : 'secondary'}
|
||||
onClick={() => setCashReceived(d.toString())}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${
|
||||
parseFloat(cashReceived) === d
|
||||
? 'bg-slate-900 text-white border-slate-900'
|
||||
: 'bg-slate-50 text-slate-700 border-slate-200 hover:border-slate-400'
|
||||
}`}
|
||||
>
|
||||
₹{d}
|
||||
</button>
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
<Button
|
||||
label="Exact"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setCashReceived(Math.ceil(grandTotal).toString())}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-bold border bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100"
|
||||
>
|
||||
Exact
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{cashReceived && parseFloat(cashReceived) >= grandTotal && (
|
||||
@@ -1105,12 +1085,12 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<div className="text-center p-5 border-2 border-dashed border-red-200 bg-red-50 rounded-xl">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 mx-auto mb-2" />
|
||||
<p className="text-sm font-bold text-red-600 mb-3">Attach a customer to issue Store Credit</p>
|
||||
<button
|
||||
<Button
|
||||
label="Add Customer"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => { setShowPayment(false); setShowCustomerPrompt(true); }}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-bold hover:bg-red-700"
|
||||
>
|
||||
Add Customer
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1136,6 +1116,8 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Same exception as PAY, and for the same reason: this is the
|
||||
commit point of the transaction. */}
|
||||
<button
|
||||
onClick={confirmPayment}
|
||||
disabled={payMethod === 'store_credit' && !customer}
|
||||
@@ -1154,9 +1136,7 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<h3 className="font-bold text-slate-900 flex items-center gap-2">
|
||||
<Receipt className="w-4 h-4" /> Receipt
|
||||
</h3>
|
||||
<button onClick={resetPos} className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-400">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<IconButton label="Close receipt" variant="ghost" size="sm" icon={<X className="w-4 h-4" />} onClick={resetPos} />
|
||||
</div>
|
||||
|
||||
<div className="p-6 font-mono text-sm text-black print:p-0">
|
||||
@@ -1205,15 +1185,8 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
</div>
|
||||
|
||||
<div className="px-5 py-4 border-t border-slate-100 bg-slate-50 flex gap-3 print:hidden">
|
||||
<button onClick={resetPos} className="flex-1 py-3 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold hover:bg-slate-50 transition-colors">
|
||||
New Sale
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.print()}
|
||||
className="flex-1 py-3 bg-slate-900 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<Receipt className="w-4 h-4" /> Print
|
||||
</button>
|
||||
<Button label="New Sale" variant="secondary" onClick={resetPos} />
|
||||
<Button label="Print" icon={<Receipt className="w-4 h-4" />} onClick={() => window.print()} />
|
||||
</div>
|
||||
</PosModal>
|
||||
)}
|
||||
@@ -1225,9 +1198,7 @@ export default function PosView({ locationid, tenantId = FIESTA_TENANT_ID }: Pos
|
||||
<h3 className="font-bold text-slate-900 flex items-center gap-2">
|
||||
<PieChart className="w-5 h-5 text-emerald-500" /> Daily Sales Report
|
||||
</h3>
|
||||
<button onClick={() => setShowReport(false)} className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-400">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<IconButton label="Close daily report" variant="ghost" size="sm" icon={<X className="w-4 h-4" />} onClick={() => setShowReport(false)} />
|
||||
</div>
|
||||
|
||||
<div className="p-5 max-h-[70vh] overflow-y-auto space-y-5">
|
||||
|
||||
Reference in New Issue
Block a user