From 5078af2dd714679e08083766c4dea43afe7c49a9 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Wed, 3 Jun 2026 17:23:58 +0330 Subject: [PATCH] feat(pos): clickable POS v2 redesign prototype at /pos2 (static, no backend) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Responsive RTL big-touch reimagining of the POS order screen for judging the redesign on real devices before decomposing the 1568-line pos-screen.tsx + wiring real logic. Self-contained: mock menu + local cart, no API/store/SignalR. - 3 zones: category chips + item grid · order ticket · sticky action bar. - lg+ side-panel ticket; smaller screens get a "view order" bar + slide-over (covers landscape tablet, portrait tablet, phone). - Big-touch (56px primary / 44px qty), brand green #0F6E56, Toman totals. Send/Pay are mock toasts. tsc clean. --- .../app/[locale]/(fullscreen)/pos2/page.tsx | 7 + .../src/components/pos2/pos2-prototype.tsx | 303 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 web/dashboard/src/app/[locale]/(fullscreen)/pos2/page.tsx create mode 100644 web/dashboard/src/components/pos2/pos2-prototype.tsx diff --git a/web/dashboard/src/app/[locale]/(fullscreen)/pos2/page.tsx b/web/dashboard/src/app/[locale]/(fullscreen)/pos2/page.tsx new file mode 100644 index 0000000..6968232 --- /dev/null +++ b/web/dashboard/src/app/[locale]/(fullscreen)/pos2/page.tsx @@ -0,0 +1,7 @@ +import { Pos2Prototype } from "@/components/pos2/pos2-prototype"; + +/** POS v2 — clickable static prototype (mock data, no backend). Open at //pos2 + * on a tablet/phone to judge the redesigned layout before we decompose + wire it. */ +export default function Pos2PrototypePage() { + return ; +} diff --git a/web/dashboard/src/components/pos2/pos2-prototype.tsx b/web/dashboard/src/components/pos2/pos2-prototype.tsx new file mode 100644 index 0000000..3d9317b --- /dev/null +++ b/web/dashboard/src/components/pos2/pos2-prototype.tsx @@ -0,0 +1,303 @@ +"use client"; + +// ───────────────────────────────────────────────────────────────────────────── +// POS v2 — clickable STATIC prototype (mock data, local state only). +// Zero coupling to the live POS: no API, no stores, no SignalR. Purpose: judge the +// layout/feel on real devices before we decompose + wire. Route: /[locale]/pos2 +// ───────────────────────────────────────────────────────────────────────────── + +import { useMemo, useState } from "react"; +import { + Search, Plus, Minus, Trash2, Send, CreditCard, Pause, SplitSquareHorizontal, + X, WifiOff, ShoppingCart, Users, Coffee, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { notify } from "@/lib/notify"; + +type Item = { id: string; name: string; price: number; cat: string }; + +const CATEGORIES = [ + { id: "all", name: "همه" }, + { id: "coffee", name: "قهوه" }, + { id: "tea", name: "دمنوش" }, + { id: "cold", name: "نوشیدنی سرد" }, + { id: "food", name: "غذا" }, + { id: "dessert", name: "دسر" }, +]; + +const ITEMS: Item[] = [ + { id: "1", name: "اسپرسو", price: 55000, cat: "coffee" }, + { id: "2", name: "آمریکانو", price: 65000, cat: "coffee" }, + { id: "3", name: "کاپوچینو", price: 85000, cat: "coffee" }, + { id: "4", name: "لاته", price: 92000, cat: "coffee" }, + { id: "5", name: "موکا", price: 98000, cat: "coffee" }, + { id: "6", name: "چای ماسالا", price: 72000, cat: "tea" }, + { id: "7", name: "دمنوش به‌لیمو", price: 68000, cat: "tea" }, + { id: "8", name: "آیس لاته", price: 105000, cat: "cold" }, + { id: "9", name: "فراپه کارامل", price: 135000, cat: "cold" }, + { id: "10", name: "لیموناد", price: 88000, cat: "cold" }, + { id: "11", name: "سالاد سزار", price: 185000, cat: "food" }, + { id: "12", name: "ساندویچ مرغ", price: 165000, cat: "food" }, + { id: "13", name: "پاستا آلفردو", price: 220000, cat: "food" }, + { id: "14", name: "چیزکیک", price: 145000, cat: "dessert" }, + { id: "15", name: "براونی", price: 120000, cat: "dessert" }, + { id: "16", name: "تیرامیسو", price: 155000, cat: "dessert" }, +]; + +type Line = { item: Item; qty: number }; +const fmt = (n: number) => n.toLocaleString("fa-IR"); +const TAX = 0.09; + +export function Pos2Prototype() { + const [cat, setCat] = useState("all"); + const [q, setQ] = useState(""); + const [lines, setLines] = useState([]); + const [cartOpen, setCartOpen] = useState(false); // mobile/portrait slide-over + + const items = useMemo( + () => ITEMS.filter((i) => (cat === "all" || i.cat === cat) && (q === "" || i.name.includes(q))), + [cat, q], + ); + const count = lines.reduce((s, l) => s + l.qty, 0); + const subtotal = lines.reduce((s, l) => s + l.qty * l.item.price, 0); + const tax = Math.round(subtotal * TAX); + const total = subtotal + tax; + + const add = (it: Item) => + setLines((ls) => { + const e = ls.find((l) => l.item.id === it.id); + return e + ? ls.map((l) => (l.item.id === it.id ? { ...l, qty: l.qty + 1 } : l)) + : [...ls, { item: it, qty: 1 }]; + }); + const bump = (id: string, d: number) => + setLines((ls) => ls.flatMap((l) => (l.item.id === id ? (l.qty + d <= 0 ? [] : [{ ...l, qty: l.qty + d }]) : [l]))); + const remove = (id: string) => setLines((ls) => ls.filter((l) => l.item.id !== id)); + + const send = () => { + if (!count) return; + notify.success("سفارش به آشپزخانه ارسال شد (نمونه)"); + setLines([]); + setCartOpen(false); + }; + const pay = () => { + if (!count) return; + notify.success(`پرداخت ${fmt(total)} تومان (نمونه)`); + setLines([]); + setCartOpen(false); + }; + + return ( +
+ {/* ── Topbar ───────────────────────────────────────────── */} +
+
+ + میز ۵ +
+ + ۲ نفر + +
+ + setQ(e.target.value)} + placeholder="جستجوی آیتم…" + className="h-11 w-full rounded-xl border border-border bg-background pe-10 ps-4 text-base outline-none focus:ring-2 focus:ring-primary/40" + /> +
+ + آفلاین + +
+ + {/* ── Body: menu zone + ticket (lg side panel) ─────────── */} +
+ {/* MENU ZONE */} +
+ {/* category chips — horizontal scroll, works all sizes */} +
+ {CATEGORIES.map((c) => ( + + ))} +
+ {/* item grid */} +
+ {items.map((it) => ( + + ))} + {items.length === 0 && ( +

آیتمی یافت نشد

+ )} +
+
+ + {/* ORDER TICKET — side panel on lg+, hidden below */} + +
+ + {/* ── Mobile/portrait: sticky "view order" bar ─────────── */} + {count > 0 && ( + + )} + + {/* ── Mobile/portrait: slide-over ticket ───────────────── */} + {cartOpen && ( +
+
setCartOpen(false)} /> +
+
+ سفارش میز ۵ + +
+ +
+
+ )} +
+ ); +} + +function Ticket({ + lines, subtotal, tax, total, count, onBump, onRemove, onSend, onPay, +}: { + lines: Line[]; subtotal: number; tax: number; total: number; count: number; + onBump: (id: string, d: number) => void; onRemove: (id: string) => void; + onSend: () => void; onPay: () => void; +}) { + return ( +
+ {/* line items */} +
+ {lines.length === 0 ? ( +
+ +

سبد خالی است

+

برای افزودن، روی آیتم‌ها بزنید

+
+ ) : ( +
    + {lines.map((l) => ( +
  • +
    +

    {l.item.name}

    +

    {fmt(l.item.price)} تومان

    +
    +
    + + {fmt(l.qty)} + +
    + +
  • + ))} +
+ )} +
+ + {/* totals */} +
+ + +
+ مبلغ کل + {fmt(total)} تومان +
+
+ + {/* action bar */} +
+ + + + +
+
+ ); +} + +function Row({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +}