This commit is contained in:
@@ -17,6 +17,7 @@ import PaymentInfo from './components/PaymentInfo'
|
|||||||
import OrderActions from './components/OrderActions'
|
import OrderActions from './components/OrderActions'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { printOrderReceipt } from './print/orderReceiptPrint'
|
||||||
|
|
||||||
const OrderDetails: FC = () => {
|
const OrderDetails: FC = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -197,6 +198,10 @@ const OrderDetails: FC = () => {
|
|||||||
setShowRefundModal(false)
|
setShowRefundModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePrintOrder = () => {
|
||||||
|
printOrderReceipt(order)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
@@ -228,6 +233,7 @@ const OrderDetails: FC = () => {
|
|||||||
getPaymentStatusText={getPaymentStatusText}
|
getPaymentStatusText={getPaymentStatusText}
|
||||||
/>
|
/>
|
||||||
<OrderActions
|
<OrderActions
|
||||||
|
onPrint={handlePrintOrder}
|
||||||
showVerifyCashPaymentButton={showVerifyCashPaymentButton}
|
showVerifyCashPaymentButton={showVerifyCashPaymentButton}
|
||||||
showSendToKitchenButton={showSendToKitchenButton}
|
showSendToKitchenButton={showSendToKitchenButton}
|
||||||
showCancelButton={showCancelButton}
|
showCancelButton={showCancelButton}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Button from '@/components/Button'
|
|||||||
import { type FC, useState, useEffect } from 'react'
|
import { type FC, useState, useEffect } from 'react'
|
||||||
|
|
||||||
interface OrderActionsProps {
|
interface OrderActionsProps {
|
||||||
|
onPrint: () => void
|
||||||
showVerifyCashPaymentButton: boolean
|
showVerifyCashPaymentButton: boolean
|
||||||
showSendToKitchenButton: boolean
|
showSendToKitchenButton: boolean
|
||||||
showCancelButton: boolean
|
showCancelButton: boolean
|
||||||
@@ -21,6 +22,7 @@ interface OrderActionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OrderActions: FC<OrderActionsProps> = ({
|
const OrderActions: FC<OrderActionsProps> = ({
|
||||||
|
onPrint,
|
||||||
showVerifyCashPaymentButton,
|
showVerifyCashPaymentButton,
|
||||||
showSendToKitchenButton,
|
showSendToKitchenButton,
|
||||||
showCancelButton,
|
showCancelButton,
|
||||||
@@ -71,11 +73,15 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
onCancelOrder()
|
onCancelOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCanceled) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-6 flex flex-col gap-3'>
|
<div className='mt-6 flex flex-col gap-3'>
|
||||||
{showVerifyCashPaymentButton && (
|
<Button
|
||||||
|
label='چاپ'
|
||||||
|
onClick={onPrint}
|
||||||
|
className='bg-slate-700 hover:bg-slate-800'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isCanceled && showVerifyCashPaymentButton && (
|
||||||
<Button
|
<Button
|
||||||
label='تایید پرداخت'
|
label='تایید پرداخت'
|
||||||
onClick={onVerifyCashPayment}
|
onClick={onVerifyCashPayment}
|
||||||
@@ -84,7 +90,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showSendToKitchenButton && (
|
{!isCanceled && showSendToKitchenButton && (
|
||||||
<Button
|
<Button
|
||||||
label='ارسال به آشپزخانه'
|
label='ارسال به آشپزخانه'
|
||||||
onClick={handleSendToKitchen}
|
onClick={handleSendToKitchen}
|
||||||
@@ -93,7 +99,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPreparing && isCourierDelivery && (
|
{!isCanceled && isPreparing && isCourierDelivery && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به پیک'
|
label='تحویل به پیک'
|
||||||
onClick={handleDeliverToCourier}
|
onClick={handleDeliverToCourier}
|
||||||
@@ -102,7 +108,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPreparing && isDineInOrCarDelivery && (
|
{!isCanceled && isPreparing && isDineInOrCarDelivery && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به گارسون'
|
label='تحویل به گارسون'
|
||||||
onClick={handleDeliverToWaiter}
|
onClick={handleDeliverToWaiter}
|
||||||
@@ -111,7 +117,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPreparing && isCustomerPickup && (
|
{!isCanceled && isPreparing && isCustomerPickup && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به رسپشنیست'
|
label='تحویل به رسپشنیست'
|
||||||
onClick={handleDeliverToReceptionist}
|
onClick={handleDeliverToReceptionist}
|
||||||
@@ -120,7 +126,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCancelButton && (
|
{!isCanceled && showCancelButton && (
|
||||||
<Button
|
<Button
|
||||||
label='لغو سفارش'
|
label='لغو سفارش'
|
||||||
onClick={handleCancelOrder}
|
onClick={handleCancelOrder}
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { formatFaNumber, formatOptionalDate, formatPrice } from "@/helpers/func";
|
||||||
|
import type { Order } from "../types/Types";
|
||||||
|
import { printHtmlDocument } from "@/shared/print/printHtmlDocument";
|
||||||
|
|
||||||
|
const baseReceiptStyles = `
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Tahoma, Arial, sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
@page {
|
||||||
|
size: 80mm auto;
|
||||||
|
margin: 4mm;
|
||||||
|
}
|
||||||
|
.receipt {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.center { text-align: center; }
|
||||||
|
.title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.sub { color: #555; font-size: 11px; }
|
||||||
|
.divider {
|
||||||
|
border-top: 1px dashed #888;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.muted { color: #666; }
|
||||||
|
.items {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.items td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.item-name { width: 46%; }
|
||||||
|
.item-qty { width: 18%; text-align: center; }
|
||||||
|
.item-unit { width: 36%; text-align: left; }
|
||||||
|
.total-row {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getCustomerName = (order: Order): string => {
|
||||||
|
const fullName = [order.user?.firstName, order.user?.lastName].filter(Boolean).join(" ").trim();
|
||||||
|
return fullName || "-";
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildOrderReceiptHtml = (order: Order): string => {
|
||||||
|
const customerAddress = order.userAddress
|
||||||
|
? `${order.userAddress.province || ""}، ${order.userAddress.city || ""}، ${order.userAddress.address || ""}، کد پستی: ${order.userAddress.postalCode || "-"}`.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const itemsHtml = order.items
|
||||||
|
.map(
|
||||||
|
(item) => `
|
||||||
|
<tr>
|
||||||
|
<td class="item-name">${item.food?.title || "-"}</td>
|
||||||
|
<td class="item-qty">${formatFaNumber(item.quantity)}</td>
|
||||||
|
<td class="item-unit">${formatPrice(item.unitPrice)}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<main class="receipt">
|
||||||
|
<section class="center">
|
||||||
|
<div class="title">${order.restaurant?.name || "رسید سفارش"}</div>
|
||||||
|
<div class="sub">سفارش شماره ${formatFaNumber(order.orderNumber)}</div>
|
||||||
|
<div class="sub">${formatOptionalDate(order.createdAt, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="row"><span class="muted">مشتری</span><span>${getCustomerName(order)}</span></div>
|
||||||
|
<div class="row"><span class="muted">موبایل</span><span>${order.user?.phone || "-"}</span></div>
|
||||||
|
<div class="row"><span class="muted">روش تحویل</span><span>${order.deliveryMethod?.description || "-"}</span></div>
|
||||||
|
<div class="row"><span class="muted">روش پرداخت</span><span>${order.paymentMethod?.description || "-"}</span></div>
|
||||||
|
${
|
||||||
|
customerAddress
|
||||||
|
? `<div class="divider"></div><div><span class="muted">آدرس مشتری</span><div>${customerAddress}</div></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<table class="items">
|
||||||
|
<tbody>
|
||||||
|
${itemsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="row"><span class="muted">جمع آیتمها</span><span>${formatPrice(order.subTotal)}</span></div>
|
||||||
|
${
|
||||||
|
order.itemsDiscount > 0
|
||||||
|
? `<div class="row"><span class="muted">تخفیف آیتمها</span><span>${formatPrice(order.itemsDiscount)}</span></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
order.couponDiscount > 0
|
||||||
|
? `<div class="row"><span class="muted">تخفیف کوپن</span><span>${formatPrice(order.couponDiscount)}</span></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="row"><span class="muted">هزینه ارسال</span><span>${formatPrice(order.deliveryFee)}</span></div>
|
||||||
|
${
|
||||||
|
order.tax > 0
|
||||||
|
? `<div class="row"><span class="muted">مالیات</span><span>${formatPrice(order.tax)}</span></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="row total-row"><span>مبلغ کل</span><span>${formatPrice(order.total)}</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
${
|
||||||
|
order.description
|
||||||
|
? `<div class="divider"></div><section><div class="muted">توضیحات:</div><div>${order.description}</div></section>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printOrderReceipt = (order: Order): void => {
|
||||||
|
const html = buildOrderReceiptHtml(order);
|
||||||
|
printHtmlDocument({
|
||||||
|
title: `سفارش ${order.orderNumber}`,
|
||||||
|
html,
|
||||||
|
widthMm: 80,
|
||||||
|
styles: baseReceiptStyles,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
type PrintHtmlDocumentParams = {
|
||||||
|
title: string;
|
||||||
|
html: string;
|
||||||
|
widthMm?: number;
|
||||||
|
styles?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultStyles = `
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
font-family: Tahoma, Arial, sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
@page {
|
||||||
|
size: 80mm auto;
|
||||||
|
margin: 4mm;
|
||||||
|
}
|
||||||
|
.receipt {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const printHtmlDocument = ({
|
||||||
|
title,
|
||||||
|
html,
|
||||||
|
widthMm = 80,
|
||||||
|
styles,
|
||||||
|
}: PrintHtmlDocumentParams): void => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const printWindow = window.open("", "_blank");
|
||||||
|
if (!printWindow) return;
|
||||||
|
|
||||||
|
const safeTitle = title.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
const pageStyles = styles ?? defaultStyles.replace("80mm", `${widthMm}mm`);
|
||||||
|
|
||||||
|
const pageHtml = `
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>${safeTitle}</title>
|
||||||
|
<style>${pageStyles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${html}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
printWindow.document.open();
|
||||||
|
printWindow.document.write(pageHtml);
|
||||||
|
printWindow.document.close();
|
||||||
|
|
||||||
|
const handlePrint = () => {
|
||||||
|
printWindow.focus();
|
||||||
|
printWindow.print();
|
||||||
|
setTimeout(() => {
|
||||||
|
printWindow.close();
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some browsers complete loading too fast for onload assignment.
|
||||||
|
if (printWindow.document.readyState === "complete") {
|
||||||
|
setTimeout(handlePrint, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printWindow.addEventListener("load", () => {
|
||||||
|
setTimeout(handlePrint, 50);
|
||||||
|
}, { once: true });
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user