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

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:
soroush.asadi
2026-06-21 14:05:00 +03:30
parent 2a24798a59
commit 0c2ded4070
@@ -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,7 +694,8 @@ 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="flex items-center gap-2">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="line-clamp-1 font-medium">{l.menuItem.name}</p> <p className="line-clamp-1 font-medium">{l.menuItem.name}</p>
<p className="text-xs text-muted-foreground">{fmt(l.menuItem.price)} تومان</p> <p className="text-xs text-muted-foreground">{fmt(l.menuItem.price)} تومان</p>
@@ -704,9 +709,30 @@ function Ticket({
<Plus className="size-4" /> <Plus className="size-4" />
</button> </button>
</div> </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 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={() => onRemove(l.menuItem.id)} className="flex size-9 items-center justify-center rounded-lg text-red-500 hover:bg-red-50" aria-label="حذف">
<Trash2 className="size-4" /> <Trash2 className="size-4" />
</button> </button>
</div>
{noteFor === l.menuItem.id || l.notes ? (
<input
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>