diff --git a/web/dashboard/src/components/pos/pos-receipt-print.css b/web/dashboard/src/components/pos/pos-receipt-print.css index 0e8a2e6..f7c0e70 100644 --- a/web/dashboard/src/components/pos/pos-receipt-print.css +++ b/web/dashboard/src/components/pos/pos-receipt-print.css @@ -1,30 +1,62 @@ +/* + * pos-receipt-print.css + * + * Fallback @media print styles for the slip preview panel. + * The real thermal print job goes through thermal-print.ts (iframe) — + * these rules only fire if window.print() is called on the main page directly. + */ + @media print { - body * { - visibility: hidden; - } - #pos-slip-print-area, - #pos-slip-print-area *, - #receipt-print-area, - #receipt-print-area * { - visibility: visible; - } - #pos-slip-print-area, - #receipt-print-area { - position: absolute; - inset: 0; + /* 80 mm roll — height tracks the content, zero blank tail */ + @page { + size: 80mm auto; margin: 0; - padding: 0; + } + + /* Force RTL and thermal width on the document root */ + html { + direction: rtl !important; + width: 80mm !important; + } + + /* Hide everything except the slip */ + body > * { + display: none !important; + } + + /* The modal overlay needs to be block but transparent */ + body > *:has(#pos-slip-print-area) { + display: block !important; + position: static !important; + background: transparent !important; + } + + #pos-slip-print-area, + #pos-slip-print-area * { + visibility: visible !important; + } + + #pos-slip-print-area { + display: block !important; + position: static !important; + width: 80mm !important; + margin: 0 !important; + padding: 3mm 4mm !important; + border: none !important; + border-radius: 0 !important; + box-shadow: none !important; } } -#pos-slip-print-area, -#receipt-print-area { - width: 80mm; - font-family: "Courier New", monospace; +/* ── Screen preview styles ───────────────────────────────────────────────── */ + +#pos-slip-print-area { + width: 100%; + max-width: 76mm; + font-family: "Tahoma", "Vazirmatn", "B Nazanin", "Courier New", monospace; font-size: 12px; direction: rtl; text-align: right; - padding: 4mm; } .receipt-divider { diff --git a/web/dashboard/src/components/pos/pos-slip-modal.tsx b/web/dashboard/src/components/pos/pos-slip-modal.tsx index 0339c90..17e465f 100644 --- a/web/dashboard/src/components/pos/pos-slip-modal.tsx +++ b/web/dashboard/src/components/pos/pos-slip-modal.tsx @@ -1,9 +1,11 @@ "use client"; import { useTranslations, useLocale } from "next-intl"; +import { Printer } from "lucide-react"; import type { Order } from "@/lib/api/types"; import { formatCurrency } from "@/lib/format"; import { formatOrderNumber } from "@/lib/order-number"; +import { buildThermalDocument, printThermal } from "@/lib/thermal-print"; import { Button } from "@/components/ui/button"; import "./pos-receipt-print.css"; @@ -48,27 +50,80 @@ export function PosSlipModal({ { dateStyle: "short", timeStyle: "short" } ).format(new Date(dateSource)); - const table = - order?.tableNumber ?? tableNumber ?? "—"; - const orderNo = order ? formatOrderNumber(order) : orderId ? formatOrderNumber({ id: orderId }) : null; + const table = order?.tableNumber ?? tableNumber ?? "—"; + const orderNo = order + ? formatOrderNumber(order) + : orderId + ? formatOrderNumber({ id: orderId }) + : null; const guest = order?.guestName ?? guestName; - const printId = "pos-slip-print-area"; + + const activeBillItems = order?.items.filter((i) => !i.isVoided) ?? []; const paymentKey = (method: string) => { const m = method.toLowerCase(); - if (m === "cash") return t("payment.cash"); - if (m === "card") return t("payment.card"); + if (m === "cash") return t("payment.cash"); + if (m === "card") return t("payment.card"); if (m === "credit") return t("payment.credit"); return method; }; - const activeBillItems = order?.items.filter((i) => !i.isVoided) ?? []; + // ── Build meta row ───────────────────────────────────────────────────────── + const metaParts: string[] = []; + metaParts.push(`${t("table")}: ${table}`); + if (orderNo) metaParts.push(`${t("order")}: #${orderNo}`); + if (guest) metaParts.push(`${t("guest")}: ${guest}`); + const metaRow = metaParts.join(" | "); + // ── Print handler ───────────────────────────────────────────────────────── + const handlePrint = () => { + const slipData = + variant === "kitchen" + ? { + cafeName, + title: t("kitchenTitle"), + date: formattedDate, + metaRow, + lines: kitchenLines.map((l) => ({ + name: l.name, + quantity: l.quantity, + notes: l.notes, + })), + footer: t("kitchenFooter"), + locale, + } + : { + cafeName, + title: t("billTitle"), + date: formattedDate, + metaRow, + lines: activeBillItems.map((item) => ({ + name: item.menuItemName, + quantity: item.quantity, + price: formatCurrency(item.unitPrice * item.quantity, numberLocale), + })), + totals: { + total: formatCurrency(order!.total, numberLocale), + payments: order!.payments?.map((p) => ({ + method: paymentKey(p.method), + amount: formatCurrency(p.amount, numberLocale), + })), + }, + footer: t("thankYou"), + locale, + }; + + printThermal(buildThermalDocument(slipData)); + }; + + // ── Render ───────────────────────────────────────────────────────────────── return (
+ + {/* ── Print preview ──────────────────────────────────────────────── */}
{cafeName}
@@ -78,20 +133,7 @@ export function PosSlipModal({
{formattedDate}
-
- {t("table")}: {table} - {orderNo ? ( - <> - {" "} - | {t("order")}: #{orderNo} - - ) : null} -
- {guest ? ( -
- {t("guest")}: {guest} -
- ) : null} +
{metaRow}
@@ -115,7 +157,7 @@ export function PosSlipModal({
))} - {variant === "bill" ? ( + {variant === "bill" && ( <>
@@ -131,15 +173,19 @@ export function PosSlipModal({
{t("thankYou")}
- ) : ( + )} + + {variant === "kitchen" && (
{t("kitchenFooter")}
)}
+ {/* ── Actions ────────────────────────────────────────────────────── */}
-