admin/src/pages/reports/discount-statistics/customer-discount-usage/CustomerDiscountUsagePage.tsx

334 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;