show items order

This commit is contained in:
hamid zarghami
2026-06-28 16:24:38 +03:30
parent 1300491a2d
commit a269266897
5 changed files with 280 additions and 238 deletions
@@ -1,4 +1,4 @@
import { BaseResponse } from "@/app/[name]/(Main)/types/Types"; import { BaseResponse, Product } from "@/app/[name]/(Main)/types/Types";
export interface ShipmentMethod { export interface ShipmentMethod {
id: string; id: string;
@@ -361,28 +361,15 @@ export interface OrderDetailPaymentMethod {
merchantId: string | null; merchantId: string | null;
} }
export interface OrderDetailFood { export interface OrderDetailVariant {
id: string; id: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
restaurant: string; value: string;
category: string;
title: string;
desc: string;
content: string[];
price: number; price: number;
order: number | null; stock: number | null;
prepareTime: number | null; product: Product;
weekDays: number[];
mealTypes: string[];
isActive: boolean;
images: string[];
inPlaceServe: boolean;
pickupServe: boolean;
score: number;
discount: number;
isSpecialOffer: boolean;
} }
export interface OrderDetailItem { export interface OrderDetailItem {
@@ -391,11 +378,12 @@ export interface OrderDetailItem {
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
order: string; order: string;
food: OrderDetailFood; variant: OrderDetailVariant;
quantity: number; quantity: number;
unitPrice: number; unitPrice: number;
discount: number; discount: number;
totalPrice: number; totalPrice: number;
status?: string;
} }
export interface OrderDetailHistory { export interface OrderDetailHistory {
+138 -100
View File
@@ -1,31 +1,57 @@
'use client'; "use client";
import { useParams, useRouter } from 'next/navigation'; import type { Product, ProductCategory } from "@/app/[name]/(Main)/types/Types";
import React from 'react'; import { isVariablePricing } from "@/app/[name]/(Main)/types/Types";
import Button from '@/components/button/PrimaryButton'; import Button from "@/components/button/PrimaryButton";
import { Skeleton } from '@/components/ui/skeleton'; import MenuItem from "@/components/listview/MenuItem";
import { Clock } from 'iconsax-react'; import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import clsx from 'clsx'; import { toast } from "@/components/Toast";
import useToggle from '@/hooks/helpers/useToggle'; import { Skeleton } from "@/components/ui/skeleton";
import Prompt from '@/components/utils/Prompt'; import Prompt from "@/components/utils/Prompt";
import { useCancelOrder, useGetOrderDetail } from '../../checkout/hooks/useOrderData'; import useToggle from "@/hooks/helpers/useToggle";
import ordersTranslations from '@/locales/fa/orders.json'; import { extractErrorMessage } from "@/lib/func";
import { toast } from '@/components/Toast'; import ordersTranslations from "@/locales/fa/orders.json";
import { extractErrorMessage } from '@/lib/func'; import { useQueryClient } from "@tanstack/react-query";
import { useQueryClient } from '@tanstack/react-query'; import clsx from "clsx";
import { Clock } from "iconsax-react";
import { useParams, useRouter } from "next/navigation";
import React from "react";
import { useCancelOrder, useGetOrderDetail } from "../../checkout/hooks/useOrderData";
import type { OrderDetailItem } from "../../checkout/types/Types";
const orderItemToProduct = (item: OrderDetailItem): Product => {
const { product, ...variant } = item.variant;
const categoryId = typeof product.category === "string" ? product.category : (product.category?.id ?? "");
return {
...product,
category: (typeof product.category === "object" && product.category ? product.category : { id: categoryId }) as ProductCategory,
variants: [
{
id: variant.id,
createdAt: variant.createdAt,
updatedAt: variant.updatedAt,
deletedAt: variant.deletedAt,
product: product.id,
value: variant.value,
price: item.unitPrice,
},
],
} as Product;
};
const getDeliveryMethodTitle = (method: string) => { const getDeliveryMethodTitle = (method: string) => {
switch (method) { switch (method) {
case 'deliveryCourier': case "deliveryCourier":
return 'ارسال توسط پیک'; return "ارسال توسط پیک";
case 'deliveryCar': case "deliveryCar":
return 'تحویل به خودرو'; return "تحویل به خودرو";
case 'pickup': case "pickup":
return 'تحویل حضوری'; return "تحویل حضوری";
case 'dineIn': case "dineIn":
return 'سرو در فروشگاه'; return "سرو در فروشگاه";
default: default:
return 'روش ارسال'; return "روش ارسال";
} }
}; };
@@ -54,15 +80,15 @@ const getDeliveryMethodTitle = (method: string) => {
const getStatusText = (status: string): string => { const getStatusText = (status: string): string => {
const statusKey = status as keyof typeof ordersTranslations.status; const statusKey = status as keyof typeof ordersTranslations.status;
return ordersTranslations.status[statusKey] || 'نامشخص'; return ordersTranslations.status[statusKey] || "نامشخص";
}; };
const formatDate = (dateString: string): string => { const formatDate = (dateString: string): string => {
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', { return date.toLocaleDateString("fa-IR", {
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric' day: "numeric",
}); });
}; };
@@ -88,137 +114,149 @@ function OrderTrackingPage() {
cancelOrder(id as string, { cancelOrder(id as string, {
onSuccess: () => { onSuccess: () => {
toggleCancelModal(); toggleCancelModal();
queryClient.invalidateQueries({ queryKey: ['order-detail', id] }); queryClient.invalidateQueries({ queryKey: ["order-detail", id] });
toast('سفارش با موفقیت لغو شد', 'success'); toast("سفارش با موفقیت لغو شد", "success");
}, },
onError: (error) => { onError: (error) => {
toast(extractErrorMessage(error), 'error'); toast(extractErrorMessage(error), "error");
} },
}); });
}; };
return ( return (
<div className="fixed inset-0 bg-container flex flex-col"> <div className="fixed inset-0 bg-container flex flex-col">
<div className='flex-1 overflow-y-auto px-4 py-6'> <div className="flex-1 overflow-y-auto px-4 py-6">
<div className='max-w-2xl mx-auto flex flex-col gap-6'> <div className="max-w-2xl mx-auto flex flex-col gap-6">
{isLoading ? ( {isLoading ? (
<> <>
<Skeleton className='h-6 w-48 mx-auto' /> <Skeleton className="h-6 w-48 mx-auto" />
<div className='flex flex-col gap-4'> <div className="flex flex-col gap-4">
<Skeleton className='h-5 w-full' /> <Skeleton className="h-5 w-full" />
<Skeleton className='h-5 w-full' /> <Skeleton className="h-5 w-full" />
<Skeleton className='h-20 w-full' /> <Skeleton className="h-20 w-full" />
<Skeleton className='h-5 w-full' /> <Skeleton className="h-5 w-full" />
</div> </div>
</> </>
) : orderDetail?.data ? ( ) : orderDetail?.data ? (
<> <>
{orderDetail.data.status === 'needConfirmation' && ( {orderDetail.data.status === "needConfirmation" && (
<div className='rounded-2xl bg-red-50 px-4 py-3 text-center'> <div className="rounded-2xl bg-red-50 px-4 py-3 text-center">
<p className='text-sm text-red-800 leading-6'> <p className="text-sm text-red-800 leading-6">لطفاً پس از دریافت پیامک، حداکثر تا ۲ ساعت نسبت به پرداخت اقدام کنید؛ در غیر این صورت سفارش بهصورت خودکار لغو خواهد شد.</p>
لطفاً پس از دریافت پیامک، حداکثر تا ۲ ساعت نسبت به پرداخت اقدام کنید؛ در غیر این صورت سفارش بهصورت خودکار لغو خواهد شد.
</p>
</div> </div>
)} )}
<h1 className='text-lg font-semibold text-center'> <h1 className="text-lg font-semibold text-center">{getDeliveryMethodTitle(orderDetail.data.deliveryMethod.method)}</h1>
{getDeliveryMethodTitle(orderDetail.data.deliveryMethod.method)}
</h1>
<div className='flex flex-col'> <div className="flex flex-col">
<div className='flex justify-between items-center h-16 border-b border-border'> <div className="flex justify-between items-center h-16 border-b border-border">
<span className='text-sm text-muted-foreground flex items-center gap-2'> <span className="text-sm text-muted-foreground flex items-center gap-2">
<Clock className='stroke-current' size={16} /> <Clock className="stroke-current" size={16} />
زمان سفارش زمان سفارش
</span> </span>
<div className='flex items-end gap-0.5'> <div className="flex items-end gap-0.5">
<span className='text-sm font-semibold'>{formatDate(orderDetail.data.createdAt)}</span> <span className="text-sm font-semibold">{formatDate(orderDetail.data.createdAt)}</span>
{/* <span className='text-xs text-muted-foreground'>{formatTime(orderDetail.data.createdAt)}</span> */} {/* <span className='text-xs text-muted-foreground'>{formatTime(orderDetail.data.createdAt)}</span> */}
</div> </div>
</div> </div>
<div className='flex justify-between items-center h-16 border-b border-border'> <div className="flex justify-between items-center h-16 border-b border-border">
<span className='text-sm text-muted-foreground'>نوع ارسال</span> <span className="text-sm text-muted-foreground">نوع ارسال</span>
<span className='text-sm font-semibold'>{orderDetail.data?.deliveryMethod?.description}</span> <span className="text-sm font-semibold">{orderDetail.data?.deliveryMethod?.description}</span>
</div> </div>
{orderDetail.data.carAddress && ( {orderDetail.data.carAddress && (
<div className='flex justify-between items-center h-16 border-b border-border'> <div className="flex justify-between items-center h-16 border-b border-border">
<div className='text-sm text-muted-foreground mb-1'>اطلاعات خودرو</div> <div className="text-sm text-muted-foreground mb-1">اطلاعات خودرو</div>
<div className='text-sm font-semibold'> <div className="text-sm font-semibold">
{orderDetail.data.carAddress.carModel} {orderDetail.data.carAddress.carColor} - پلاک: {orderDetail.data.carAddress.plateNumber} {orderDetail.data.carAddress.carModel} {orderDetail.data.carAddress.carColor} - پلاک: {orderDetail.data.carAddress.plateNumber}
</div> </div>
</div> </div>
)} )}
<div className='flex justify-between items-center h-16 border-b border-border'> <div className="flex justify-between items-center h-16 border-b border-border">
<span className='text-sm text-muted-foreground'>شماره سفارش</span> <span className="text-sm text-muted-foreground">شماره سفارش</span>
<span className='text-sm font-semibold'>#{orderDetail.data.orderNumber}</span> <span className="text-sm font-semibold">#{orderDetail.data.orderNumber}</span>
</div> </div>
<div className='flex justify-between items-center h-16 border-b border-border'> <div className="flex justify-between items-center h-16 border-b border-border">
<span className='text-sm text-muted-foreground'>وضعیت سفارش</span> <span className="text-sm text-muted-foreground">وضعیت سفارش</span>
<span className='text-sm font-semibold'>{getStatusText(orderDetail.data.status)}</span> <span className="text-sm font-semibold">{getStatusText(orderDetail.data.status)}</span>
</div> </div>
{orderDetail.data.tableNumber && ( {orderDetail.data.tableNumber && (
<div className='flex justify-between items-center h-16 border-b border-border'> <div className="flex justify-between items-center h-16 border-b border-border">
<span className='text-sm text-muted-foreground'>شماره میز</span> <span className="text-sm text-muted-foreground">شماره میز</span>
<span className='text-sm font-semibold'>#{orderDetail.data.tableNumber}</span> <span className="text-sm font-semibold">#{orderDetail.data.tableNumber}</span>
</div> </div>
)} )}
{orderDetail.data.userAddress && ( {orderDetail.data.userAddress && (
<div className='py-3 border-b border-border'> <div className="py-3 border-b border-border">
<div className='text-sm text-muted-foreground mb-1'>آدرس تحویل</div> <div className="text-sm text-muted-foreground mb-1">آدرس تحویل</div>
<div className='text-sm'>{orderDetail.data.userAddress.address}</div> <div className="text-sm">{orderDetail.data.userAddress.address}</div>
</div> </div>
)} )}
<div className='flex justify-between items-center py-4 border-t border-border'> {orderDetail.data.items?.length > 0 && (
<span className='text-sm font-medium'>مجموع سفارش</span> <div className="py-4 mt-7 border-b border-border">
<span className='text-sm font-bold '> <div className="text-sm font-medium mb-4">اقلام سفارش</div>
{orderDetail.data.total.toLocaleString('fa-IR')} تومان <div className="flex flex-col gap-4">
{orderDetail.data.items.map((item) => {
const product = orderItemToProduct(item);
const showPendingBadge = isVariablePricing(product);
return (
<MenuItemRenderer key={item.id}>
<MenuItem
product={product}
variantId={item.variant.id}
variantValue={item.variant.value}
readOnly
badge={
showPendingBadge ? (
<span className="inline-block rounded-lg bg-orange-50 dark:bg-orange-950/40 px-3 py-1.5 text-xs text-orange-600 dark:text-orange-400">
{ordersTranslations.itemStatus.needConfirmation}
</span> </span>
) : undefined
}
/>
</MenuItemRenderer>
);
})}
</div>
</div>
)}
<div className="flex justify-between items-center py-4 border-t border-border">
<span className="text-sm font-medium">مجموع سفارش</span>
<span className="text-sm font-bold ">{orderDetail.data.total.toLocaleString("fa-IR")} تومان</span>
</div> </div>
</div> </div>
</> </>
) : ( ) : (
<div className='text-center text-muted-foreground py-12'> <div className="text-center text-muted-foreground py-12">اطلاعات سفارش یافت نشد</div>
اطلاعات سفارش یافت نشد
</div>
)} )}
</div> </div>
</div> </div>
<div className='p-4 border-t border-border bg-container'> <div className="p-4 border-t border-border bg-container">
<div className='max-w-2xl mx-auto grid grid-cols-2 gap-4'> <div className="max-w-2xl mx-auto grid grid-cols-2 gap-4">
<Button <Button className="dark:bg-white dark:text-black! dark:hover:bg-gray-100" onClick={() => router.push(`/${name}`)} type="button">
className='dark:bg-white dark:text-black! dark:hover:bg-gray-100'
onClick={() => router.push(`/${name}`)}
type='button'>
بازگشت به منو بازگشت به منو
</Button> </Button>
<Button <Button
type='submit' type="submit"
onClick={toggleCancelModal} onClick={toggleCancelModal}
className='bg-disabled! text-foreground!' className="bg-disabled! text-foreground!"
disabled={ disabled={orderDetail?.data?.status === "cancelled" || orderDetail?.data?.status === "delivered" || orderDetail?.data?.status === "delivering"}
orderDetail?.data?.status === 'cancelled' ||
orderDetail?.data?.status === 'delivered' ||
orderDetail?.data?.status === 'delivering'
}
> >
لغو سفارش لغو سفارش
</Button> </Button>
</div> </div>
</div> </div>
<div className={clsx( <div className={clsx("fixed inset-0 z-1001", !cancelModal && "pointer-events-none")}>
'fixed inset-0 z-1001',
!cancelModal && 'pointer-events-none'
)}>
<Prompt <Prompt
title={'لغو سفارش'} title={"لغو سفارش"}
description={'آیا از درخواست خود اطمینان دارید؟'} description={"آیا از درخواست خود اطمینان دارید؟"}
textConfirm={'بله'} textConfirm={"بله"}
textCancel={'منصرف شدم'} textCancel={"منصرف شدم"}
onConfirm={onCancelOrder} onConfirm={onCancelOrder}
visible={cancelModal} visible={cancelModal}
onClick={toggleCancelModal} onClick={toggleCancelModal}
+15 -2
View File
@@ -8,7 +8,7 @@ import { CartQuantityControl } from "@/components/CartQuantityControl";
import { ArrowLeft } from "iconsax-react"; import { ArrowLeft } from "iconsax-react";
import Link from "next/link"; import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { memo, useMemo } from "react"; import { memo, useMemo, type ReactNode } from "react";
export type MenuItemViewMode = "list" | "grid"; export type MenuItemViewMode = "list" | "grid";
@@ -20,9 +20,13 @@ interface MenuItemProps {
variantValue?: string | null; variantValue?: string | null;
/** حالت نمایش: list = یک ستونه (پیش‌فرض)، grid = دو ستونه */ /** حالت نمایش: list = یک ستونه (پیش‌فرض)، grid = دو ستونه */
viewMode?: MenuItemViewMode; viewMode?: MenuItemViewMode;
/** فقط نمایش اطلاعات بدون دکمه‌های افزودن/جزئیات */
readOnly?: boolean;
/** برچسب اختیاری بالای آیتم */
badge?: ReactNode;
} }
const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValueProp, viewMode = "list" }: MenuItemProps) => { const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValueProp, viewMode = "list", readOnly = false, badge }: MenuItemProps) => {
const { items, addToCart, removeFromCart, isCartMutating } = useCart(); const { items, addToCart, removeFromCart, isCartMutating } = useCart();
const params = useParams<{ name: string }>(); const params = useParams<{ name: string }>();
const name = params?.name || ""; const name = params?.name || "";
@@ -146,6 +150,12 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
} }
return ( return (
<>
{badge && (
<div className="absolute top-3 left-3 z-10">
{badge}
</div>
)}
<div className="flex gap-4 w-full min-h-28 items-stretch"> <div className="flex gap-4 w-full min-h-28 items-stretch">
<Link href={productDetailUrl} className="cursor-pointer shrink-0 self-center"> <Link href={productDetailUrl} className="cursor-pointer shrink-0 self-center">
<img className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 aspect-square object-cover" src={resolvedImage} height={112} width={112} alt={productName} /> <img className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 aspect-square object-cover" src={resolvedImage} height={112} width={112} alt={productName} />
@@ -175,6 +185,7 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
)} )}
</div> </div>
</div> </div>
{!readOnly && (
<div className="flex flex-col justify-end shrink-0"> <div className="flex flex-col justify-end shrink-0">
{showDetailsInsteadOfAdd ? ( {showDetailsInsteadOfAdd ? (
<div className="w-[115px]"> <div className="w-[115px]">
@@ -189,7 +200,9 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
</div> </div>
)} )}
</div> </div>
)}
</div> </div>
</>
); );
}; };
+1 -1
View File
@@ -15,7 +15,7 @@ function MenuItemRendererComponent({ children, variant = 'list', ...rest }: Prop
className={`${rest.className} box-shadow-normal transition-all duration-200 ease-out w-full bg-container rounded-3xl ${ className={`${rest.className} box-shadow-normal transition-all duration-200 ease-out w-full bg-container rounded-3xl ${
isGrid isGrid
? 'p-3 flex flex-col' ? 'p-3 flex flex-col'
: 'py-4 pb-4! px-4 flex items-stretch' : 'relative py-4 pb-4! px-4 flex items-stretch overflow-visible'
}`} }`}
> >
{children} {children}
+3
View File
@@ -19,6 +19,9 @@
"Online": "پرداخت آنلاین", "Online": "پرداخت آنلاین",
"Wallet": "پرداخت با کیف پول" "Wallet": "پرداخت با کیف پول"
}, },
"itemStatus": {
"needConfirmation": "در انتظار تایید قیمت نهایی"
},
"status": { "status": {
"new": "سفارش جدید", "new": "سفارش جدید",
"needConfirmation": "نیاز به تایید فروشگاه", "needConfirmation": "نیاز به تایید فروشگاه",