detail of order
This commit is contained in:
@@ -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,
|
||||
removeCoupon,
|
||||
setTableNumber,
|
||||
getOrderDetail,
|
||||
cancelOrder,
|
||||
} from "../service/OrderService";
|
||||
|
||||
export const useGetShipmentMethod = () => {
|
||||
@@ -76,3 +78,17 @@ export const useSetTableNumber = () => {
|
||||
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,
|
||||
PaymentMethodsResponse,
|
||||
CreateOrderResponse,
|
||||
OrderDetailResponse,
|
||||
} from "../types/Types";
|
||||
|
||||
export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => {
|
||||
@@ -59,3 +60,15 @@ export const setTableNumber = async (number: number) => {
|
||||
});
|
||||
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 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>;
|
||||
|
||||
@@ -1,163 +1,177 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import Button from '@/components/button/PrimaryButton';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { MarkerData } from '@/app/[name]/(Profile)/profile/address/types/Types';
|
||||
import { HandPlatter, Package } from 'lucide-react';
|
||||
import AnimatedBar from '@/components/utils/AnimatedBar';
|
||||
import { Clock } from 'iconsax-react';
|
||||
import clsx from 'clsx';
|
||||
import useToggle from '@/hooks/helpers/useToggle';
|
||||
import Prompt from '@/components/utils/Prompt';
|
||||
import { useGetOrderDetail } from '../../checkout/hooks/useOrderData';
|
||||
import ordersTranslations from '@/locales/fa/orders.json';
|
||||
|
||||
const CustomMap = dynamic(
|
||||
() => import('@/components/map/CustomMap'),
|
||||
{ ssr: false } // Leaflet requires browser APIs
|
||||
);
|
||||
const getDeliveryMethodTitle = (method: string) => {
|
||||
switch (method) {
|
||||
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() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
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 { state: cancelModal, toggle: toggleCancelModal } = useToggle();
|
||||
|
||||
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 { data: orderDetail, isLoading } = useGetOrderDetail(id as string);
|
||||
|
||||
const onCancelOrder = (e: React.MouseEvent | null) => {
|
||||
if (!e) return;
|
||||
|
||||
// TODO: Implement cancel order logic
|
||||
};
|
||||
|
||||
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">
|
||||
<CustomMap
|
||||
initialPosition={mapCenter}
|
||||
zoom={14}
|
||||
markers={markers}
|
||||
onZoomChange={handleZoomChange}
|
||||
onClick={handleMapClick}
|
||||
markerActive={false}
|
||||
searchEnabled={false}
|
||||
/>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex justify-between items-center py-3 border-b border-border'>
|
||||
<span className='text-sm text-muted-foreground'>شماره سفارش</span>
|
||||
<span className='text-base font-semibold'>#{orderDetail.data.orderNumber}</span>
|
||||
</div>
|
||||
|
||||
{orderDetail.data.tableNumber && (
|
||||
<div className='flex justify-between items-center py-3 border-b border-border'>
|
||||
<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 className="absolute bottom-0 left-0 right-0 z-[1000]">
|
||||
<div className='relative w-full h-full' >
|
||||
|
||||
<AnimatedBottomSheet
|
||||
bgOpacity={0}
|
||||
inDuration={0}
|
||||
blurOpacity={null}
|
||||
noBlur
|
||||
visible={true}
|
||||
showCloseButton={false}
|
||||
showTitle={false}
|
||||
title='آدرس'
|
||||
<div className='p-4 border-t border-border bg-container'>
|
||||
<div className='max-w-2xl mx-auto grid grid-cols-2 gap-4'>
|
||||
<Button
|
||||
onClick={() => router.back()}
|
||||
type='button'>
|
||||
بازگشت به منو
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
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'>
|
||||
<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 */}
|
||||
لغو سفارش
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +180,7 @@ function OrderTrackingPage() {
|
||||
!cancelModal && 'pointer-events-none'
|
||||
)}>
|
||||
<Prompt
|
||||
title={'لفو سفارش'}
|
||||
title={'لغو سفارش'}
|
||||
description={'آیا از درخواست خود اطمینان دارید؟'}
|
||||
textConfirm={'بله'}
|
||||
textCancel={'منصرف شدم'}
|
||||
|
||||
@@ -18,5 +18,21 @@
|
||||
"Cash": "پرداخت نقدی",
|
||||
"Online": "پرداخت آنلاین",
|
||||
"Wallet": "پرداخت با کیف پول"
|
||||
},
|
||||
"status": {
|
||||
"new": "سفارش جدید",
|
||||
"pendingPayment": "در انتظار پرداخت",
|
||||
"paid": "پرداخت شده",
|
||||
"confirmed": "تایید شده",
|
||||
"preparing": "در حال آمادهسازی",
|
||||
"ready": "آماده تحویل",
|
||||
"shipped": "ارسال شده",
|
||||
"delivering": "در حال ارسال",
|
||||
"completed": "تکمیل شده",
|
||||
"delivered": "تحویل داده شده",
|
||||
"canceled": "لغو شده",
|
||||
"cancelled": "لغو شده",
|
||||
"failed": "ناموفق",
|
||||
"refunded": "بازگشت وجه"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user