feat(pos): set a per-item note on each cart line in POS v2
CI/CD / CI · API (dotnet build + test) (push) Successful in 43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m13s
CI/CD / CI · Admin Web (tsc) (push) Successful in 40s
CI/CD / CI · Website (tsc) (push) Successful in 48s
CI/CD / CI · Koja (tsc) (push) Successful in 52s
CI/CD / Deploy · all services (push) Successful in 3m9s
CI/CD / CI · API (dotnet build + test) (push) Successful in 43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m13s
CI/CD / CI · Admin Web (tsc) (push) Successful in 40s
CI/CD / CI · Website (tsc) (push) Successful in 48s
CI/CD / CI · Koja (tsc) (push) Successful in 52s
CI/CD / Deploy · all services (push) Successful in 3m9s
The cart store, order payload (create + add-items + offline), KDS ticket and receipt already supported per-item notes — but POS v2 had no way to enter one. Adds a note button on each cart line that toggles an inline input (e.g. "no sugar"); the note shows highlighted when set and rides along to the kitchen/bar ticket. No backend change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
|||||||
Search, Plus, Minus, Trash2, Send, CreditCard, Pause, SplitSquareHorizontal,
|
Search, Plus, Minus, Trash2, Send, CreditCard, Pause, SplitSquareHorizontal,
|
||||||
X, WifiOff, ShoppingCart, Users, Coffee, ArrowRight, LayoutGrid, Armchair,
|
X, WifiOff, ShoppingCart, Users, Coffee, ArrowRight, LayoutGrid, Armchair,
|
||||||
Banknote, Check, Delete, ReceiptText, ShoppingBag, Loader2, RotateCcw,
|
Banknote, Check, Delete, ReceiptText, ShoppingBag, Loader2, RotateCcw,
|
||||||
BadgePercent, Sparkles, Home,
|
BadgePercent, Sparkles, Home, StickyNote,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { notify } from "@/lib/notify";
|
import { notify } from "@/lib/notify";
|
||||||
@@ -101,6 +101,7 @@ export function Pos2Screen() {
|
|||||||
const addItem = useCartStore((s) => s.addItem);
|
const addItem = useCartStore((s) => s.addItem);
|
||||||
const updateQty = useCartStore((s) => s.updateQty);
|
const updateQty = useCartStore((s) => s.updateQty);
|
||||||
const removeItem = useCartStore((s) => s.removeItem);
|
const removeItem = useCartStore((s) => s.removeItem);
|
||||||
|
const setNotes = useCartStore((s) => s.setNotes);
|
||||||
const setTableId = useCartStore((s) => s.setTableId);
|
const setTableId = useCartStore((s) => s.setTableId);
|
||||||
const setOrderType = useCartStore((s) => s.setOrderType);
|
const setOrderType = useCartStore((s) => s.setOrderType);
|
||||||
const hydrateFromOrder = useCartStore((s) => s.hydrateFromOrder);
|
const hydrateFromOrder = useCartStore((s) => s.hydrateFromOrder);
|
||||||
@@ -320,6 +321,7 @@ export function Pos2Screen() {
|
|||||||
cafeId, lines: live, subtotal, discount, tax, total, count, pendingCount,
|
cafeId, lines: live, subtotal, discount, tax, total, count, pendingCount,
|
||||||
onBump: (id: string, d: number) => { const l = items.find((x) => x.menuItem.id === id); if (l) updateQty(id, l.quantity + d); },
|
onBump: (id: string, d: number) => { const l = items.find((x) => x.menuItem.id === id); if (l) updateQty(id, l.quantity + d); },
|
||||||
onRemove: removeItem, onSend: send, onPay: openPay, onSplit: openPay,
|
onRemove: removeItem, onSend: send, onPay: openPay, onSplit: openPay,
|
||||||
|
onNote: (id: string, notes: string) => setNotes(id, notes),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── TABLE BOARD ────────────────────────────────────────────────────────────
|
// ── TABLE BOARD ────────────────────────────────────────────────────────────
|
||||||
@@ -669,15 +671,17 @@ function Pos2Extras({ cafeId }: { cafeId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Order ticket ─────────────────────────────────────────────────────────────
|
// ── Order ticket ─────────────────────────────────────────────────────────────
|
||||||
type TicketLine = { menuItem: MenuItem; quantity: number };
|
type TicketLine = { menuItem: MenuItem; quantity: number; notes?: string };
|
||||||
function Ticket({
|
function Ticket({
|
||||||
cafeId, lines, subtotal, discount, tax, total, count, pendingCount, onBump, onRemove, onSend, onPay, onSplit,
|
cafeId, lines, subtotal, discount, tax, total, count, pendingCount, onBump, onRemove, onNote, onSend, onPay, onSplit,
|
||||||
}: {
|
}: {
|
||||||
cafeId: string; lines: TicketLine[]; subtotal: number; discount: number; tax: number; total: number;
|
cafeId: string; lines: TicketLine[]; subtotal: number; discount: number; tax: number; total: number;
|
||||||
count: number; pendingCount: number;
|
count: number; pendingCount: number;
|
||||||
onBump: (id: string, d: number) => void; onRemove: (id: string) => void;
|
onBump: (id: string, d: number) => void; onRemove: (id: string) => void;
|
||||||
|
onNote: (id: string, notes: string) => void;
|
||||||
onSend: () => void; onPay: () => void; onSplit: () => void;
|
onSend: () => void; onPay: () => void; onSplit: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [noteFor, setNoteFor] = useState<string | null>(null);
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
||||||
@@ -690,23 +694,45 @@ function Ticket({
|
|||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{lines.map((l) => (
|
{lines.map((l) => (
|
||||||
<li key={l.menuItem.id} className="flex items-center gap-2 rounded-xl border border-border/70 p-2">
|
<li key={l.menuItem.id} className="rounded-xl border border-border/70 p-2">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="flex items-center gap-2">
|
||||||
<p className="line-clamp-1 font-medium">{l.menuItem.name}</p>
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs text-muted-foreground">{fmt(l.menuItem.price)} تومان</p>
|
<p className="line-clamp-1 font-medium">{l.menuItem.name}</p>
|
||||||
</div>
|
<p className="text-xs text-muted-foreground">{fmt(l.menuItem.price)} تومان</p>
|
||||||
<div className="flex items-center gap-1">
|
</div>
|
||||||
<button type="button" onClick={() => onBump(l.menuItem.id, -1)} className="flex size-11 items-center justify-center rounded-lg bg-muted hover:bg-accent active:scale-95" aria-label="کم">
|
<div className="flex items-center gap-1">
|
||||||
<Minus className="size-4" />
|
<button type="button" onClick={() => onBump(l.menuItem.id, -1)} className="flex size-11 items-center justify-center rounded-lg bg-muted hover:bg-accent active:scale-95" aria-label="کم">
|
||||||
|
<Minus className="size-4" />
|
||||||
|
</button>
|
||||||
|
<span className="w-7 text-center font-bold">{fmt(l.quantity)}</span>
|
||||||
|
<button type="button" onClick={() => onBump(l.menuItem.id, 1)} className="flex size-11 items-center justify-center rounded-lg bg-primary/10 text-primary hover:bg-primary/20 active:scale-95" aria-label="زیاد">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNoteFor((cur) => (cur === l.menuItem.id ? null : l.menuItem.id))}
|
||||||
|
className={`flex size-9 items-center justify-center rounded-lg hover:bg-accent active:scale-95 ${l.notes ? "text-primary" : "text-muted-foreground"}`}
|
||||||
|
aria-label="یادداشت"
|
||||||
|
title="یادداشت آیتم"
|
||||||
|
>
|
||||||
|
<StickyNote className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
<span className="w-7 text-center font-bold">{fmt(l.quantity)}</span>
|
<button type="button" onClick={() => onRemove(l.menuItem.id)} className="flex size-9 items-center justify-center rounded-lg text-red-500 hover:bg-red-50" aria-label="حذف">
|
||||||
<button type="button" onClick={() => onBump(l.menuItem.id, 1)} className="flex size-11 items-center justify-center rounded-lg bg-primary/10 text-primary hover:bg-primary/20 active:scale-95" aria-label="زیاد">
|
<Trash2 className="size-4" />
|
||||||
<Plus className="size-4" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={() => onRemove(l.menuItem.id)} className="flex size-9 items-center justify-center rounded-lg text-red-500 hover:bg-red-50" aria-label="حذف">
|
{noteFor === l.menuItem.id || l.notes ? (
|
||||||
<Trash2 className="size-4" />
|
<input
|
||||||
</button>
|
value={l.notes ?? ""}
|
||||||
|
onChange={(e) => onNote(l.menuItem.id, e.target.value)}
|
||||||
|
placeholder="یادداشت برای آشپزخانه (مثلاً بدون شکر)"
|
||||||
|
autoFocus={noteFor === l.menuItem.id}
|
||||||
|
maxLength={200}
|
||||||
|
dir="rtl"
|
||||||
|
className="mt-2 w-full rounded-lg border border-border/70 bg-background px-2.5 py-1.5 text-sm outline-none focus:border-primary"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user