334 lines
12 KiB
TypeScript
334 lines
12 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
||
import { useCustomerDiscountUsageReport } from '../core/_hooks';
|
||
import { CustomerDiscountUsageFilters } 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, TrendingUp, Users, DollarSign, Hash, X } from 'lucide-react';
|
||
import { formatWithThousands, parseFormattedNumber, persianToEnglish } from '@/utils/numberUtils';
|
||
import { formatCurrency, formatDateTime } from '@/utils/formatters';
|
||
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
|
||
|
||
|
||
const CustomerDiscountUsagePage = () => {
|
||
const [filters, setFilters] = useState<CustomerDiscountUsageFilters>({
|
||
user_id: 0,
|
||
limit: 50,
|
||
offset: 0,
|
||
});
|
||
|
||
const [tempFilters, setTempFilters] = useState<CustomerDiscountUsageFilters>({
|
||
user_id: 0,
|
||
limit: 50,
|
||
offset: 0,
|
||
});
|
||
|
||
const [refetchKey, setRefetchKey] = useState(0);
|
||
|
||
const filtersWithKey = useMemo(() => ({
|
||
...filters,
|
||
_refetchKey: refetchKey,
|
||
}), [filters, refetchKey]);
|
||
|
||
const { data, isLoading, error } = useCustomerDiscountUsageReport(filtersWithKey as CustomerDiscountUsageFilters & { _refetchKey?: number });
|
||
|
||
const handleTempFilterChange = (key: keyof CustomerDiscountUsageFilters, value: any) => {
|
||
setTempFilters(prev => ({
|
||
...prev,
|
||
[key]: value,
|
||
}));
|
||
};
|
||
|
||
const handleDateRangeChange = (from: string | undefined, to: string | undefined) => {
|
||
setTempFilters(prev => ({
|
||
...prev,
|
||
date_range: {
|
||
from,
|
||
to,
|
||
},
|
||
}));
|
||
};
|
||
|
||
const handleNumericFilterChange = (key: 'discount_id' | 'user_id', raw: string) => {
|
||
const converted = persianToEnglish(raw).replace(/[^\d]/g, '');
|
||
const numeric = converted ? Number(converted) : undefined;
|
||
if (key === 'user_id') {
|
||
handleTempFilterChange('user_id', numeric || 0);
|
||
} else {
|
||
handleTempFilterChange(key, numeric);
|
||
}
|
||
};
|
||
|
||
const handleApplyFilters = () => {
|
||
const newFilters = {
|
||
...tempFilters,
|
||
offset: 0,
|
||
};
|
||
setFilters(newFilters);
|
||
setRefetchKey(prev => prev + 1);
|
||
};
|
||
|
||
const handlePageChange = (page: number) => {
|
||
setFilters(prev => ({
|
||
...prev,
|
||
offset: (page - 1) * prev.limit,
|
||
}));
|
||
};
|
||
|
||
const handleClearFilters = () => {
|
||
const clearedFilters = {
|
||
user_id: 0,
|
||
limit: 50,
|
||
offset: 0,
|
||
};
|
||
setTempFilters(clearedFilters);
|
||
setFilters(clearedFilters);
|
||
};
|
||
|
||
const columns: TableColumn[] = [
|
||
{
|
||
key: 'discount_code',
|
||
label: 'کد تخفیف',
|
||
align: 'right',
|
||
},
|
||
{
|
||
key: 'discount_name',
|
||
label: 'نام کد تخفیف',
|
||
align: 'right',
|
||
},
|
||
{
|
||
key: 'order_number',
|
||
label: 'شماره سفارش',
|
||
align: 'right',
|
||
},
|
||
{
|
||
key: 'amount',
|
||
label: 'مبلغ تخفیف',
|
||
align: 'right',
|
||
},
|
||
{
|
||
key: 'used_at',
|
||
label: 'زمان استفاده',
|
||
align: 'right',
|
||
},
|
||
];
|
||
|
||
const tableData = (data?.usages || []).map(usage => ({
|
||
discount_code: usage.discount_code,
|
||
discount_name: usage.discount_name,
|
||
order_number: usage.order_number || '-',
|
||
amount: formatCurrency(usage.amount),
|
||
used_at: formatDateTime(usage.used_at),
|
||
}));
|
||
|
||
const currentPage = Math.floor(filters.offset / filters.limit) + 1;
|
||
const totalPages = data ? Math.ceil(data.total / filters.limit) : 1;
|
||
|
||
return (
|
||
<PageContainer>
|
||
<PageTitle>گزارش استفاده کاربر خاص از کدهای تخفیف</PageTitle>
|
||
|
||
{/* Filters */}
|
||
<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 mb-4">
|
||
<div className="flex items-center gap-2">
|
||
<Filter className="h-5 w-5 text-gray-500" />
|
||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">فیلترها</h3>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
onClick={handleApplyFilters}
|
||
disabled={!tempFilters.user_id || tempFilters.user_id === 0}
|
||
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>
|
||
|
||
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||
<span className="font-semibold">توجه:</span> برای مشاهده گزارش، لطفاً شناسه کاربر را وارد کنید. این فیلد الزامی است.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
شناسه کاربر <span className="text-red-500">*</span>
|
||
</label>
|
||
<Input
|
||
value={tempFilters.user_id?.toString() || ''}
|
||
onChange={(e) => handleNumericFilterChange('user_id', e.target.value)}
|
||
placeholder="مثلاً 456"
|
||
numeric
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
کد تخفیف
|
||
</label>
|
||
<Input
|
||
value={tempFilters.discount_code || ''}
|
||
onChange={(e) => handleTempFilterChange('discount_code', e.target.value || undefined)}
|
||
placeholder="مثلاً SUMMER2025"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
شناسه کد تخفیف
|
||
</label>
|
||
<Input
|
||
value={tempFilters.discount_id?.toString() || ''}
|
||
onChange={(e) => handleNumericFilterChange('discount_id', e.target.value)}
|
||
placeholder="مثلاً 123"
|
||
numeric
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
تاریخ شروع
|
||
</label>
|
||
<JalaliDateTimePicker
|
||
value={tempFilters.date_range?.from}
|
||
onChange={(value) => handleDateRangeChange(value, tempFilters.date_range?.to)}
|
||
placeholder="انتخاب تاریخ شروع"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
تاریخ پایان
|
||
</label>
|
||
<JalaliDateTimePicker
|
||
value={tempFilters.date_range?.to}
|
||
onChange={(value) => handleDateRangeChange(tempFilters.date_range?.from, value)}
|
||
placeholder="انتخاب تاریخ پایان"
|
||
/>
|
||
</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">
|
||
<div className="flex items-center gap-3">
|
||
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||
<Hash className="h-5 w-5 text-blue-600 dark:text-blue-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.summary.total_usages)}
|
||
</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">
|
||
<DollarSign className="h-5 w-5 text-green-600 dark:text-green-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.total_discount_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-purple-100 dark:bg-purple-900 rounded-lg">
|
||
<Hash className="h-5 w-5 text-purple-600 dark:text-purple-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.summary.unique_codes)}
|
||
</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">
|
||
<TrendingUp 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_discount_per_order)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Table */}
|
||
{isLoading ? (
|
||
<ReportSkeleton summaryCardCount={4} tableColumnCount={7} />
|
||
) : 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>
|
||
</div>
|
||
) : filters.user_id === 0 ? (
|
||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||
<p className="text-yellow-600 dark:text-yellow-400">لطفاً شناسه کاربر را وارد کنید</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<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
|
||
currentPage={currentPage}
|
||
totalPages={totalPages}
|
||
onPageChange={handlePageChange}
|
||
itemsPerPage={filters.limit}
|
||
totalItems={data.total}
|
||
/>
|
||
</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>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default CustomerDiscountUsagePage;
|
||
|