diff --git a/web/dashboard/public/fonts/Vazirmatn-Variable.woff2 b/web/dashboard/public/fonts/Vazirmatn-Variable.woff2 new file mode 100644 index 0000000..a501289 Binary files /dev/null and b/web/dashboard/public/fonts/Vazirmatn-Variable.woff2 differ diff --git a/web/dashboard/src/components/pos/pos-pay-panel.tsx b/web/dashboard/src/components/pos/pos-pay-panel.tsx index b4f2b59..4254b62 100644 --- a/web/dashboard/src/components/pos/pos-pay-panel.tsx +++ b/web/dashboard/src/components/pos/pos-pay-panel.tsx @@ -624,6 +624,12 @@ export function PosPayPanel({ cafeId, numberLocale, branchId = null }: PosPayPan variant="bill" order={receiptOrder} cafeName={cafeName} + logoUrl={cafeSettings?.logoUrl} + tagline={ + [cafeSettings?.address, cafeSettings?.phone] + .filter(Boolean) + .join(" • ") || undefined + } onClose={() => setReceiptOrder(null)} /> ) : null} diff --git a/web/dashboard/src/components/pos/pos-receipt-print.css b/web/dashboard/src/components/pos/pos-receipt-print.css index f7c0e70..f71458e 100644 --- a/web/dashboard/src/components/pos/pos-receipt-print.css +++ b/web/dashboard/src/components/pos/pos-receipt-print.css @@ -53,7 +53,7 @@ #pos-slip-print-area { width: 100%; max-width: 76mm; - font-family: "Tahoma", "Vazirmatn", "B Nazanin", "Courier New", monospace; + font-family: "Vazirmatn", "Tahoma", "Arial", sans-serif; font-size: 12px; direction: rtl; text-align: right; diff --git a/web/dashboard/src/components/pos/pos-slip-modal.tsx b/web/dashboard/src/components/pos/pos-slip-modal.tsx index 17e465f..259fb67 100644 --- a/web/dashboard/src/components/pos/pos-slip-modal.tsx +++ b/web/dashboard/src/components/pos/pos-slip-modal.tsx @@ -6,6 +6,7 @@ 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 { resolveMediaUrl } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import "./pos-receipt-print.css"; @@ -18,6 +19,10 @@ export type KitchenSlipLine = { type PosSlipModalProps = { variant: "kitchen" | "bill"; cafeName: string; + /** Café logo for receipt branding. */ + logoUrl?: string; + /** Address / phone line shown under the café name on the bill. */ + tagline?: string; onClose: () => void; /** Full order for customer bill */ order?: Order; @@ -32,6 +37,8 @@ type PosSlipModalProps = { export function PosSlipModal({ variant, cafeName, + logoUrl, + tagline, onClose, order, kitchenLines = [], @@ -94,6 +101,8 @@ export function PosSlipModal({ } : { cafeName, + logoUrl: resolveMediaUrl(logoUrl), + tagline, title: t("billTitle"), date: formattedDate, metaRow, @@ -126,8 +135,19 @@ export function PosSlipModal({ id="pos-slip-print-area" className="mb-4 rounded-md border border-dashed border-border p-3" > -
{cafeName}
-
+ {variant === "bill" && logoUrl && ( + // eslint-disable-next-line @next/next/no-img-element + + )} +
{cafeName}
+ {variant === "bill" && tagline && ( +
{tagline}
+ )} +
{variant === "kitchen" ? t("kitchenTitle") : t("billTitle")}
diff --git a/web/dashboard/src/lib/thermal-print.ts b/web/dashboard/src/lib/thermal-print.ts index 459de1d..63c0821 100644 --- a/web/dashboard/src/lib/thermal-print.ts +++ b/web/dashboard/src/lib/thermal-print.ts @@ -40,8 +40,19 @@ export type ThermalSlipData = { footer?: string; /** 'fa' (default) → RTL, 'en' → LTR */ locale?: string; + /** Optional café logo URL — rendered centered above the name for branding. */ + logoUrl?: string; + /** Optional tagline / address line under the café name. */ + tagline?: string; }; +/** Absolute URL to the bundled Vazirmatn web-font (same origin). */ +function vazirmatnFontUrl(): string { + const origin = + typeof window !== "undefined" ? window.location.origin : ""; + return `${origin}/fonts/Vazirmatn-Variable.woff2`; +} + // ───────────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────────── @@ -97,11 +108,28 @@ export function buildThermalDocument(data: ThermalSlipData): string { ? `
${esc(data.footer)}
` : ""; + const logoHtml = data.logoUrl + ? `` + : ""; + const taglineHtml = data.tagline + ? `
${esc(data.tagline)}
` + : ""; + + const fontUrl = vazirmatnFontUrl(); + return `
-
${esc(data.cafeName)}
-
${esc(data.title)}
-
${esc(data.date)}
- ${data.metaRow ? `
${esc(data.metaRow)}
` : ""} + ${logoHtml} +
${esc(data.cafeName)}
+ ${taglineHtml} +
${esc(data.title)}
+
${esc(data.date)}
+ ${data.metaRow ? `
${esc(data.metaRow)}
` : ""}
${linesHtml} ${totalsHtml} @@ -199,8 +281,7 @@ export function printThermal(html: string): void { doc.write(html); doc.close(); - // rAF gives the iframe one layout pass before we trigger the print dialog - requestAnimationFrame(() => { + const fire = () => { try { frame!.contentWindow?.focus(); frame!.contentWindow?.print(); @@ -208,5 +289,25 @@ export function printThermal(html: string): void { // Safari throws if the frame was cross-origin — shouldn't happen for // same-origin same-page iframes, but guard just in case. } - }); + }; + + // Wait for the embedded Vazirmatn web-font to finish loading inside the + // iframe before printing — otherwise the spooler may rasterise the receipt + // with a fallback font. `document.fonts.ready` resolves once @font-face + // loads complete; we cap the wait so a font failure never blocks printing. + const win = frame.contentWindow; + const fontSet = win?.document?.fonts; + if (fontSet) { + let printed = false; + const run = () => { + if (printed) return; + printed = true; + requestAnimationFrame(fire); + }; + fontSet.ready.then(run).catch(run); + // Safety timeout in case fonts.ready never settles. + setTimeout(run, 1500); + } else { + requestAnimationFrame(fire); + } }