feat(orders): update API routes and enhance order models for improved data handling

This commit is contained in:
hosseintaromi 2025-09-26 14:14:04 +03:30
parent aec7db2c19
commit 5cda2bd5d2
3 changed files with 122 additions and 140 deletions

View File

@ -38,11 +38,11 @@ export const API_ROUTES = {
DELETE_PERMISSION: (id: string) => `permissions/${id}`, DELETE_PERMISSION: (id: string) => `permissions/${id}`,
// Product Options APIs (non-admin) // Product Options APIs (non-admin)
GET_PRODUCT_OPTIONS: "api/v1/product-options", GET_PRODUCT_OPTIONS: "products/options",
GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, GET_PRODUCT_OPTION: (id: string) => `products/options/${id}`,
CREATE_PRODUCT_OPTION: "api/v1/product-options", CREATE_PRODUCT_OPTION: "products/options",
UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, UPDATE_PRODUCT_OPTION: (id: string) => `products/options/${id}`,
DELETE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`, DELETE_PRODUCT_OPTION: (id: string) => `products/options/${id}`,
// Categories APIs (non-admin) // Categories APIs (non-admin)
GET_CATEGORIES: "api/v1/products/categories", GET_CATEGORIES: "api/v1/products/categories",
@ -79,7 +79,7 @@ export const API_ROUTES = {
DELETE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`, DELETE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`,
// Landing Hero APIs // Landing Hero APIs
GET_LANDING_HERO: "api/v1/settings/landing/hero", // non-admin GET_LANDING_HERO: "settings/landing/hero", // non-admin
UPDATE_LANDING_HERO: "settings/landing/hero", // admin UPDATE_LANDING_HERO: "settings/landing/hero", // admin
// Discount Codes APIs // Discount Codes APIs

View File

@ -22,25 +22,37 @@ export interface OrderItem {
product_image?: string; product_image?: string;
variant_id?: number; variant_id?: number;
variant_name?: string; variant_name?: string;
product_variant_id?: number;
product_variant_name?: string;
quantity: number; quantity: number;
unit_price: number; unit_price: number;
total_price: number; total_price: number;
discount_amount?: number; discount_amount?: number;
weight?: number;
final_weight?: number;
} }
export interface OrderAddress { export interface OrderAddress {
id: number; id: number;
type: "billing" | "shipping"; type?: "billing" | "shipping";
first_name: string; // legacy fields
last_name: string; first_name?: string;
last_name?: string;
company?: string; company?: string;
address_line_1: string; address_line_1?: string;
address_line_2?: string; address_line_2?: string;
city: string; city?: string;
state: string; state?: string;
postal_code: string; postal_code?: string;
country: string; country?: string;
phone?: string; phone?: string;
// new fields
name?: string;
address?: string;
region?: string;
plaque?: number;
unit?: number;
receiving_address?: string;
} }
export interface OrderPayment { export interface OrderPayment {
@ -65,18 +77,37 @@ export interface Order {
order: { order: {
id: number; id: number;
order_number: string; order_number: string;
customer: OrderCustomer; customer?: OrderCustomer;
status: OrderStatus; status: OrderStatus;
items: OrderItem[]; items: OrderItem[];
billing_address: OrderAddress; billing_address: OrderAddress;
shipping_address: OrderAddress; shipping_address: OrderAddress;
payment: OrderPayment; payment?: OrderPayment;
subtotal: number; // new flat fields from API
tax_amount: number; invoice_id?: number;
shipping_amount: number; user_id?: number;
discount_amount: number; user?: {
total_amount: number; id: number;
final_total: number; phone_number: string;
first_name: string;
last_name: string;
email: string;
national_code?: string;
verified: boolean;
avatar?: string;
};
payment_status?: PaymentStatus;
net_total?: number;
vat_total?: number;
shipping_total?: number;
discount_total?: number;
// legacy totals kept for compatibility
subtotal?: number;
tax_amount?: number;
shipping_amount?: number;
discount_amount?: number;
total_amount?: number;
final_total?: number;
currency: string; currency: string;
notes?: string; notes?: string;
tracking_number?: string; tracking_number?: string;

View File

@ -169,10 +169,7 @@ const OrderDetailPage = () => {
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ آخرین بروزرسانی</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">تاریخ آخرین بروزرسانی</h4>
<p className="text-gray-600 dark:text-gray-400">{order?.updated_at ? formatDate(order.updated_at) : 'نامشخص'}</p> <p className="text-gray-600 dark:text-gray-400">{order?.updated_at ? formatDate(order.updated_at) : 'نامشخص'}</p>
</div> </div>
<div> {/* روش حمل و نقل در داده‌های فعلی وجود ندارد */}
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">روش حمل و نقل</h4>
<p className="text-gray-600 dark:text-gray-400">{order?.shipping_method_id || 'تعریف نشده'}</p>
</div>
{order?.tracking_number && ( {order?.tracking_number && (
<div> <div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">کد رهگیری</h4> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">کد رهگیری</h4>
@ -207,55 +204,72 @@ const OrderDetailPage = () => {
<SectionTitle>محصولات سفارش</SectionTitle> <SectionTitle>محصولات سفارش</SectionTitle>
</div> </div>
</div> </div>
<div className="p-6"> <div className="p-0">
<div className="space-y-4"> {order?.items && order.items.length > 0 ? (
{order?.items && order.items.length > 0 ? order.items.map((item) => ( <div className="divide-y divide-gray-200 dark:divide-gray-700">
<div key={item.id} className="flex items-center gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg"> <div className="grid grid-cols-12 px-6 py-3 text-xs text-gray-500 dark:text-gray-400">
{item.product_image && ( <div className="col-span-5">محصول</div>
<img <div className="col-span-2 text-center">تعداد</div>
src={item.product_image} <div className="col-span-2 text-center">وزن (گرم)</div>
alt={item.product_name} <div className="col-span-1 text-center">قیمت واحد</div>
className="w-16 h-16 object-cover rounded-lg" <div className="col-span-2 text-left">جمع</div>
/>
)}
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-gray-100">
{item.product_name || `محصول شناسه: ${item.product_id}`}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{item.product_variant_name || `واریانت شناسه: ${item.product_variant_id}`}
</p>
<p className="text-xs text-gray-400 dark:text-gray-500">
شناسه آیتم: {item.id}
</p>
<div className="flex items-center gap-4 mt-2">
<span className="text-sm text-gray-600 dark:text-gray-400">
تعداد: {item.quantity}
</span>
<span className="text-sm text-gray-600 dark:text-gray-400">
قیمت واحد: {formatCurrency(item.unit_price || 0)}
</span>
<span className="text-sm text-gray-600 dark:text-gray-400">
وزن: {item.weight || 0} کگ
</span>
{item.final_weight && item.final_weight !== item.weight && (
<span className="text-sm text-gray-600 dark:text-gray-400">
وزن نهایی: {item.final_weight} کگ
</span>
)}
</div>
</div>
<div className="text-right">
<p className="font-medium text-gray-900 dark:text-gray-100">
{formatCurrency(item.total_price || 0)}
</p>
</div>
</div> </div>
)) : ( {order.items.map((item) => {
<p className="text-gray-500 dark:text-gray-400 text-center py-4"> const baseWeight = (item.final_weight ?? item.weight ?? 0) as number;
محصولی در این سفارش یافت نشد const weightGr = Math.round(baseWeight * 1000);
</p> const formatFa = (n: number) => new Intl.NumberFormat('fa-IR').format(n);
)} return (
<div key={item.id} className="grid grid-cols-12 px-6 py-4 items-center">
<div className="col-span-5">
<div className="font-medium text-gray-900 dark:text-gray-100">
{item.product_name || `محصول شناسه: ${item.product_id}`}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{item.product_variant_name || `واریانت شناسه: ${item.product_variant_id}`}
</div>
</div>
<div className="col-span-2 text-center text-sm text-gray-700 dark:text-gray-300">{formatFa(item.quantity || 0)}</div>
<div className="col-span-2 text-center text-sm text-gray-700 dark:text-gray-300">{formatFa(weightGr)}</div>
<div className="col-span-1 text-center text-sm text-gray-700 dark:text-gray-300">{formatCurrency(item.unit_price || 0)}</div>
<div className="col-span-2 text-left font-semibold text-gray-900 dark:text-gray-100">{formatCurrency(item.total_price || 0)}</div>
</div>
);
})}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400 text-center py-6">
محصولی در این سفارش یافت نشد
</p>
)}
</div>
</div>
{/* آدرس‌ها - منتقل شده به زیر محصولات */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-orange-50 to-red-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-100 dark:bg-orange-900 rounded-lg">
<MapPin className="h-5 w-5 text-orange-600 dark:text-orange-300" />
</div>
<SectionTitle>آدرسها</SectionTitle>
</div>
</div>
<div className="p-6 space-y-6">
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">آدرس ارسال</h4>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1 break-words">
<p className="break-words"><strong>نام:</strong> {order?.shipping_address?.name || 'نام نامشخص'}</p>
<p className="break-words"><strong>آدرس:</strong> {order?.shipping_address?.address || 'آدرس نامشخص'}</p>
<p className="break-words"><strong>شهر:</strong> {order?.shipping_address?.city || 'شهر نامشخص'}, <strong>استان:</strong> {order?.shipping_address?.state || 'استان نامشخص'}</p>
<p className="break-words"><strong>کشور:</strong> {order?.shipping_address?.country || 'کشور نامشخص'}</p>
<p className="break-words"><strong>منطقه:</strong> {order?.shipping_address?.region || 'منطقه نامشخص'}</p>
<p className="break-words"><strong>کد پستی:</strong> {order?.shipping_address?.postal_code || 'نامشخص'}</p>
{order?.shipping_address?.plaque && (
<p className="break-words"><strong>پلاک:</strong> {order.shipping_address.plaque}, <strong>واحد:</strong> {order.shipping_address.unit || 'ندارد'}</p>
)}
{order?.shipping_address?.receiving_address && (
<p className="break-words"><strong>آدرس تحویل:</strong> {order.shipping_address.receiving_address}</p>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -308,72 +322,9 @@ const OrderDetailPage = () => {
)} )}
</div> </div>
</div> </div>
{/* اطلاعات مشتری */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
<User className="h-5 w-5 text-purple-600 dark:text-purple-300" />
</div>
<SectionTitle>اطلاعات مشتری</SectionTitle>
</div>
</div>
<div className="p-6">
{order?.customer ? (
<div className="space-y-4">
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">نام</h4>
<p className="text-gray-600 dark:text-gray-400">
{order.customer.first_name || 'نامشخص'} {order.customer.last_name || ''}
</p>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400">{order.customer.email || 'ایمیل نامشخص'}</p>
</div>
{order.customer.phone && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400" dir="ltr">{order.customer.phone}</p>
</div>
)}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400">اطلاعات مشتری در دسترس نیست</p>
)}
</div>
</div>
{/* آدرس‌ها */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-orange-50 to-red-50 dark:from-gray-700 dark:to-gray-600 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-100 dark:bg-orange-900 rounded-lg">
<MapPin className="h-5 w-5 text-orange-600 dark:text-orange-300" />
</div>
<SectionTitle>آدرسها</SectionTitle>
</div>
</div>
<div className="p-6 space-y-6">
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">آدرس ارسال</h4>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<p><strong>نام:</strong> {order?.shipping_address?.name || 'نام نامشخص'}</p>
<p><strong>آدرس:</strong> {order?.shipping_address?.address || 'آدرس نامشخص'}</p>
<p><strong>شهر:</strong> {order?.shipping_address?.city || 'شهر نامشخص'}, <strong>استان:</strong> {order?.shipping_address?.state || 'استان نامشخص'}</p>
<p><strong>کشور:</strong> {order?.shipping_address?.country || 'کشور نامشخص'}</p>
<p><strong>منطقه:</strong> {order?.shipping_address?.region || 'منطقه نامشخص'}</p>
<p><strong>کد پستی:</strong> {order?.shipping_address?.postal_code || 'نامشخص'}</p>
{order?.shipping_address?.plaque && (
<p><strong>پلاک:</strong> {order.shipping_address.plaque}, <strong>واحد:</strong> {order.shipping_address.unit || 'ندارد'}</p>
)}
{order?.shipping_address?.receiving_address && (
<p><strong>آدرس تحویل:</strong> {order.shipping_address.receiving_address}</p>
)}
</div>
</div>
</div>
</div>
{/* اطلاعات پرداخت */} {/* اطلاعات پرداخت */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@ -401,7 +352,7 @@ const OrderDetailPage = () => {
{(order?.discount_total || 0) > 0 && ( {(order?.discount_total || 0) > 0 && (
<div className="flex justify-between text-green-600 dark:text-green-400"> <div className="flex justify-between text-green-600 dark:text-green-400">
<span>تخفیف</span> <span>تخفیف</span>
<span className="font-medium">-{formatCurrency(order.discount_total)}</span> <span className="font-medium">-{formatCurrency(order?.discount_total || 0)}</span>
</div> </div>
)} )}
<hr className="border-gray-200 dark:border-gray-700" /> <hr className="border-gray-200 dark:border-gray-700" />