feat(pos): print customer receipt from the POS page
CI/CD / CI · API (dotnet build + test) (push) Successful in 44s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m12s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 48s
CI/CD / CI · Koja (tsc) (push) Successful in 51s
CI/CD / Deploy · all services (push) Successful in 3m0s
CI/CD / CI · API (dotnet build + test) (push) Successful in 44s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m12s
CI/CD / CI · Admin Web (tsc) (push) Successful in 39s
CI/CD / CI · Website (tsc) (push) Successful in 48s
CI/CD / CI · Koja (tsc) (push) Successful in 51s
CI/CD / Deploy · all services (push) Successful in 3m0s
POS v2 auto-printed the receipt on full payment but had no manual button. Adds a "چاپ فاکتور" (print receipt) action in the order panel that prints/reprints the active saved order's customer receipt, plus a "print receipt" action on the payment-success toast. Replaces the dead disabled "hold" placeholder button. Backend print endpoint unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Search, Plus, Minus, Trash2, Send, CreditCard, Pause, SplitSquareHorizontal,
|
Search, Plus, Minus, Trash2, Send, CreditCard, SplitSquareHorizontal,
|
||||||
X, WifiOff, ShoppingCart, Users, Coffee, ArrowRight, LayoutGrid, Armchair,
|
X, WifiOff, ShoppingCart, Users, Coffee, ArrowRight, LayoutGrid, Armchair,
|
||||||
Banknote, Check, Delete, ReceiptText, ShoppingBag, Loader2, RotateCcw,
|
Banknote, Check, Delete, ReceiptText, ShoppingBag, Loader2, RotateCcw,
|
||||||
BadgePercent, Sparkles, Home, StickyNote,
|
BadgePercent, Sparkles, Home, StickyNote,
|
||||||
@@ -24,6 +24,7 @@ import { useCartStore } from "@/lib/stores/cart.store";
|
|||||||
import { apiGet, apiPost, ApiClientError } from "@/lib/api/client";
|
import { apiGet, apiPost, ApiClientError } from "@/lib/api/client";
|
||||||
import { submitOrderToApi, orderAmountDue, isLocalOrder } from "@/lib/pos/submit-order";
|
import { submitOrderToApi, orderAmountDue, isLocalOrder } from "@/lib/pos/submit-order";
|
||||||
import { requestPosPayment, posDeviceErrorMessage } from "@/lib/api/pos-device";
|
import { requestPosPayment, posDeviceErrorMessage } from "@/lib/api/pos-device";
|
||||||
|
import { printReceipt } from "@/lib/api/print";
|
||||||
import { PosCustomerPicker } from "@/components/pos/pos-customer-picker";
|
import { PosCustomerPicker } from "@/components/pos/pos-customer-picker";
|
||||||
import { Can } from "@/components/auth/can";
|
import { Can } from "@/components/auth/can";
|
||||||
import type { Customer, MenuItem, Order, TableBoardItem } from "@/lib/api/types";
|
import type { Customer, MenuItem, Order, TableBoardItem } from "@/lib/api/types";
|
||||||
@@ -107,6 +108,7 @@ export function Pos2Screen() {
|
|||||||
const hydrateFromOrder = useCartStore((s) => s.hydrateFromOrder);
|
const hydrateFromOrder = useCartStore((s) => s.hydrateFromOrder);
|
||||||
const clearSession = useCartStore((s) => s.clearSession);
|
const clearSession = useCartStore((s) => s.clearSession);
|
||||||
const activeOrderNo = useCartStore((s) => s.activeOrderDisplayNumber);
|
const activeOrderNo = useCartStore((s) => s.activeOrderDisplayNumber);
|
||||||
|
const activeOrderId = useCartStore((s) => s.activeOrderId);
|
||||||
const appliedCoupon = useCartStore((s) => s.appliedCoupon);
|
const appliedCoupon = useCartStore((s) => s.appliedCoupon);
|
||||||
|
|
||||||
// local view state
|
// local view state
|
||||||
@@ -285,7 +287,10 @@ export function Pos2Screen() {
|
|||||||
loyaltyPointsToRedeem: loyaltyRedeem > 0 ? loyaltyRedeem : undefined,
|
loyaltyPointsToRedeem: loyaltyRedeem > 0 ? loyaltyRedeem : undefined,
|
||||||
});
|
});
|
||||||
const paid = payments.reduce((s, p) => s + p.amount, 0);
|
const paid = payments.reduce((s, p) => s + p.amount, 0);
|
||||||
notify.success(`پرداخت ${fmt(paid)} تومان ثبت شد`);
|
const paidOrderId = payTarget.id;
|
||||||
|
notify.success(`پرداخت ${fmt(paid)} تومان ثبت شد`, {
|
||||||
|
action: { label: "چاپ فاکتور", onClick: () => void printReceipt(cafeId as string, paidOrderId) },
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["tables-board", cafeId] });
|
queryClient.invalidateQueries({ queryKey: ["tables-board", cafeId] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders-open", cafeId] });
|
queryClient.invalidateQueries({ queryKey: ["orders-open", cafeId] });
|
||||||
backToBoard();
|
backToBoard();
|
||||||
@@ -302,6 +307,27 @@ export function Pos2Screen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Print (or reprint) the customer receipt for the active, already-saved order.
|
||||||
|
const printActiveReceipt = async () => {
|
||||||
|
if (!activeOrderId || isLocalOrder(activeOrderId)) {
|
||||||
|
notify.error("ابتدا سفارش را ثبت کنید");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await printReceipt(cafeId as string, activeOrderId);
|
||||||
|
notify.success("فاکتور برای چاپ ارسال شد");
|
||||||
|
} catch (e) {
|
||||||
|
const code = e instanceof ApiClientError ? e.code : "";
|
||||||
|
notify.error(
|
||||||
|
code === "PRINTER_NOT_CONFIGURED" || code === "KITCHEN_PRINTER_NOT_CONFIGURED"
|
||||||
|
? "پرینتر فاکتور تنظیم نشده است"
|
||||||
|
: code === "PRINTER_CONNECTION_FAILED"
|
||||||
|
? "اتصال به پرینتر برقرار نشد"
|
||||||
|
: "چاپ فاکتور ناموفق بود",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── guards ───────────────────────────────────────────────────────────────
|
// ── guards ───────────────────────────────────────────────────────────────
|
||||||
if (!cafeId) {
|
if (!cafeId) {
|
||||||
return (
|
return (
|
||||||
@@ -322,6 +348,8 @@ export function Pos2Screen() {
|
|||||||
onBump: (id: string, d: number) => { const l = items.find((x) => x.menuItem.id === id); if (l) updateQty(id, l.quantity + d); },
|
onBump: (id: string, d: number) => { const l = items.find((x) => x.menuItem.id === id); if (l) updateQty(id, l.quantity + d); },
|
||||||
onRemove: removeItem, onSend: send, onPay: openPay, onSplit: openPay,
|
onRemove: removeItem, onSend: send, onPay: openPay, onSplit: openPay,
|
||||||
onNote: (id: string, notes: string) => setNotes(id, notes),
|
onNote: (id: string, notes: string) => setNotes(id, notes),
|
||||||
|
canPrint: !!activeOrderId && !isLocalOrder(activeOrderId),
|
||||||
|
onPrintReceipt: printActiveReceipt,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── TABLE BOARD ────────────────────────────────────────────────────────────
|
// ── TABLE BOARD ────────────────────────────────────────────────────────────
|
||||||
@@ -673,13 +701,14 @@ function Pos2Extras({ cafeId }: { cafeId: string }) {
|
|||||||
// ── Order ticket ─────────────────────────────────────────────────────────────
|
// ── Order ticket ─────────────────────────────────────────────────────────────
|
||||||
type TicketLine = { menuItem: MenuItem; quantity: number; notes?: string };
|
type TicketLine = { menuItem: MenuItem; quantity: number; notes?: string };
|
||||||
function Ticket({
|
function Ticket({
|
||||||
cafeId, lines, subtotal, discount, tax, total, count, pendingCount, onBump, onRemove, onNote, onSend, onPay, onSplit,
|
cafeId, lines, subtotal, discount, tax, total, count, pendingCount, onBump, onRemove, onNote, onSend, onPay, onSplit, canPrint, onPrintReceipt,
|
||||||
}: {
|
}: {
|
||||||
cafeId: string; lines: TicketLine[]; subtotal: number; discount: number; tax: number; total: number;
|
cafeId: string; lines: TicketLine[]; subtotal: number; discount: number; tax: number; total: number;
|
||||||
count: number; pendingCount: number;
|
count: number; pendingCount: number;
|
||||||
onBump: (id: string, d: number) => void; onRemove: (id: string) => void;
|
onBump: (id: string, d: number) => void; onRemove: (id: string) => void;
|
||||||
onNote: (id: string, notes: string) => void;
|
onNote: (id: string, notes: string) => void;
|
||||||
onSend: () => void; onPay: () => void; onSplit: () => void;
|
onSend: () => void; onPay: () => void; onSplit: () => void;
|
||||||
|
canPrint: boolean; onPrintReceipt: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [noteFor, setNoteFor] = useState<string | null>(null);
|
const [noteFor, setNoteFor] = useState<string | null>(null);
|
||||||
return (
|
return (
|
||||||
@@ -762,8 +791,10 @@ function Ticket({
|
|||||||
<CreditCard className="size-5" /> پرداخت
|
<CreditCard className="size-5" /> پرداخت
|
||||||
</button>
|
</button>
|
||||||
</Can>
|
</Can>
|
||||||
<button type="button" disabled className="flex min-h-[48px] items-center justify-center gap-1.5 rounded-xl bg-muted text-sm font-medium text-muted-foreground opacity-50">
|
<button type="button" disabled={!canPrint} onClick={onPrintReceipt}
|
||||||
<Pause className="size-4" /> نگهداشتن
|
className="flex min-h-[48px] items-center justify-center gap-1.5 rounded-xl bg-muted text-sm font-medium text-foreground hover:bg-accent disabled:opacity-40"
|
||||||
|
title="چاپ فاکتور مشتری">
|
||||||
|
<ReceiptText className="size-4" /> چاپ فاکتور
|
||||||
</button>
|
</button>
|
||||||
<Can permission="HandlePayments">
|
<Can permission="HandlePayments">
|
||||||
<button type="button" disabled={count === 0} onClick={onSplit}
|
<button type="button" disabled={count === 0} onClick={onSplit}
|
||||||
|
|||||||
Reference in New Issue
Block a user