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:
parent
481e7e748a
commit
165968f9f9
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -22,6 +22,7 @@ export interface AdminNotificationsResponse {
|
|||
unread_count: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface AdminNotificationsUnreadResponse {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ const ContactUsListPage: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{messages.length > 0 && totalPages > 1 && (
|
||||
{messages.length > 0 && total > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue