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);
+ }
}