feat(notifications): add total count to admin notifications response and update pagination logic

- Introduced an optional `total` field in the `AdminNotificationsResponse` interface to provide the total number of notifications.
- Updated pagination logic in `AdminNotificationsListPage` to utilize the new `total` field for accurate page calculations.
- Adjusted the pagination component to reflect the total number of items based on the presence of the `total` field.

These changes enhance the functionality and user experience of the admin notifications feature.
This commit is contained in:
hosseintaromi 2026-02-19 10:18:02 +03:30
parent 481e7e748a
commit 165968f9f9
13 changed files with 613 additions and 225 deletions

46
src/constant/enums.ts Normal file
View File

@ -0,0 +1,46 @@
export const PAYMENT_TYPE_OPTIONS: { value: string; label: string }[] = [
{ value: '', label: 'همه' },
{ value: 'bank-topup', label: 'افزایش موجودی کیف پول' },
{ value: 'card-to-card', label: 'پرداخت به روش کارت به کارت' },
{ value: 'debit-rial-wallet', label: 'پرداخت از کیف ریالی' },
{ value: 'debit-gold18k-wallet', label: 'پرداخت از کیف طلا' },
];
export const PAYMENT_STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: '', label: 'همه' },
{ value: 'pending', label: 'در انتظار پرداخت' },
{ value: 'paid', label: 'پرداخت شده' },
{ value: 'failed', label: 'ناموفق' },
{ value: 'refunded', label: 'مرجوع شده' },
{ value: 'cancelled', label: 'لغو شده' },
];
export const SHIPMENT_STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: '', label: 'همه' },
{ value: 'pending', label: 'در انتظار' },
{ value: 'confirmed', label: 'تأیید شده' },
{ value: 'processing', label: 'در حال پردازش' },
{ value: 'shipped', label: 'ارسال شده' },
{ value: 'delivered', label: 'تحویل داده شده' },
{ value: 'refunded', label: 'مرجوع شده' },
{ value: 'cancelled', label: 'لغو شده' },
];
const PAYMENT_TYPE_LABELS: Record<string, string> = {
'bank-topup': 'افزایش موجودی کیف پول',
'card-to-card': 'پرداخت به روش کارت به کارت',
'debit-rial-wallet': 'پرداخت از کیف ریالی',
'debit-gold18k-wallet': 'پرداخت از کیف طلا',
'credit-card': 'پرداخت بانکی',
'debit-card': 'کارت بانکی',
'bank-transfer': 'حواله بانکی',
'cash-on-delivery': 'پرداخت در محل',
wallet: 'کیف پول',
unknown: 'نامشخص',
};
export const getPaymentTypeLabel = (type?: string): string => {
if (!type) return '';
const key = type.toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-');
return PAYMENT_TYPE_LABELS[key] || type;
};

View File

@ -22,6 +22,7 @@ export interface AdminNotificationsResponse {
unread_count: number;
offset: number;
limit: number;
total?: number;
}
export interface AdminNotificationsUnreadResponse {

View File

@ -19,7 +19,14 @@ const AdminNotificationsListPage = () => {
const notifications = data?.notifications || [];
const unreadCount = data?.unread_count || 0;
const totalPages = Math.ceil((data?.notifications?.length || 0) / filters.limit);
const hasTotal = data?.total !== undefined && data?.total !== null;
const totalCount = hasTotal ? (data!.total ?? 0) : notifications.length;
const currentPage = Math.floor(filters.offset / filters.limit) + 1;
const totalPages = hasTotal
? Math.max(1, Math.ceil((data!.total ?? 0) / filters.limit))
: notifications.length >= filters.limit
? currentPage + 1
: currentPage;
const filteredNotifications = filterType === 'unread'
? notifications.filter(n => !n.is_read)
@ -234,11 +241,11 @@ const AdminNotificationsListPage = () => {
{totalPages > 1 && (
<div className="mt-6">
<Pagination
currentPage={Math.floor(filters.offset / filters.limit) + 1}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
itemsPerPage={filters.limit}
totalItems={notifications.length}
totalItems={hasTotal ? (data?.total ?? 0) : totalCount}
/>
</div>
)}

View File

@ -147,7 +147,7 @@ const ContactUsListPage: React.FC = () => {
)}
</div>
{messages.length > 0 && totalPages > 1 && (
{messages.length > 0 && total > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}

View File

@ -22,6 +22,7 @@ import {
import { englishToPersian } from '@/utils/numberUtils';
import { API_GATE_WAY } from '@/constant/routes';
import { formatCurrency, formatDateTime } from '@/utils/formatters';
import { getPaymentTypeLabel } from '@/constant/enums';
const resolveImageUrl = (imageUrl?: string): string => {
if (!imageUrl) return '';
@ -57,23 +58,6 @@ const getStatusText = (status: OrderStatus) => {
};
const formatPaymentType = (type?: string) => {
if (!type) return '';
const key = type.toLowerCase().replace(/\s+/g, '-').replace(/_/g, '-');
const mapping: Record<string, string> = {
'bank-topup': 'افزایش موجودی کیف پول',
'card-to-card': 'پرداخت به روش کارت به کارت',
'debit-rial-wallet': 'پرداخت از کیف ریالی',
'debit-gold18k-wallet': 'پرداخت از کیف طلا',
'credit-card': 'پرداخت بانکی',
'debit-card': 'کارت بانکی',
'bank-transfer': 'حواله بانکی',
'cash-on-delivery': 'پرداخت در محل',
'wallet': 'کیف پول',
};
return mapping[key] || type;
};
const OrderDetailPage = () => {
const navigate = useNavigate();
const { id } = useParams();
@ -552,7 +536,7 @@ const OrderDetailPage = () => {
{Array.isArray((data as any)?.payments) && (data as any)?.payments.length > 0 && (
<div className="flex items-center justify-between text-base gap-2">
<span className="text-gray-700 dark:text-gray-200 shrink-0">روش پرداخت</span>
<span className="text-gray-900 dark:text-gray-100 break-words text-left">{formatPaymentType((data as any).payments[0].payment_type)}</span>
<span className="text-gray-900 dark:text-gray-100 break-words text-left">{getPaymentTypeLabel((data as any).payments[0].payment_type)}</span>
</div>
)}
{order?.invoice_id && (

View File

@ -28,6 +28,7 @@ import { FiltersSection } from '@/components/common/FiltersSection';
import { EmptyState } from '@/components/common/EmptyState';
import { ActionButtons } from '@/components/common/ActionButtons';
import { StatusBadge } from '@/components/ui/StatusBadge';
import { PAYMENT_STATUS_OPTIONS } from '@/constant/enums';
import { formatCurrency, formatDate } from '@/utils/formatters';
@ -327,12 +328,9 @@ const OrdersListPage = () => {
onChange={(e) => setFilters(prev => ({ ...prev, payment_status: e.target.value as any || undefined, page: 1 }))}
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
>
<option value="">همه وضعیتهای پرداخت</option>
<option value="pending">در انتظار پرداخت</option>
<option value="paid">پرداخت شده</option>
<option value="failed">پرداخت ناموفق</option>
<option value="refunded">بازپرداخت شده</option>
<option value="cancelled">لغو شده</option>
{PAYMENT_STATUS_OPTIONS.map(o => (
<option key={o.value || 'all'} value={o.value}>{o.value === '' ? 'همه وضعیت‌های پرداخت' : o.label}</option>
))}
</select>
</div>

View File

@ -12,17 +12,7 @@ import { useIPGStatus, useUpdateIPGStatus } from '../core/_hooks';
import { IPGStatus, IPG_LABELS } from '../core/_models';
import { usePaymentMethodsReport, usePaymentTransactionsReport } from '@/pages/reports/payment-statistics/core/_hooks';
import { TableColumn } from '@/types';
const getPaymentTypeLabel = (type: string): string => {
const labels: Record<string, string> = {
'bank-topup': 'افزایش موجودی کیف پول',
'card-to-card': 'پرداخت به روش کارت به کارت',
'debit-rial-wallet': 'پرداخت از کیف ریالی',
'debit-gold18k-wallet': 'پرداخت از کیف طلا',
unknown: 'نامشخص',
};
return labels[type] || type;
};
import { getPaymentTypeLabel } from '@/constant/enums';
const IPGListPage = () => {
const { data, isLoading, error } = useIPGStatus();

View File

@ -15,22 +15,12 @@ import { formatCurrency, formatDateTime } from '@/utils/formatters';
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
import { useSearchUsers, useUsers } from '@/pages/users-admin/core/_hooks';
import { UserFilters } from '@/pages/users-admin/core/_models';
import { PAYMENT_TYPE_OPTIONS, getPaymentTypeLabel } from '@/constant/enums';
const formatPercentage = (value: number) => {
return formatWithThousands(value.toFixed(2)) + '%';
};
const getPaymentTypeLabel = (type: string): string => {
const labels: Record<string, string> = {
'bank-topup': 'افزایش موجودی کیف پول',
'card-to-card': 'پرداخت به روش کارت به کارت',
'debit-rial-wallet': 'پرداخت از کیف ریالی',
'debit-gold18k-wallet': 'پرداخت از کیف طلا',
};
return labels[type] || type;
};
const PaymentMethodsReportPage = () => {
const [filters, setFilters] = useState<PaymentMethodsFilters>({
limit: 50,
@ -271,11 +261,9 @@ const PaymentMethodsReportPage = () => {
onChange={(e) => handleTempFilterChange('payment_type', e.target.value || undefined)}
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
>
<option value="">همه</option>
<option value="bank-topup">افزایش موجودی کیف پول</option>
<option value="card-to-card">پرداخت به روش کارت به کارت</option>
<option value="debit-rial-wallet">پرداخت از کیف ریالی</option>
<option value="debit-gold18k-wallet">پرداخت از کیف طلا</option>
{PAYMENT_TYPE_OPTIONS.map(o => (
<option key={o.value || 'all'} value={o.value}>{o.label}</option>
))}
</select>
</div>

View File

@ -10,7 +10,7 @@ export const getSalesSummaryReport = async (
queryParams.from = filters.from;
queryParams.to = filters.to;
if (filters.status?.length) queryParams.status = Array.isArray(filters.status) ? filters.status.join(',') : filters.status;
if (filters.status?.length) queryParams.status = filters.status.join(",");
if (filters.product_sku) queryParams.product_sku = filters.product_sku;
if (filters.product_name) queryParams.product_name = filters.product_name;
if (filters.min_quantity !== undefined) queryParams.min_quantity = filters.min_quantity;

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useRef, useEffect } from 'react';
import { useSalesSummaryReport } from '../core/_hooks';
import { SalesSummaryFilters } from '../core/_models';
import { Button } from '@/components/ui/Button';
@ -8,12 +8,86 @@ import { TableColumn } from '@/types';
import { JalaliDateTimePicker } from '@/components/ui/JalaliDateTimePicker';
import { PageContainer, PageTitle } from '@/components/ui/Typography';
import { Pagination } from '@/components/ui/Pagination';
import { Filter, TrendingUp, DollarSign, Package, ShoppingCart, X, Image as ImageIcon } from 'lucide-react';
import { Filter, TrendingUp, DollarSign, Package, ShoppingCart, X, Info, ChevronDown } from 'lucide-react';
import { formatWithThousands, parseFormattedNumber, persianToEnglish } from '@/utils/numberUtils';
import { formatCurrency } from '@/utils/formatters';
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
import DateObject from 'react-date-object';
const ORDER_STATUS_OPTIONS = [
{ value: 'delivered', label: 'تحویل شده' },
{ value: 'pending', label: 'در انتظار' },
{ value: 'confirmed', label: 'تأیید شده' },
{ value: 'processing', label: 'در حال پردازش' },
{ value: 'shipped', label: 'ارسال شده' },
{ value: 'refunded', label: 'مرجوع شده' },
{ value: 'cancelled', label: 'لغو شده' },
];
const LIMIT_OPTIONS = [20, 50, 100];
const StatusMultiSelect = ({
label,
options,
value,
onChange,
placeholder,
}: {
label: string;
options: { value: string; label: string }[];
value: string[];
onChange: (v: string[]) => void;
placeholder?: string;
}) => {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const fn = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setIsOpen(false);
};
document.addEventListener("mousedown", fn);
return () => document.removeEventListener("mousedown", fn);
}, []);
const toggle = (val: string) => {
if (value.includes(val)) onChange(value.filter((s) => s !== val));
else onChange([...value, val]);
};
const selectedLabels = options.filter((o) => value.includes(o.value)).map((o) => o.label);
return (
<div ref={ref} className="relative">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{label}</label>
<button
type="button"
onClick={() => setIsOpen((o) => !o)}
className="input w-full flex items-center justify-between gap-2 text-right"
>
<ChevronDown className={`h-4 w-4 flex-shrink-0 transition-transform ${isOpen ? "rotate-180" : ""}`} />
<span className={selectedLabels.length ? "text-gray-900 dark:text-gray-100" : "text-gray-500 dark:text-gray-400"}>
{selectedLabels.length ? selectedLabels.join("، ") : placeholder}
</span>
</button>
{isOpen && (
<div className="absolute z-50 w-full mt-1 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg">
{options.map((o) => (
<label
key={o.value}
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
>
<input
type="checkbox"
checked={value.includes(o.value)}
onChange={() => toggle(o.value)}
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
<span className="text-sm text-gray-900 dark:text-gray-100">{o.label}</span>
</label>
))}
</div>
)}
</div>
);
};
const toIsoString = (date: DateObject): string => {
try {
const g = date.convert(undefined);
@ -351,11 +425,41 @@ const SalesSummaryReportPage = () => {
numeric
/>
</div>
<StatusMultiSelect
label="وضعیت سفارش"
options={ORDER_STATUS_OPTIONS}
value={tempFilters.status || []}
onChange={(v) => handleTempFilterChange('status', v.length ? v : undefined)}
placeholder="خالی = فقط تحویل شده"
/>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
تعداد در هر صفحه
</label>
<select
value={tempFilters.limit ?? 50}
onChange={(e) => handleTempFilterChange('limit', Number(e.target.value))}
className="input w-full"
>
{LIMIT_OPTIONS.map((n) => (
<option key={n} value={n}>{n}</option>
))}
</select>
</div>
</div>
</div>
{data && (
<>
<div className="flex items-center gap-2 mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
<p className="text-sm text-blue-800 dark:text-blue-200">
اعداد خلاصه بدون فیلتر هستند؛ فقط لیست محصولات بر اساس فیلترها محدود میشود.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
@ -385,6 +489,20 @@ const SalesSummaryReportPage = () => {
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-100 dark:bg-indigo-900 rounded-lg">
<Package className="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">وزن با اجرت</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatWithThousands(data.total_final_weight, 2)} گرم
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
@ -412,6 +530,48 @@ const SalesSummaryReportPage = () => {
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-100 dark:bg-amber-900 rounded-lg">
<DollarSign className="h-5 w-5 text-amber-600 dark:text-amber-400" />
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">میانگین قیمت فروش</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatCurrency(data.average_price)}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-teal-100 dark:bg-teal-900 rounded-lg">
<Package className="h-5 w-5 text-teal-600 dark:text-teal-400" />
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">میانگین وزن</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatWithThousands(data.average_weight, 2)} گرم
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-100 dark:bg-red-900 rounded-lg">
<TrendingUp className="h-5 w-5 text-red-600 dark:text-red-400" />
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">مجموع تخفیف</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatCurrency(data.total_discount)}
</p>
</div>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6">

View File

@ -9,11 +9,25 @@ export interface ShipmentsByMethodFilters {
date_range?: DateRange;
customer_name?: string;
user_id?: number;
status?: 'pending' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
phone_number?: string;
order_id?: number;
order_number?: string;
status?: 'pending' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'refunded' | 'cancelled';
payment_status?: 'pending' | 'paid' | 'failed' | 'refunded' | 'cancelled';
payment_type?: string;
min_shipping_cost?: number;
max_shipping_cost?: number;
min_order_amount?: number;
max_order_amount?: number;
city?: string;
state?: string;
region?: string;
delivery_date?: string;
delivery_from_hour?: number;
delivery_to_hour?: number;
group_by_method?: boolean;
sort_by?: 'created_at' | 'shipping_cost' | 'order_amount' | 'delivery_date';
sort_order?: 'asc' | 'desc';
limit: number;
offset: number;
}

View File

@ -12,21 +12,39 @@ export const getShipmentsByMethodReport = async (
if (filters.shipping_method_code)
queryParams.shipping_method_code = filters.shipping_method_code;
if (filters.shipping_method_id)
if (filters.shipping_method_id != null)
queryParams.shipping_method_id = filters.shipping_method_id;
if (filters.user_id) queryParams.user_id = filters.user_id;
if (filters.user_id != null) queryParams.user_id = filters.user_id;
if (filters.phone_number) queryParams.phone_number = filters.phone_number;
if (filters.customer_name) queryParams.customer_name = filters.customer_name;
if (filters.order_id != null) queryParams.order_id = filters.order_id;
if (filters.order_number) queryParams.order_number = filters.order_number;
if (filters.status) queryParams.status = filters.status;
if (filters.payment_status) queryParams.payment_status = filters.payment_status;
if (filters.min_shipping_cost)
if (filters.payment_type) queryParams.payment_type = filters.payment_type;
if (filters.min_shipping_cost != null)
queryParams.min_shipping_cost = filters.min_shipping_cost;
if (filters.max_shipping_cost)
if (filters.max_shipping_cost != null)
queryParams.max_shipping_cost = filters.max_shipping_cost;
if (filters.min_order_amount != null)
queryParams.min_order_amount = filters.min_order_amount;
if (filters.max_order_amount != null)
queryParams.max_order_amount = filters.max_order_amount;
if (filters.city) queryParams.city = filters.city;
if (filters.state) queryParams.state = filters.state;
if (filters.region) queryParams.region = filters.region;
if (filters.delivery_date) queryParams.delivery_date = filters.delivery_date;
if (filters.delivery_from_hour != null)
queryParams.delivery_from_hour = filters.delivery_from_hour;
if (filters.delivery_to_hour != null)
queryParams.delivery_to_hour = filters.delivery_to_hour;
if (filters.date_range?.from) queryParams.from_date = filters.date_range.from;
if (filters.date_range?.to) queryParams.to_date = filters.date_range.to;
if (typeof filters.group_by_method === "boolean") {
queryParams.group_by_method = filters.group_by_method ? "true" : "false";
}
if (filters.sort_by) queryParams.sort_by = filters.sort_by;
if (filters.sort_order) queryParams.sort_order = filters.sort_order;
if (typeof filters.limit === "number") queryParams.limit = filters.limit;
if (typeof filters.offset === "number") queryParams.offset = filters.offset;

View File

@ -4,23 +4,41 @@ import { useShipmentsByMethodReport } from '../core/_hooks';
import { ShipmentsByMethodFilters } from '../core/_models';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { Table } from '@/components/ui/Table';
import { TableColumn } from '@/types';
import { JalaliDateTimePicker } from '@/components/ui/JalaliDateTimePicker';
import { PageContainer, PageTitle } from '@/components/ui/Typography';
import { Pagination } from '@/components/ui/Pagination';
import { Filter, Truck, DollarSign, Package, Users, Clock, X } from 'lucide-react';
import { formatWithThousands, parseFormattedNumber, persianToEnglish } from '@/utils/numberUtils';
import { Filter, Truck, DollarSign, Package, Clock, X, ChevronDown, ChevronUp } from 'lucide-react';
import { formatWithThousands, persianToEnglish } from '@/utils/numberUtils';
import { formatCurrency, formatDateTime } from '@/utils/formatters';
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
import { PAYMENT_TYPE_OPTIONS, PAYMENT_STATUS_OPTIONS, SHIPMENT_STATUS_OPTIONS } from '@/constant/enums';
import { useShippingMethods } from '@/pages/shipping-methods/core/_hooks';
const formatWeight = (weight: number) => {
return formatWithThousands(weight) + ' گرم';
};
const SORT_OPTIONS = [
{ value: 'created_at', label: 'تاریخ ایجاد' },
{ value: 'shipping_cost', label: 'هزینه ارسال' },
{ value: 'order_amount', label: 'مبلغ سفارش' },
{ value: 'delivery_date', label: 'تاریخ تحویل' },
];
const getDefaultFilters = (initialShippingMethodId?: number, initialShippingMethodCode?: string): ShipmentsByMethodFilters => ({
limit: 50,
offset: 0,
group_by_method: false,
sort_order: 'desc',
...(initialShippingMethodId ? { shipping_method_id: initialShippingMethodId } : {}),
...(initialShippingMethodCode ? { shipping_method_code: initialShippingMethodCode } : {}),
});
const ShipmentsByMethodReportPage = () => {
const [searchParams] = useSearchParams();
const { data: shippingMethods = [] } = useShippingMethods();
const initialShippingMethodId = useMemo(() => {
const value = searchParams.get('shipping_method_id');
if (!value) return undefined;
@ -32,107 +50,65 @@ const ShipmentsByMethodReportPage = () => {
return value || undefined;
}, [searchParams]);
const [filters, setFilters] = useState<ShipmentsByMethodFilters>({
limit: 50,
offset: 0,
group_by_method: false,
...(initialShippingMethodId ? { shipping_method_id: initialShippingMethodId } : {}),
...(initialShippingMethodCode ? { shipping_method_code: initialShippingMethodCode } : {}),
});
const defaultFilters = useMemo(
() => getDefaultFilters(initialShippingMethodId, initialShippingMethodCode),
[initialShippingMethodId, initialShippingMethodCode]
);
const [filters, setFilters] = useState<ShipmentsByMethodFilters>(defaultFilters);
const [tempFilters, setTempFilters] = useState<ShipmentsByMethodFilters>(defaultFilters);
const [showFilters, setShowFilters] = useState(false);
const { data, isLoading, error } = useShipmentsByMethodReport(filters);
const handleFilterChange = (key: keyof ShipmentsByMethodFilters, value: any) => {
setFilters(prev => ({
const handleTempFilterChange = (key: keyof ShipmentsByMethodFilters, value: any) => {
setTempFilters(prev => ({ ...prev, [key]: value }));
};
const handleDateChange = (from?: string, to?: string) => {
setTempFilters(prev => ({
...prev,
[key]: value,
offset: 0,
date_range: from || to ? { from, to } : undefined,
}));
};
const handleDateRangeChange = (from: string | undefined, to: string | undefined) => {
setFilters(prev => ({
...prev,
date_range: {
from,
to,
},
offset: 0,
}));
};
const handleNumericFilterChange = (key: 'shipping_method_id' | 'user_id' | 'min_shipping_cost' | 'max_shipping_cost', raw: string) => {
const handleNumericFilterChange = (
key: 'shipping_method_id' | 'user_id' | 'order_id' | 'min_shipping_cost' | 'max_shipping_cost' | 'min_order_amount' | 'max_order_amount' | 'delivery_from_hour' | 'delivery_to_hour',
raw: string
) => {
const converted = persianToEnglish(raw).replace(/[^\d]/g, '');
const numeric = converted ? Number(converted) : undefined;
handleFilterChange(key, numeric);
handleTempFilterChange(key, numeric);
};
const handleApplyFilters = () => {
setFilters({ ...tempFilters, offset: 0 });
};
const handlePageChange = (page: number) => {
setFilters(prev => ({
...prev,
offset: (page - 1) * prev.limit,
}));
setFilters(prev => ({ ...prev, offset: (page - 1) * prev.limit }));
};
const handleClearFilters = () => {
setFilters({
limit: 50,
offset: 0,
group_by_method: false,
});
const cleared = getDefaultFilters(initialShippingMethodId, initialShippingMethodCode);
setTempFilters(cleared);
setFilters(cleared);
};
const columns: TableColumn[] = [
{
key: 'order_number',
label: 'شماره سفارش',
align: 'right',
},
{
key: 'customer_name',
label: 'نام مشتری',
align: 'right',
},
{
key: 'customer_phone',
label: 'شماره تماس',
align: 'right',
},
{
key: 'shipping_method',
label: 'روش ارسال',
align: 'right',
},
{
key: 'shipping_cost',
label: 'هزینه ارسال',
align: 'right',
},
{
key: 'order_amount',
label: 'مبلغ سفارش',
align: 'right',
},
{
key: 'total_weight',
label: 'وزن',
align: 'right',
},
{
key: 'status',
label: 'وضعیت',
align: 'right',
},
{
key: 'payment_status',
label: 'وضعیت پرداخت',
align: 'right',
},
{
key: 'created_at',
label: 'زمان ثبت',
align: 'right',
},
{ key: 'order_number', label: 'شماره سفارش', align: 'right' },
{ key: 'customer_name', label: 'نام مشتری', align: 'right' },
{ key: 'customer_phone', label: 'شماره تماس', align: 'right' },
{ key: 'shipping_method', label: 'روش ارسال', align: 'right' },
{ key: 'shipping_cost', label: 'هزینه ارسال', align: 'right' },
{ key: 'order_amount', label: 'مبلغ سفارش', align: 'right' },
{ key: 'total_weight', label: 'وزن', align: 'right' },
{ key: 'delivery_date', label: 'تاریخ تحویل', align: 'right' },
{ key: 'status', label: 'وضعیت', align: 'right' },
{ key: 'payment_status', label: 'وضعیت پرداخت', align: 'right' },
{ key: 'created_at', label: 'زمان ثبت', align: 'right' },
{ key: 'shipped_at', label: 'زمان ارسال', align: 'right' },
{ key: 'delivered_at', label: 'زمان تحویل', align: 'right' },
];
const tableData = (data?.shipments || []).map(shipment => ({
@ -143,19 +119,278 @@ const ShipmentsByMethodReportPage = () => {
shipping_cost: formatCurrency(shipment.shipping_cost),
order_amount: formatCurrency(shipment.order_amount),
total_weight: formatWeight(shipment.total_weight),
delivery_date: shipment.delivery_date
? `${shipment.delivery_date}${shipment.delivery_from_hour != null && shipment.delivery_to_hour != null ? ` ${shipment.delivery_from_hour}-${shipment.delivery_to_hour}` : ''}`
: '-',
status: shipment.status,
payment_status: shipment.payment_status,
created_at: formatDateTime(shipment.created_at),
})) || [];
shipped_at: shipment.shipped_at ? formatDateTime(shipment.shipped_at) : '-',
delivered_at: shipment.delivered_at ? formatDateTime(shipment.delivered_at) : '-',
}));
const currentPage = Math.floor(filters.offset / filters.limit) + 1;
const totalPages = data ? Math.ceil(data.total / filters.limit) : 1;
return (
<PageContainer>
<PageTitle>گزارش ارسالها بر اساس روش</PageTitle>
<PageTitle>ارسالها</PageTitle>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-6">
<div className="flex items-center justify-between">
<button
type="button"
onClick={() => setShowFilters(prev => !prev)}
className="flex items-center gap-2 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
>
<Filter className="h-5 w-5" />
<span className="font-medium">فیلترها</span>
{showFilters ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</button>
<div className="flex items-center gap-2">
<Button onClick={handleApplyFilters} className="flex items-center gap-2">
<Filter className="h-4 w-4" />
اعمال فیلترها
</Button>
<Button variant="secondary" size="sm" onClick={handleClearFilters} className="flex items-center gap-2">
<X className="h-4 w-4" />
پاک کردن
</Button>
</div>
</div>
{showFilters && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">تاریخ شروع</label>
<JalaliDateTimePicker
value={tempFilters.date_range?.from}
onChange={value => handleDateChange(value, tempFilters.date_range?.to)}
placeholder="از"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">تاریخ پایان</label>
<JalaliDateTimePicker
value={tempFilters.date_range?.to}
onChange={value => handleDateChange(tempFilters.date_range?.from, value)}
placeholder="تا"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">روش ارسال</label>
<select
value={tempFilters.shipping_method_id ?? ''}
onChange={e => {
const v = e.target.value;
if (v === '') {
setTempFilters(prev => ({
...prev,
shipping_method_id: undefined,
shipping_method_code: undefined,
}));
} else {
const method = shippingMethods.find(m => m.id === Number(v));
if (method) {
setTempFilters(prev => ({
...prev,
shipping_method_id: method.id,
shipping_method_code: method.code,
}));
}
}
}}
className="input w-full min-h-[2.5rem]"
>
<option value="">همه</option>
{shippingMethods.map(m => (
<option key={m.id} value={m.id}>{m.name || m.code}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">شناسه کاربر</label>
<Input
value={tempFilters.user_id ?? ''}
onChange={e => handleNumericFilterChange('user_id', e.target.value)}
placeholder="عدد"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">شماره تلفن</label>
<Input
value={tempFilters.phone_number ?? ''}
onChange={e => handleTempFilterChange('phone_number', e.target.value || undefined)}
placeholder="۰۹۱۲..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">نام مشتری</label>
<Input
value={tempFilters.customer_name ?? ''}
onChange={e => handleTempFilterChange('customer_name', e.target.value || undefined)}
placeholder="جستجو"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">شناسه سفارش</label>
<Input
value={tempFilters.order_id ?? ''}
onChange={e => handleNumericFilterChange('order_id', e.target.value)}
placeholder="عدد"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">شماره سفارش</label>
<Input
value={tempFilters.order_number ?? ''}
onChange={e => handleTempFilterChange('order_number', e.target.value || undefined)}
placeholder="ORD-..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">وضعیت</label>
<select
value={tempFilters.status ?? ''}
onChange={e => handleTempFilterChange('status', e.target.value || undefined)}
className="input w-full min-h-[2.5rem]"
>
{SHIPMENT_STATUS_OPTIONS.map(o => (
<option key={o.value || 'all'} value={o.value}>{o.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">وضعیت پرداخت</label>
<select
value={tempFilters.payment_status ?? ''}
onChange={e => handleTempFilterChange('payment_status', e.target.value || undefined)}
className="input w-full min-h-[2.5rem]"
>
{PAYMENT_STATUS_OPTIONS.map(o => (
<option key={o.value || 'all'} value={o.value}>{o.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">نوع پرداخت</label>
<select
value={tempFilters.payment_type ?? ''}
onChange={e => handleTempFilterChange('payment_type', e.target.value || undefined)}
className="input w-full min-h-[2.5rem]"
>
{PAYMENT_TYPE_OPTIONS.map(o => (
<option key={o.value || 'all'} value={o.value}>{o.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">حداقل هزینه ارسال</label>
<Input
value={tempFilters.min_shipping_cost ?? ''}
onChange={e => handleNumericFilterChange('min_shipping_cost', e.target.value)}
placeholder="ریال"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">حداکثر هزینه ارسال</label>
<Input
value={tempFilters.max_shipping_cost ?? ''}
onChange={e => handleNumericFilterChange('max_shipping_cost', e.target.value)}
placeholder="ریال"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">حداقل مبلغ سفارش</label>
<Input
value={tempFilters.min_order_amount ?? ''}
onChange={e => handleNumericFilterChange('min_order_amount', e.target.value)}
placeholder="ریال"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">حداکثر مبلغ سفارش</label>
<Input
value={tempFilters.max_order_amount ?? ''}
onChange={e => handleNumericFilterChange('max_order_amount', e.target.value)}
placeholder="ریال"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">شهر</label>
<Input
value={tempFilters.city ?? ''}
onChange={e => handleTempFilterChange('city', e.target.value || undefined)}
placeholder="تهران"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">استان</label>
<Input
value={tempFilters.state ?? ''}
onChange={e => handleTempFilterChange('state', e.target.value || undefined)}
placeholder="تهران"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">منطقه</label>
<Input
value={tempFilters.region ?? ''}
onChange={e => handleTempFilterChange('region', e.target.value || undefined)}
placeholder=""
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">تاریخ تحویل (YYYY-MM-DD)</label>
<Input
value={tempFilters.delivery_date ?? ''}
onChange={e => handleTempFilterChange('delivery_date', e.target.value || undefined)}
placeholder="2025-12-25"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">ساعت شروع تحویل (0-23)</label>
<Input
value={tempFilters.delivery_from_hour ?? ''}
onChange={e => handleNumericFilterChange('delivery_from_hour', e.target.value)}
placeholder="۱۴"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">ساعت پایان تحویل (0-23)</label>
<Input
value={tempFilters.delivery_to_hour ?? ''}
onChange={e => handleNumericFilterChange('delivery_to_hour', e.target.value)}
placeholder="۱۸"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">مرتبسازی بر اساس</label>
<select
value={tempFilters.sort_by ?? 'created_at'}
onChange={e => handleTempFilterChange('sort_by', (e.target.value || 'created_at') as ShipmentsByMethodFilters['sort_by'])}
className="input w-full min-h-[2.5rem]"
>
{SORT_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">ترتیب</label>
<select
value={tempFilters.sort_order ?? 'desc'}
onChange={e => handleTempFilterChange('sort_order', (e.target.value || 'desc') as 'asc' | 'desc')}
className="input w-full min-h-[2.5rem]"
>
<option value="desc">نزولی</option>
<option value="asc">صعودی</option>
</select>
</div>
</div>
)}
</div>
{/* Summary Cards */}
{data?.summary && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
@ -165,13 +400,10 @@ const ShipmentsByMethodReportPage = () => {
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">کل ارسالها</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatWithThousands(data.summary.total_shipments)}
</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatWithThousands(data.summary.total_shipments)}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
@ -179,13 +411,10 @@ const ShipmentsByMethodReportPage = () => {
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">مجموع هزینه ارسال</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatCurrency(data.summary.total_shipping_cost)}
</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatCurrency(data.summary.total_shipping_cost)}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
@ -193,27 +422,21 @@ const ShipmentsByMethodReportPage = () => {
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">مجموع مبلغ سفارشات</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatCurrency(data.summary.total_order_amount)}
</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatCurrency(data.summary.total_order_amount)}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<Clock className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">میانگین هزینه</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatCurrency(data.summary.average_shipping_cost)}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">میانگین هزینه ارسال</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatCurrency(data.summary.average_shipping_cost)}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-gray-100 dark:bg-gray-700 rounded-lg">
@ -221,13 +444,10 @@ const ShipmentsByMethodReportPage = () => {
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">در انتظار</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatWithThousands(data.summary.pending_shipments)}
</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatWithThousands(data.summary.pending_shipments)}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
@ -235,13 +455,10 @@ const ShipmentsByMethodReportPage = () => {
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">ارسال شده</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatWithThousands(data.summary.shipped_count)}
</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatWithThousands(data.summary.shipped_count)}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
@ -249,13 +466,10 @@ const ShipmentsByMethodReportPage = () => {
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">تحویل داده شده</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatWithThousands(data.summary.delivered_count)}
</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatWithThousands(data.summary.delivered_count)}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-100 dark:bg-red-900 rounded-lg">
@ -263,55 +477,27 @@ const ShipmentsByMethodReportPage = () => {
</div>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">لغو شده</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatWithThousands(data.summary.cancelled_count)}
</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">{formatWithThousands(data.summary.cancelled_count)}</p>
</div>
</div>
</div>
</div>
)}
{/* Method Summaries */}
{data?.method_summaries && data.method_summaries.length > 0 && (
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
آمار هر روش ارسال
</h3>
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">آمار هر روش ارسال</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.method_summaries.map((method) => (
<div
key={method.shipping_method_id}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4"
>
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{method.shipping_method || method.shipping_method_code}
</h4>
{data.method_summaries.map(method => (
<div key={method.shipping_method_id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">{method.shipping_method || method.shipping_method_code}</h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">تعداد ارسال:</span>
<span className="font-medium">{formatWithThousands(method.shipment_count)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">مجموع درآمد:</span>
<span className="font-medium">{formatCurrency(method.total_revenue)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">مجموع هزینه:</span>
<span className="font-medium">{formatCurrency(method.total_shipping_cost)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">میانگین وزن:</span>
<span className="font-medium">{formatWeight(method.average_weight)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">تحویل شده:</span>
<span className="font-medium text-green-600">{formatWithThousands(method.delivered_count)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">لغو شده:</span>
<span className="font-medium text-red-600">{formatWithThousands(method.cancelled_count)}</span>
</div>
<div className="flex justify-between"><span className="text-gray-600 dark:text-gray-400">تعداد ارسال:</span><span className="font-medium">{formatWithThousands(method.shipment_count)}</span></div>
<div className="flex justify-between"><span className="text-gray-600 dark:text-gray-400">مجموع درآمد:</span><span className="font-medium">{formatCurrency(method.total_revenue)}</span></div>
<div className="flex justify-between"><span className="text-gray-600 dark:text-gray-400">مجموع هزینه:</span><span className="font-medium">{formatCurrency(method.total_shipping_cost)}</span></div>
<div className="flex justify-between"><span className="text-gray-600 dark:text-gray-400">میانگین وزن:</span><span className="font-medium">{formatWeight(method.average_weight)}</span></div>
<div className="flex justify-between"><span className="text-gray-600 dark:text-gray-400">تحویل شده:</span><span className="font-medium text-green-600">{formatWithThousands(method.delivered_count)}</span></div>
<div className="flex justify-between"><span className="text-gray-600 dark:text-gray-400">لغو شده:</span><span className="font-medium text-red-600">{formatWithThousands(method.cancelled_count)}</span></div>
</div>
</div>
))}
@ -319,9 +505,8 @@ const ShipmentsByMethodReportPage = () => {
</div>
)}
{/* Table */}
{isLoading ? (
<ReportSkeleton summaryCardCount={8} tableColumnCount={9} showMethodSummaries={true} />
<ReportSkeleton summaryCardCount={8} tableColumnCount={13} showMethodSummaries={true} />
) : error ? (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p className="text-red-600 dark:text-red-400">خطا در دریافت دادهها</p>
@ -331,7 +516,6 @@ const ShipmentsByMethodReportPage = () => {
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<Table columns={columns} data={tableData} loading={isLoading} />
</div>
{data && data.total > 0 && totalPages > 1 && (
<div className="mt-4 flex justify-center">
<Pagination
@ -343,7 +527,6 @@ const ShipmentsByMethodReportPage = () => {
/>
</div>
)}
{data && data.total === 0 && (
<div className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-8 text-center">
<p className="text-gray-600 dark:text-gray-400">دادهای یافت نشد</p>
@ -356,4 +539,3 @@ const ShipmentsByMethodReportPage = () => {
};
export default ShipmentsByMethodReportPage;