From 165968f9f9bc0cd9190d96c529e6c234bf85abb5 Mon Sep 17 00:00:00 2001 From: hosseintaromi Date: Thu, 19 Feb 2026 10:18:02 +0330 Subject: [PATCH] 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. --- src/constant/enums.ts | 46 ++ src/pages/admin-notifications/core/_models.ts | 1 + .../AdminNotificationsListPage.tsx | 13 +- .../contact-us-list/ContactUsListPage.tsx | 2 +- .../orders/order-detail/OrderDetailPage.tsx | 20 +- .../orders/orders-list/OrdersListPage.tsx | 10 +- .../payment-ipg/ipg-list/IPGListPage.tsx | 12 +- .../PaymentMethodsReportPage.tsx | 20 +- .../reports/sales-summary/core/_requests.ts | 2 +- .../SalesSummaryReportPage.tsx | 164 +++++- .../shipment-statistics/core/_models.ts | 16 +- .../shipment-statistics/core/_requests.ts | 26 +- .../ShipmentsByMethodReportPage.tsx | 506 ++++++++++++------ 13 files changed, 613 insertions(+), 225 deletions(-) create mode 100644 src/constant/enums.ts diff --git a/src/constant/enums.ts b/src/constant/enums.ts new file mode 100644 index 0000000..48e678f --- /dev/null +++ b/src/constant/enums.ts @@ -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 = { + '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; +}; diff --git a/src/pages/admin-notifications/core/_models.ts b/src/pages/admin-notifications/core/_models.ts index e81f9e2..fadf882 100644 --- a/src/pages/admin-notifications/core/_models.ts +++ b/src/pages/admin-notifications/core/_models.ts @@ -22,6 +22,7 @@ export interface AdminNotificationsResponse { unread_count: number; offset: number; limit: number; + total?: number; } export interface AdminNotificationsUnreadResponse { diff --git a/src/pages/admin-notifications/notifications-list/AdminNotificationsListPage.tsx b/src/pages/admin-notifications/notifications-list/AdminNotificationsListPage.tsx index ae3023f..624784b 100644 --- a/src/pages/admin-notifications/notifications-list/AdminNotificationsListPage.tsx +++ b/src/pages/admin-notifications/notifications-list/AdminNotificationsListPage.tsx @@ -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 && (
)} diff --git a/src/pages/contact-us/contact-us-list/ContactUsListPage.tsx b/src/pages/contact-us/contact-us-list/ContactUsListPage.tsx index 4e26bde..730796c 100644 --- a/src/pages/contact-us/contact-us-list/ContactUsListPage.tsx +++ b/src/pages/contact-us/contact-us-list/ContactUsListPage.tsx @@ -147,7 +147,7 @@ const ContactUsListPage: React.FC = () => { )} - {messages.length > 0 && totalPages > 1 && ( + {messages.length > 0 && total > 0 && ( { 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 = { - '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 && (
روش پرداخت - {formatPaymentType((data as any).payments[0].payment_type)} + {getPaymentTypeLabel((data as any).payments[0].payment_type)}
)} {order?.invoice_id && ( diff --git a/src/pages/orders/orders-list/OrdersListPage.tsx b/src/pages/orders/orders-list/OrdersListPage.tsx index c02b6ea..0803863 100644 --- a/src/pages/orders/orders-list/OrdersListPage.tsx +++ b/src/pages/orders/orders-list/OrdersListPage.tsx @@ -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" > - - - - - - + {PAYMENT_STATUS_OPTIONS.map(o => ( + + ))} diff --git a/src/pages/payment-ipg/ipg-list/IPGListPage.tsx b/src/pages/payment-ipg/ipg-list/IPGListPage.tsx index 3e82ee4..67f8ccd 100644 --- a/src/pages/payment-ipg/ipg-list/IPGListPage.tsx +++ b/src/pages/payment-ipg/ipg-list/IPGListPage.tsx @@ -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 = { - '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(); diff --git a/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx b/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx index 1f6c841..3b840a4 100644 --- a/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx +++ b/src/pages/reports/payment-statistics/payment-methods-report/PaymentMethodsReportPage.tsx @@ -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 = { - 'bank-topup': 'افزایش موجودی کیف پول', - 'card-to-card': 'پرداخت به روش کارت به کارت', - 'debit-rial-wallet': 'پرداخت از کیف ریالی', - 'debit-gold18k-wallet': 'پرداخت از کیف طلا', - }; - return labels[type] || type; -}; - - const PaymentMethodsReportPage = () => { const [filters, setFilters] = useState({ 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" > - - - - - + {PAYMENT_TYPE_OPTIONS.map(o => ( + + ))} diff --git a/src/pages/reports/sales-summary/core/_requests.ts b/src/pages/reports/sales-summary/core/_requests.ts index 2dbd597..e0f28cc 100644 --- a/src/pages/reports/sales-summary/core/_requests.ts +++ b/src/pages/reports/sales-summary/core/_requests.ts @@ -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; diff --git a/src/pages/reports/sales-summary/sales-summary-report/SalesSummaryReportPage.tsx b/src/pages/reports/sales-summary/sales-summary-report/SalesSummaryReportPage.tsx index c4de660..c1805ec 100644 --- a/src/pages/reports/sales-summary/sales-summary-report/SalesSummaryReportPage.tsx +++ b/src/pages/reports/sales-summary/sales-summary-report/SalesSummaryReportPage.tsx @@ -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(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 ( +
+ + + {isOpen && ( +
+ {options.map((o) => ( + + ))} +
+ )} +
+ ); +}; + const toIsoString = (date: DateObject): string => { try { const g = date.convert(undefined); @@ -351,11 +425,41 @@ const SalesSummaryReportPage = () => { numeric /> + + handleTempFilterChange('status', v.length ? v : undefined)} + placeholder="خالی = فقط تحویل شده" + /> + +
+ + +
{data && ( <> +
+ +

+ اعداد خلاصه بدون فیلتر هستند؛ فقط لیست محصولات بر اساس فیلترها محدود می‌شود. +

+
+
@@ -385,6 +489,20 @@ const SalesSummaryReportPage = () => {
+
+
+
+ +
+
+

وزن با اجرت

+

+ {formatWithThousands(data.total_final_weight, 2)} گرم +

+
+
+
+
@@ -412,6 +530,48 @@ const SalesSummaryReportPage = () => {
+ +
+
+
+ +
+
+

میانگین قیمت فروش

+

+ {formatCurrency(data.average_price)} +

+
+
+
+ +
+
+
+ +
+
+

میانگین وزن

+

+ {formatWithThousands(data.average_weight, 2)} گرم +

+
+
+
+ +
+
+
+ +
+
+

مجموع تخفیف

+

+ {formatCurrency(data.total_discount)} +

+
+
+
diff --git a/src/pages/reports/shipment-statistics/core/_models.ts b/src/pages/reports/shipment-statistics/core/_models.ts index 24121a6..4babcb6 100644 --- a/src/pages/reports/shipment-statistics/core/_models.ts +++ b/src/pages/reports/shipment-statistics/core/_models.ts @@ -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; } diff --git a/src/pages/reports/shipment-statistics/core/_requests.ts b/src/pages/reports/shipment-statistics/core/_requests.ts index 2c7eada..dd55786 100644 --- a/src/pages/reports/shipment-statistics/core/_requests.ts +++ b/src/pages/reports/shipment-statistics/core/_requests.ts @@ -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; diff --git a/src/pages/reports/shipment-statistics/shipments-by-method-report/ShipmentsByMethodReportPage.tsx b/src/pages/reports/shipment-statistics/shipments-by-method-report/ShipmentsByMethodReportPage.tsx index b91ce69..5b386ea 100644 --- a/src/pages/reports/shipment-statistics/shipments-by-method-report/ShipmentsByMethodReportPage.tsx +++ b/src/pages/reports/shipment-statistics/shipments-by-method-report/ShipmentsByMethodReportPage.tsx @@ -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({ - 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(defaultFilters); + const [tempFilters, setTempFilters] = useState(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 ( - گزارش ارسال‌ها بر اساس روش + ارسال‌ها + +
+
+ +
+ + +
+
+ + {showFilters && ( +
+
+ + handleDateChange(value, tempFilters.date_range?.to)} + placeholder="از" + /> +
+
+ + handleDateChange(tempFilters.date_range?.from, value)} + placeholder="تا" + /> +
+
+ + +
+
+ + handleNumericFilterChange('user_id', e.target.value)} + placeholder="عدد" + /> +
+
+ + handleTempFilterChange('phone_number', e.target.value || undefined)} + placeholder="۰۹۱۲..." + /> +
+
+ + handleTempFilterChange('customer_name', e.target.value || undefined)} + placeholder="جستجو" + /> +
+
+ + handleNumericFilterChange('order_id', e.target.value)} + placeholder="عدد" + /> +
+
+ + handleTempFilterChange('order_number', e.target.value || undefined)} + placeholder="ORD-..." + /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + handleNumericFilterChange('min_shipping_cost', e.target.value)} + placeholder="ریال" + /> +
+
+ + handleNumericFilterChange('max_shipping_cost', e.target.value)} + placeholder="ریال" + /> +
+
+ + handleNumericFilterChange('min_order_amount', e.target.value)} + placeholder="ریال" + /> +
+
+ + handleNumericFilterChange('max_order_amount', e.target.value)} + placeholder="ریال" + /> +
+
+ + handleTempFilterChange('city', e.target.value || undefined)} + placeholder="تهران" + /> +
+
+ + handleTempFilterChange('state', e.target.value || undefined)} + placeholder="تهران" + /> +
+
+ + handleTempFilterChange('region', e.target.value || undefined)} + placeholder="" + /> +
+
+ + handleTempFilterChange('delivery_date', e.target.value || undefined)} + placeholder="2025-12-25" + /> +
+
+ + handleNumericFilterChange('delivery_from_hour', e.target.value)} + placeholder="۱۴" + /> +
+
+ + handleNumericFilterChange('delivery_to_hour', e.target.value)} + placeholder="۱۸" + /> +
+
+ + +
+
+ + +
+
+ )} +
- {/* Summary Cards */} {data?.summary && (
@@ -165,13 +400,10 @@ const ShipmentsByMethodReportPage = () => {

کل ارسال‌ها

-

- {formatWithThousands(data.summary.total_shipments)} -

+

{formatWithThousands(data.summary.total_shipments)}

-
@@ -179,13 +411,10 @@ const ShipmentsByMethodReportPage = () => {

مجموع هزینه ارسال

-

- {formatCurrency(data.summary.total_shipping_cost)} -

+

{formatCurrency(data.summary.total_shipping_cost)}

-
@@ -193,27 +422,21 @@ const ShipmentsByMethodReportPage = () => {

مجموع مبلغ سفارشات

-

- {formatCurrency(data.summary.total_order_amount)} -

+

{formatCurrency(data.summary.total_order_amount)}

-
-

میانگین هزینه

-

- {formatCurrency(data.summary.average_shipping_cost)} -

+

میانگین هزینه ارسال

+

{formatCurrency(data.summary.average_shipping_cost)}

-
@@ -221,13 +444,10 @@ const ShipmentsByMethodReportPage = () => {

در انتظار

-

- {formatWithThousands(data.summary.pending_shipments)} -

+

{formatWithThousands(data.summary.pending_shipments)}

-
@@ -235,13 +455,10 @@ const ShipmentsByMethodReportPage = () => {

ارسال شده

-

- {formatWithThousands(data.summary.shipped_count)} -

+

{formatWithThousands(data.summary.shipped_count)}

-
@@ -249,13 +466,10 @@ const ShipmentsByMethodReportPage = () => {

تحویل داده شده

-

- {formatWithThousands(data.summary.delivered_count)} -

+

{formatWithThousands(data.summary.delivered_count)}

-
@@ -263,55 +477,27 @@ const ShipmentsByMethodReportPage = () => {

لغو شده

-

- {formatWithThousands(data.summary.cancelled_count)} -

+

{formatWithThousands(data.summary.cancelled_count)}

)} - {/* Method Summaries */} {data?.method_summaries && data.method_summaries.length > 0 && (
-

- آمار هر روش ارسال -

+

آمار هر روش ارسال

- {data.method_summaries.map((method) => ( -
-

- {method.shipping_method || method.shipping_method_code} -

+ {data.method_summaries.map(method => ( +
+

{method.shipping_method || method.shipping_method_code}

-
- تعداد ارسال: - {formatWithThousands(method.shipment_count)} -
-
- مجموع درآمد: - {formatCurrency(method.total_revenue)} -
-
- مجموع هزینه: - {formatCurrency(method.total_shipping_cost)} -
-
- میانگین وزن: - {formatWeight(method.average_weight)} -
-
- تحویل شده: - {formatWithThousands(method.delivered_count)} -
-
- لغو شده: - {formatWithThousands(method.cancelled_count)} -
+
تعداد ارسال:{formatWithThousands(method.shipment_count)}
+
مجموع درآمد:{formatCurrency(method.total_revenue)}
+
مجموع هزینه:{formatCurrency(method.total_shipping_cost)}
+
میانگین وزن:{formatWeight(method.average_weight)}
+
تحویل شده:{formatWithThousands(method.delivered_count)}
+
لغو شده:{formatWithThousands(method.cancelled_count)}
))} @@ -319,9 +505,8 @@ const ShipmentsByMethodReportPage = () => {
)} - {/* Table */} {isLoading ? ( - + ) : error ? (

خطا در دریافت داده‌ها

@@ -331,7 +516,6 @@ const ShipmentsByMethodReportPage = () => {
- {data && data.total > 0 && totalPages > 1 && (
{ />
)} - {data && data.total === 0 && (

داده‌ای یافت نشد

@@ -356,4 +539,3 @@ const ShipmentsByMethodReportPage = () => { }; export default ShipmentsByMethodReportPage; -