detail of order

This commit is contained in:
hamid zarghami
2025-12-17 12:08:00 +03:30
parent b34750e907
commit 79aa9b2d88
6 changed files with 366 additions and 133 deletions
@@ -0,0 +1,14 @@
export const enum OrderStatus {
NEW = "new",
PENDING_PAYMENT = "pendingPayment",
PAID = "paid",
CONFIRMED = "confirmed",
PREPARING = "preparing",
READY = "ready",
SHIPPED = "shipped",
COMPLETED = "completed",
CANCELED = "canceled",
FAILED = "failed",
REFUNDED = "refunded",
}
@@ -9,6 +9,8 @@ import {
applyCoupon, applyCoupon,
removeCoupon, removeCoupon,
setTableNumber, setTableNumber,
getOrderDetail,
cancelOrder,
} from "../service/OrderService"; } from "../service/OrderService";
export const useGetShipmentMethod = () => { export const useGetShipmentMethod = () => {
@@ -76,3 +78,17 @@ export const useSetTableNumber = () => {
mutationFn: setTableNumber, mutationFn: setTableNumber,
}); });
}; };
export const useGetOrderDetail = (id: string) => {
return useQuery({
queryKey: ["order-detail", id],
queryFn: () => getOrderDetail(id),
enabled: !!id,
});
};
export const useCancelOrder = () => {
return useMutation({
mutationFn: cancelOrder,
});
};
@@ -3,6 +3,7 @@ import {
ShipmentMethodsResponse, ShipmentMethodsResponse,
PaymentMethodsResponse, PaymentMethodsResponse,
CreateOrderResponse, CreateOrderResponse,
OrderDetailResponse,
} from "../types/Types"; } from "../types/Types";
export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => { export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => {
@@ -59,3 +60,15 @@ export const setTableNumber = async (number: number) => {
}); });
return data; return data;
}; };
export const getOrderDetail = async (
id: string
): Promise<OrderDetailResponse> => {
const { data } = await api.get<OrderDetailResponse>(`/public/orders/${id}`);
return data;
};
export const cancelOrder = async (id: string) => {
const { data } = await api.patch(`/public/orders/${id}/cancel`);
return data;
};
@@ -250,3 +250,163 @@ export interface CreateOrderData {
} }
export type CreateOrderResponse = BaseResponse<CreateOrderData>; export type CreateOrderResponse = BaseResponse<CreateOrderData>;
export interface OrderDetailUser {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
firstName: string;
lastName: string;
birthDate: string;
marriageDate: string;
referrer: string | null;
isActive: boolean;
gender: boolean;
avatarUrl: string | null;
phone: string;
}
export interface RestaurantScore {
scoreAmount: string;
scoreCredit: string;
birthdayScore: string;
purchaseScore: string;
referrerScore: string;
registerScore: string;
purchaseAmount: string;
marriageDateScore: string;
}
export interface OrderDetailRestaurant {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
name: string;
slug: string;
logo: string | null;
address: string | null;
menuColor: string | null;
latitude: number | null;
longitude: number | null;
serviceArea: ServiceArea;
isActive: boolean;
establishedYear: number | null;
phone: string;
instagram: string | null;
telegram: string | null;
whatsapp: string | null;
description: string | null;
seoTitle: string | null;
seoDescription: string | null;
tagNames: string | null;
images: string | null;
vat: number;
domain: string;
score: RestaurantScore;
}
export interface OrderDetailDeliveryMethod {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
method: "dineIn" | "pickup" | "deliveryCar" | "deliveryCourier";
restaurant: string;
deliveryFee: number;
deliveryFeeType: "fixed" | "percentage";
kilometerNumber: number | null;
deliveryFeeForKilometer: number | null;
minOrderPrice: number;
description: string;
enabled: boolean;
order: number;
}
export interface OrderDetailPaymentMethod {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
restaurant: string;
method: "CardOnDelivery" | "Cash" | "Online";
gateway: string | null;
description: string;
enabled: boolean;
order: number;
merchantId: string | null;
}
export interface OrderDetailFood {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
restaurant: string;
category: string;
title: string;
desc: string;
content: string[];
price: number;
order: number | null;
prepareTime: number | null;
weekDays: number[];
mealTypes: string[];
isActive: boolean;
images: string[];
inPlaceServe: boolean;
pickupServe: boolean;
score: number;
discount: number;
isSpecialOffer: boolean;
}
export interface OrderDetailItem {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
order: string;
food: OrderDetailFood;
quantity: number;
unitPrice: number;
discount: number;
totalPrice: number;
}
export interface OrderDetail {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
user: OrderDetailUser;
restaurant: OrderDetailRestaurant;
deliveryMethod: OrderDetailDeliveryMethod;
userAddress: OrderAddress | null;
carAddress: string | null;
paymentMethod: OrderDetailPaymentMethod;
orderNumber: number;
couponDiscount: number;
couponDetail: string | null;
itemsDiscount: number;
totalDiscount: number;
subTotal: number;
tax: number;
deliveryFee: number;
total: number;
totalItems: number;
description: string | null;
tableNumber: string;
status:
| "new"
| "preparing"
| "ready"
| "delivering"
| "delivered"
| "cancelled";
paymentStatus: "pending" | "paid" | "failed" | "refunded";
items: OrderDetailItem[];
}
export type OrderDetailResponse = BaseResponse<OrderDetail>;
+147 -133
View File
@@ -1,163 +1,177 @@
'use client'; 'use client';
import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useState, useEffect } from 'react'; import React from 'react';
import dynamic from 'next/dynamic';
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
import Button from '@/components/button/PrimaryButton'; import Button from '@/components/button/PrimaryButton';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { MarkerData } from '@/app/[name]/(Profile)/profile/address/types/Types';
import { HandPlatter, Package } from 'lucide-react'; import { HandPlatter, Package } from 'lucide-react';
import AnimatedBar from '@/components/utils/AnimatedBar'; import AnimatedBar from '@/components/utils/AnimatedBar';
import { Clock } from 'iconsax-react'; import { Clock } from 'iconsax-react';
import clsx from 'clsx'; import clsx from 'clsx';
import useToggle from '@/hooks/helpers/useToggle'; import useToggle from '@/hooks/helpers/useToggle';
import Prompt from '@/components/utils/Prompt'; import Prompt from '@/components/utils/Prompt';
import { useGetOrderDetail } from '../../checkout/hooks/useOrderData';
import ordersTranslations from '@/locales/fa/orders.json';
const CustomMap = dynamic( const getDeliveryMethodTitle = (method: string) => {
() => import('@/components/map/CustomMap'), switch (method) {
{ ssr: false } // Leaflet requires browser APIs case 'deliveryCourier':
); return 'ارسال توسط پیک رستوران';
case 'deliveryCar':
return 'ارسال توسط خودرو';
case 'pickup':
return 'تحویل حضوری';
case 'dineIn':
return 'سرو در محل';
default:
return 'روش ارسال';
}
};
const getStatusPercentage = (status: string) => {
switch (status) {
case 'new':
case 'pendingPayment':
return 0;
case 'paid':
case 'confirmed':
return 20;
case 'preparing':
return 40;
case 'ready':
return 60;
case 'shipped':
case 'delivering':
return 80;
case 'completed':
case 'delivered':
return 100;
default:
return 0;
}
};
const getStatusText = (status: string): string => {
const statusKey = status as keyof typeof ordersTranslations.status;
return ordersTranslations.status[statusKey] || 'نامشخص';
};
function OrderTrackingPage() { function OrderTrackingPage() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id } = useParams(); const { id } = useParams();
const params = useSearchParams();
// const [selectedPosition, setSelectedPosition] = useState<[number, number] | null>(null);
// const [selectedAddress, setSelectedAddress] = useState<NominatimReverseGeocodingResponse | null>(null);
const [markers, setMarkers] = useState<MarkerData[]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [mapCenter, setMapCenter] = useState<[number, number]>([Number(params?.get('lon')) || 35.6892, Number(params?.get('lon')) || 51.3890]);
const router = useRouter(); const router = useRouter();
const { state: cancelModal, toggle: toggleCancelModal } = useToggle(); const { state: cancelModal, toggle: toggleCancelModal } = useToggle();
const { data: orderDetail, isLoading } = useGetOrderDetail(id as string);
useEffect(() => {
// Initialize markers on the client side
const initializeMarkers = async () => {
// Dynamic import of Leaflet on the client side
const L = (await import('leaflet')).default;
const iconHtml = `
<div style="display: flex; flex-direction: column; align-items: center; text-align: center;">
<img src="/icons/restaurant-marker.svg" alt="موقعیت رستوران" style="width: 24px; height: 30px; display: block;" />
</div>
`;
setMarkers([
{
position: mapCenter,
title: 'موقعیت رستوران',
icon: L.divIcon({
html: iconHtml,
className: 'custom-restaurant-marker',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
})
}
]);
};
initializeMarkers();
}, [mapCenter]);
const handleZoomChange = (zoom: number) => {
console.log('Zoom level:', zoom);
};
const handleMapClick = (e: L.LeafletMouseEvent) => {
console.log('Map clicked at:', e.latlng);
};
const onCancelOrder = (e: React.MouseEvent | null) => { const onCancelOrder = (e: React.MouseEvent | null) => {
if (!e) return; if (!e) return;
// TODO: Implement cancel order logic
}; };
return ( return (
<div className="fixed inset-0 bg-gray-50"> <div className="fixed inset-0 bg-container flex flex-col">
<div className='flex-1 overflow-y-auto px-4 py-6'>
<div className='max-w-2xl mx-auto flex flex-col gap-6'>
{isLoading ? (
<>
<Skeleton className='h-6 w-48 mx-auto' />
<div className='flex flex-col gap-4'>
<Skeleton className='h-5 w-full' />
<Skeleton className='h-5 w-full' />
<Skeleton className='h-20 w-full' />
<Skeleton className='h-5 w-full' />
</div>
</>
) : orderDetail?.data ? (
<>
<h1 className='text-lg font-semibold text-center'>
{getDeliveryMethodTitle(orderDetail.data.deliveryMethod.method)}
</h1>
<div className="absolute inset-0"> <div className='flex flex-col gap-4'>
<CustomMap <div className='flex justify-between items-center py-3 border-b border-border'>
initialPosition={mapCenter} <span className='text-sm text-muted-foreground'>شماره سفارش</span>
zoom={14} <span className='text-base font-semibold'>#{orderDetail.data.orderNumber}</span>
markers={markers} </div>
onZoomChange={handleZoomChange}
onClick={handleMapClick} {orderDetail.data.tableNumber && (
markerActive={false} <div className='flex justify-between items-center py-3 border-b border-border'>
searchEnabled={false} <span className='text-sm text-muted-foreground'>شماره میز</span>
/> <span className='text-base font-semibold'>{orderDetail.data.tableNumber}</span>
</div>
)}
{orderDetail.data.userAddress && (
<div className='py-3 border-b border-border'>
<div className='text-sm text-muted-foreground mb-1'>آدرس تحویل</div>
<div className='text-sm'>{orderDetail.data.userAddress.address}</div>
</div>
)}
<div className='py-4 bg-card rounded-lg flex flex-col gap-4'>
<div className='px-4'>
<div className='text-xs text-muted-foreground mb-1'>وضعیت سفارش</div>
<div className='text-base font-semibold text-primary'>
{getStatusText(orderDetail.data.status)}
</div>
</div>
<div className='px-4 flex gap-2 items-center'>
<HandPlatter className='text-muted-foreground' size={24} />
<div className='flex-1'>
<AnimatedBar percentage={getStatusPercentage(orderDetail.data.status)} />
</div>
<Package className='text-muted-foreground' size={24} />
</div>
<div className='px-4 flex gap-2 items-center text-muted-foreground'>
<Clock className='stroke-current' size={20} />
<span className='text-sm'>
{new Date(orderDetail.data.createdAt).toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
</div>
<div className='flex justify-between items-center py-4 border-t border-border'>
<span className='text-base font-medium'>مجموع سفارش</span>
<span className='text-lg font-bold text-primary'>
{orderDetail.data.total.toLocaleString('fa-IR')} تومان
</span>
</div>
</div>
</>
) : (
<div className='text-center text-muted-foreground py-12'>
اطلاعات سفارش یافت نشد
</div>
)}
</div>
</div> </div>
<div className="absolute bottom-0 left-0 right-0 z-[1000]"> <div className='p-4 border-t border-border bg-container'>
<div className='relative w-full h-full' > <div className='max-w-2xl mx-auto grid grid-cols-2 gap-4'>
<Button
<AnimatedBottomSheet onClick={() => router.back()}
bgOpacity={0} type='button'>
inDuration={0} بازگشت به منو
blurOpacity={null} </Button>
noBlur <Button
visible={true} type='submit'
showCloseButton={false}
showTitle={false}
title='آدرس'
onClick={toggleCancelModal} onClick={toggleCancelModal}
overrideClassName='pt-1! bg-container!' className='bg-disabled! text-foreground!'
disabled={
orderDetail?.data?.status === 'cancelled' ||
orderDetail?.data?.status === 'delivered' ||
orderDetail?.data?.status === 'delivering'
}
> >
<div className='px-4'> لغو سفارش
<div className='px-4 mt-2 bg-container flex flex-col gap-6'> </Button>
<div className='w-[35px] place-self-center border-2 border-border dark:border-disabled rounded'></div>
<h1 className='text-base font-medium text-center'>ارسال توسط پیک رستوران</h1>
<hr className='border border-border ' />
<div className='text-sm'>
<Skeleton
className='h-6 w-full'
/>
</div>
<div className='text-sm2'>
<Skeleton
className='h-6 w-full'
/>
</div>
<hr className='border border-border' />
<div className='text-sm'>
<Skeleton
className='h-6 w-full'
/>
</div>
<div className='text-sm2 flex gap-2 items-center'>
<Clock className='stroke-primary dark:stroke-neutral-300 size-5' />
<Skeleton
className='h-6 w-full'
/>
</div>
<div className='flex gap-2'>
<HandPlatter className='text-disabled-text ' size={24} />
<AnimatedBar percentage={50} />
<Package className='text-disabled-text' size={24} />
</div>
</div>
<div className='grid grid-cols-2 gap-4 mt-11'>
<Button
onClick={() => router.back()}
type='button'>
بازگشت به منو
</Button>
<Button
type='submit'
onClick={toggleCancelModal}
className='bg-disabled! text-foreground!'
>
لغو سفارش
</Button>
</div>
</div>
</AnimatedBottomSheet>
{/* Logout Modal */}
</div> </div>
</div> </div>
@@ -166,7 +180,7 @@ function OrderTrackingPage() {
!cancelModal && 'pointer-events-none' !cancelModal && 'pointer-events-none'
)}> )}>
<Prompt <Prompt
title={فو سفارش'} title={غو سفارش'}
description={'آیا از درخواست خود اطمینان دارید؟'} description={'آیا از درخواست خود اطمینان دارید؟'}
textConfirm={'بله'} textConfirm={'بله'}
textCancel={'منصرف شدم'} textCancel={'منصرف شدم'}
+16
View File
@@ -18,5 +18,21 @@
"Cash": "پرداخت نقدی", "Cash": "پرداخت نقدی",
"Online": "پرداخت آنلاین", "Online": "پرداخت آنلاین",
"Wallet": "پرداخت با کیف پول" "Wallet": "پرداخت با کیف پول"
},
"status": {
"new": "سفارش جدید",
"pendingPayment": "در انتظار پرداخت",
"paid": "پرداخت شده",
"confirmed": "تایید شده",
"preparing": "در حال آماده‌سازی",
"ready": "آماده تحویل",
"shipped": "ارسال شده",
"delivering": "در حال ارسال",
"completed": "تکمیل شده",
"delivered": "تحویل داده شده",
"canceled": "لغو شده",
"cancelled": "لغو شده",
"failed": "ناموفق",
"refunded": "بازگشت وجه"
} }
} }