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}`,
// Product Options APIs (non-admin)
GET_PRODUCT_OPTIONS: "api/v1/product-options",
GET_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
CREATE_PRODUCT_OPTION: "api/v1/product-options",
UPDATE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
DELETE_PRODUCT_OPTION: (id: string) => `api/v1/product-options/${id}`,
GET_PRODUCT_OPTIONS: "products/options",
GET_PRODUCT_OPTION: (id: string) => `products/options/${id}`,
CREATE_PRODUCT_OPTION: "products/options",
UPDATE_PRODUCT_OPTION: (id: string) => `products/options/${id}`,
DELETE_PRODUCT_OPTION: (id: string) => `products/options/${id}`,
// Categories APIs (non-admin)
GET_CATEGORIES: "api/v1/products/categories",
@ -79,7 +79,7 @@ export const API_ROUTES = {
DELETE_IMAGE: (imageId: string) => `api/v1/products/images/${imageId}`,
// 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
// Discount Codes APIs

View File

@ -22,25 +22,37 @@ export interface OrderItem {
product_image?: string;
variant_id?: number;
variant_name?: string;
product_variant_id?: number;
product_variant_name?: string;
quantity: number;
unit_price: number;
total_price: number;
discount_amount?: number;
weight?: number;
final_weight?: number;
}
export interface OrderAddress {
id: number;
type: "billing" | "shipping";
first_name: string;
last_name: string;
type?: "billing" | "shipping";
// legacy fields
first_name?: string;
last_name?: string;
company?: string;
address_line_1: string;
address_line_1?: string;
address_line_2?: string;
city: string;
state: string;
postal_code: string;
country: string;
city?: string;
state?: string;
postal_code?: string;
country?: string;
phone?: string;
// new fields
name?: string;
address?: string;
region?: string;
plaque?: number;
unit?: number;
receiving_address?: string;
}
export interface OrderPayment {
@ -65,18 +77,37 @@ export interface Order {
order: {
id: number;
order_number: string;
customer: OrderCustomer;
customer?: OrderCustomer;
status: OrderStatus;
items: OrderItem[];
billing_address: OrderAddress;
shipping_address: OrderAddress;
payment: OrderPayment;
subtotal: number;
tax_amount: number;
shipping_amount: number;
discount_amount: number;
total_amount: number;
final_total: number;
payment?: OrderPayment;
// new flat fields from API
invoice_id?: number;
user_id?: number;
user?: {
id: 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;
notes?: 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>
<p className="text-gray-600 dark:text-gray-400">{order?.updated_at ? formatDate(order.updated_at) : 'نامشخص'}</p>
</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 && (
<div>
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-2">کد رهگیری</h4>
@ -207,55 +204,72 @@ const OrderDetailPage = () => {
<SectionTitle>محصولات سفارش</SectionTitle>
</div>
</div>
<div className="p-6">
<div className="space-y-4">
{order?.items && order.items.length > 0 ? order.items.map((item) => (
<div key={item.id} className="flex items-center gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
{item.product_image && (
<img
src={item.product_image}
alt={item.product_name}
className="w-16 h-16 object-cover rounded-lg"
/>
)}
<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 className="p-0">
{order?.items && order.items.length > 0 ? (
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="grid grid-cols-12 px-6 py-3 text-xs text-gray-500 dark:text-gray-400">
<div className="col-span-5">محصول</div>
<div className="col-span-2 text-center">تعداد</div>
<div className="col-span-2 text-center">وزن (گرم)</div>
<div className="col-span-1 text-center">قیمت واحد</div>
<div className="col-span-2 text-left">جمع</div>
</div>
)) : (
<p className="text-gray-500 dark:text-gray-400 text-center py-4">
محصولی در این سفارش یافت نشد
</p>
)}
{order.items.map((item) => {
const baseWeight = (item.final_weight ?? item.weight ?? 0) as number;
const weightGr = Math.round(baseWeight * 1000);
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>
@ -308,72 +322,9 @@ const OrderDetailPage = () => {
)}
</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">
@ -401,7 +352,7 @@ const OrderDetailPage = () => {
{(order?.discount_total || 0) > 0 && (
<div className="flex justify-between text-green-600 dark:text-green-400">
<span>تخفیف</span>
<span className="font-medium">-{formatCurrency(order.discount_total)}</span>
<span className="font-medium">-{formatCurrency(order?.discount_total || 0)}</span>
</div>
)}
<hr className="border-gray-200 dark:border-gray-700" />