bugfix : remove orphan

This commit is contained in:
soroush.asadi
2026-05-30 09:42:32 +03:30
parent 2850ed8ed7
commit 51e422272d
4 changed files with 87 additions and 11 deletions
-1
View File
@@ -314,7 +314,6 @@ jobs:
- name: Start main services - name: Start main services
run: | run: |
docker compose up -d \ docker compose up -d \
--remove-orphans \
--no-deps \ --no-deps \
postgres redis api web website koja postgres redis api web website koja
@@ -27,6 +27,13 @@ type PaymentRow = {
amount: string; amount: string;
}; };
type BranchPrintSettings = {
receiptHeader?: string | null;
receiptFooter?: string | null;
wifiPassword?: string | null;
paperWidthMm?: number;
};
type PosPayPanelProps = { type PosPayPanelProps = {
cafeId: string; cafeId: string;
numberLocale: string; numberLocale: string;
@@ -48,6 +55,7 @@ export function PosPayPanel({ cafeId, numberLocale, branchId = null }: PosPayPan
const [debouncedSearch, setDebouncedSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState("");
const [payMessage, setPayMessage] = useState<string | null>(null); const [payMessage, setPayMessage] = useState<string | null>(null);
const [receiptOrder, setReceiptOrder] = useState<Order | null>(null); const [receiptOrder, setReceiptOrder] = useState<Order | null>(null);
const printSettingsBranchId = receiptOrder?.branchId ?? branchId ?? null;
const [lastPaidOrderId, setLastPaidOrderId] = useState<string | null>(null); const [lastPaidOrderId, setLastPaidOrderId] = useState<string | null>(null);
const [paymentRows, setPaymentRows] = useState<PaymentRow[]>([ const [paymentRows, setPaymentRows] = useState<PaymentRow[]>([
{ method: "Cash", amount: "" }, { method: "Cash", amount: "" },
@@ -79,6 +87,16 @@ export function PosPayPanel({ cafeId, numberLocale, branchId = null }: PosPayPan
enabled: !!cafeId, enabled: !!cafeId,
}); });
const { data: printSettings } = useQuery({
queryKey: ["branch-print-settings", cafeId, printSettingsBranchId],
queryFn: () =>
apiGet<BranchPrintSettings>(
`/api/cafes/${cafeId}/branches/${printSettingsBranchId}/print-settings`
),
enabled: !!cafeId && !!printSettingsBranchId,
staleTime: 5 * 60 * 1000,
});
const displayedOrders = useMemo(() => { const displayedOrders = useMemo(() => {
if (!filterTableId) return openOrders; if (!filterTableId) return openOrders;
return openOrders.filter((o) => o.tableId === filterTableId); return openOrders.filter((o) => o.tableId === filterTableId);
@@ -630,6 +648,10 @@ export function PosPayPanel({ cafeId, numberLocale, branchId = null }: PosPayPan
.filter(Boolean) .filter(Boolean)
.join(" • ") || undefined .join(" • ") || undefined
} }
receiptHeader={printSettings?.receiptHeader}
receiptFooter={printSettings?.receiptFooter}
wifiPassword={printSettings?.wifiPassword}
paperWidthMm={printSettings?.paperWidthMm}
onClose={() => setReceiptOrder(null)} onClose={() => setReceiptOrder(null)}
/> />
) : null} ) : null}
@@ -23,6 +23,14 @@ type PosSlipModalProps = {
logoUrl?: string; logoUrl?: string;
/** Address / phone line shown under the café name on the bill. */ /** Address / phone line shown under the café name on the bill. */
tagline?: string; tagline?: string;
/** Custom header note from branch print settings (bill only). */
receiptHeader?: string | null;
/** Custom footer note from branch print settings (bill only). */
receiptFooter?: string | null;
/** WiFi password printed near the bill footer. */
wifiPassword?: string | null;
/** Paper width in mm — 58 or 80 (default 80). */
paperWidthMm?: number;
onClose: () => void; onClose: () => void;
/** Full order for customer bill */ /** Full order for customer bill */
order?: Order; order?: Order;
@@ -39,6 +47,10 @@ export function PosSlipModal({
cafeName, cafeName,
logoUrl, logoUrl,
tagline, tagline,
receiptHeader,
receiptFooter,
wifiPassword,
paperWidthMm,
onClose, onClose,
order, order,
kitchenLines = [], kitchenLines = [],
@@ -103,6 +115,9 @@ export function PosSlipModal({
cafeName, cafeName,
logoUrl: resolveMediaUrl(logoUrl), logoUrl: resolveMediaUrl(logoUrl),
tagline, tagline,
header: receiptHeader?.trim() || undefined,
wifi: wifiPassword?.trim() || undefined,
paperWidthMm,
title: t("billTitle"), title: t("billTitle"),
date: formattedDate, date: formattedDate,
metaRow, metaRow,
@@ -118,7 +133,7 @@ export function PosSlipModal({
amount: formatCurrency(p.amount, numberLocale), amount: formatCurrency(p.amount, numberLocale),
})), })),
}, },
footer: t("thankYou"), footer: receiptFooter?.trim() || t("thankYou"),
locale, locale,
}; };
@@ -147,6 +162,11 @@ export function PosSlipModal({
{variant === "bill" && tagline && ( {variant === "bill" && tagline && (
<div className="text-center text-[10px] text-muted-foreground">{tagline}</div> <div className="text-center text-[10px] text-muted-foreground">{tagline}</div>
)} )}
{variant === "bill" && receiptHeader?.trim() && (
<div className="whitespace-pre-line text-center text-[11px] font-medium text-foreground/80">
{receiptHeader.trim()}
</div>
)}
<div className="mb-1 mt-1.5 border-y border-foreground/60 py-0.5 text-center text-xs font-bold"> <div className="mb-1 mt-1.5 border-y border-foreground/60 py-0.5 text-center text-xs font-bold">
{variant === "kitchen" ? t("kitchenTitle") : t("billTitle")} {variant === "kitchen" ? t("kitchenTitle") : t("billTitle")}
</div> </div>
@@ -191,7 +211,14 @@ export function PosSlipModal({
</div> </div>
))} ))}
<div className="receipt-divider" /> <div className="receipt-divider" />
<div className="mt-2 text-center text-xs">{t("thankYou")}</div> {wifiPassword?.trim() && (
<div className="text-center text-[11px]" dir="ltr">
WiFi: {wifiPassword.trim()}
</div>
)}
<div className="mt-2 text-center text-xs">
{receiptFooter?.trim() || t("thankYou")}
</div>
</> </>
)} )}
+36 -8
View File
@@ -44,6 +44,12 @@ export type ThermalSlipData = {
logoUrl?: string; logoUrl?: string;
/** Optional tagline / address line under the café name. */ /** Optional tagline / address line under the café name. */
tagline?: string; tagline?: string;
/** Optional custom header note (from branch print settings) shown under the name. */
header?: string;
/** Optional WiFi password line printed near the footer. */
wifi?: string;
/** Paper width in mm — 58 or 80 (default 80). Controls page width + scale. */
paperWidthMm?: number;
}; };
/** Absolute URL to the bundled Vazirmatn web-font (same origin). */ /** Absolute URL to the bundled Vazirmatn web-font (same origin). */
@@ -104,9 +110,14 @@ export function buildThermalDocument(data: ThermalSlipData): string {
`; `;
} }
const footerHtml = data.footer const wifiLabel = isRtl ? "وای‌فای" : "WiFi";
? `<hr class="dashed"><div class="center sm muted">${esc(data.footer)}</div>` const footerInner = [
: ""; data.wifi ? `<div class="center sm">${wifiLabel}: ${esc(data.wifi)}</div>` : "",
data.footer ? `<div class="center sm muted">${esc(data.footer)}</div>` : "",
]
.filter(Boolean)
.join("");
const footerHtml = footerInner ? `<hr class="dashed">${footerInner}` : "";
const logoHtml = data.logoUrl const logoHtml = data.logoUrl
? `<img class="logo" src="${esc(data.logoUrl)}" alt="">` ? `<img class="logo" src="${esc(data.logoUrl)}" alt="">`
@@ -114,6 +125,15 @@ export function buildThermalDocument(data: ThermalSlipData): string {
const taglineHtml = data.tagline const taglineHtml = data.tagline
? `<div class="center tagline">${esc(data.tagline)}</div>` ? `<div class="center tagline">${esc(data.tagline)}</div>`
: ""; : "";
const headerHtml = data.header
? `<div class="center header-note">${esc(data.header)}</div>`
: "";
// Paper width: 58 mm or 80 mm. Narrower paper gets a slightly smaller base
// font so lines don't wrap awkwardly.
const widthMm = data.paperWidthMm === 58 ? 58 : 80;
const baseFontPt = widthMm === 58 ? 10 : 11.5;
const wrapPadMm = widthMm === 58 ? "3mm 3mm 5mm" : "4mm 4.5mm 6mm";
const fontUrl = vazirmatnFontUrl(); const fontUrl = vazirmatnFontUrl();
@@ -132,18 +152,18 @@ export function buildThermalDocument(data: ThermalSlipData): string {
} }
/* ── Page ─────────────────────────────────────────────────── */ /* ── Page ─────────────────────────────────────────────────── */
@page { @page {
/* 80 mm wide; height tracks the content — no blank tail */ /* width tracks the configured paper; height tracks content — no blank tail */
size: 80mm auto; size: ${widthMm}mm auto;
margin: 0; margin: 0;
} }
/* ── Reset ────────────────────────────────────────────────── */ /* ── Reset ────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ── Root ─────────────────────────────────────────────────── */ /* ── Root ─────────────────────────────────────────────────── */
html, body { html, body {
width: 80mm; width: ${widthMm}mm;
direction: ${dir}; direction: ${dir};
font-family: 'Vazirmatn', 'Tahoma', 'Arial', sans-serif; font-family: 'Vazirmatn', 'Tahoma', 'Arial', sans-serif;
font-size: 11.5pt; font-size: ${baseFontPt}pt;
font-weight: 500; font-weight: 500;
line-height: 1.55; line-height: 1.55;
color: #000; color: #000;
@@ -152,7 +172,7 @@ html, body {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
/* ── Wrapper ──────────────────────────────────────────────── */ /* ── Wrapper ──────────────────────────────────────────────── */
.wrap { padding: 4mm 4.5mm 6mm; } .wrap { padding: ${wrapPadMm}; }
/* ── Branding header ──────────────────────────────────────── */ /* ── Branding header ──────────────────────────────────────── */
.logo { .logo {
display: block; display: block;
@@ -174,6 +194,13 @@ html, body {
color: #444; color: #444;
margin-top: 0.5mm; margin-top: 0.5mm;
} }
.header-note {
font-size: 9pt;
font-weight: 600;
color: #222;
margin-top: 1mm;
white-space: pre-line;
}
.doc-title { .doc-title {
text-align: center; text-align: center;
font-size: 10pt; font-size: 10pt;
@@ -233,6 +260,7 @@ hr.solid { border: none; border-top: 1px solid #000; margin: 2.5mm 0; }
${logoHtml} ${logoHtml}
<div class="cafe-name">${esc(data.cafeName)}</div> <div class="cafe-name">${esc(data.cafeName)}</div>
${taglineHtml} ${taglineHtml}
${headerHtml}
<div class="doc-title">${esc(data.title)}</div> <div class="doc-title">${esc(data.title)}</div>
<div class="date">${esc(data.date)}</div> <div class="date">${esc(data.date)}</div>
${data.metaRow ? `<div class="meta">${esc(data.metaRow)}</div>` : ""} ${data.metaRow ? `<div class="meta">${esc(data.metaRow)}</div>` : ""}