feat(financial-report): add filters for representation, date range, and improve data fetching

This commit is contained in:
hamed
2026-07-12 14:47:53 +03:30
parent 0ec0b92705
commit 147ea0c318
2 changed files with 43 additions and 4 deletions
+41 -2
View File
@@ -5,6 +5,11 @@ import type { ApiResponse, PaginatedResponse } from '../lib/api';
import { formatRial, formatDate } from '../lib/utils';
import DataTable, { Column } from '../components/ui/DataTable';
import Pagination from '../components/ui/Pagination';
import SearchableSelect from '../components/ui/SearchableSelect';
import PersianDateInput from '../components/ui/PersianDateInput';
const dayStart = (d: string): number => Math.floor(new Date(`${d}T00:00:00`).getTime() / 1000);
const dayEnd = (d: string): number => Math.floor(new Date(`${d}T23:59:59`).getTime() / 1000);
interface Breakdown {
uuid: string;
@@ -41,6 +46,9 @@ const SOURCE_LABEL: Record<string, string> = {
export default function FinancialReportPage() {
const [page, setPage] = useState(1);
const [source, setSource] = useState('');
const [repId, setRepId] = useState<number | null>(null);
const [fromDate, setFromDate] = useState('');
const [toDate, setToDate] = useState('');
const limit = 15;
const summaryQ = useQuery({
@@ -52,11 +60,24 @@ export default function FinancialReportPage() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const summary: Summary | undefined = (summaryQ.data?.data as any)?.data ?? summaryQ.data?.data;
const repsQ = useQuery({
queryKey: ['representations-select'],
queryFn: () => api.get<PaginatedResponse<{ id: number; full_name: string; city: string | null }>>('/api/v1/admin/representations?limit=200'),
staleTime: 60_000,
});
const repOptions = [
{ value: '', label: 'همه نمایندگان' },
...(repsQ.data?.data ?? []).map((r) => ({ value: r.id, label: `${r.full_name}${r.city ? `${r.city}` : ''}` })),
];
const { data, isLoading } = useQuery({
queryKey: ['financial-breakdowns', page, source],
queryKey: ['financial-breakdowns', page, source, repId, fromDate, toDate],
queryFn: () => {
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
if (source) params.set('source', source);
if (repId) params.set('representation_id', String(repId));
if (fromDate) params.set('from', String(dayStart(fromDate)));
if (toDate) params.set('to', String(dayEnd(toDate)));
return api.get<PaginatedResponse<Breakdown>>(`/api/v1/admin/financial-breakdowns?${params}`);
},
});
@@ -104,12 +125,30 @@ export default function FinancialReportPage() {
))}
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 'var(--gap)' }}>
<div className="seg" style={{ marginBottom: 'var(--gap)' }}>
<button className={!source ? 'on' : ''} onClick={() => { setSource(''); setPage(1); }}>همه</button>
<button className={source === 'appointment' ? 'on' : ''} onClick={() => { setSource('appointment'); setPage(1); }}>نوبت</button>
<button className={source === 'subscription' ? 'on' : ''} onClick={() => { setSource('subscription'); setPage(1); }}>ارتقاء اشتراک</button>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 'var(--gap)', flexWrap: 'wrap', alignItems: 'center' }}>
<div style={{ minWidth: 220 }}>
<SearchableSelect
options={repOptions}
value={repId ?? ''}
onChange={(v) => { setRepId(v === '' || v === null ? null : Number(v)); setPage(1); }}
placeholder="فیلتر نماینده…"
/>
</div>
<PersianDateInput value={fromDate} onChange={(v) => { setFromDate(v); setPage(1); }} placeholder="از تاریخ" max={toDate || undefined} />
<PersianDateInput value={toDate} onChange={(v) => { setToDate(v); setPage(1); }} placeholder="تا تاریخ" min={fromDate || undefined} />
{(repId || fromDate || toDate || source) && (
<button className="btn ghost sm" onClick={() => { setRepId(null); setFromDate(''); setToDate(''); setSource(''); setPage(1); }}>
پاککردن فیلترها
</button>
)}
</div>
<DataTable
columns={columns}
data={items}
@@ -348,7 +348,7 @@ export default function RepresentationDetailPage() {
{repDoctors.map((d) => (
<tr key={d.uuid} style={{ cursor: 'pointer' }} onClick={() => navigate(`/admin/doctors/${d.uuid}`)}>
<td><b>{d.name}</b></td>
<td dir="ltr" style={{ fontFamily: 'monospace' }} className="text-[var(--text-3)]">{d.medical_code ?? '—'}</td>
<td style={{ fontFamily: 'monospace' }} className="text-[var(--text-3)]"><span dir="ltr">{d.medical_code ?? '—'}</span></td>
<td>{formatNumber(d.past_count)}</td>
<td>{d.upcoming_count > 0 ? <span className="badge green">{formatNumber(d.upcoming_count)}</span> : <span className="text-[var(--text-3)]">۰</span>}</td>
<td><ActiveBadge active={d.is_active} /></td>
@@ -398,7 +398,7 @@ export default function RepresentationDetailPage() {
<tr key={a.uuid} style={{ cursor: 'pointer' }} onClick={() => navigate(`/admin/doctors/${a.doctor_uuid}`)}>
<td><b>{a.doctor_name}</b></td>
<td>{a.patient_name ?? '—'}</td>
<td dir="ltr" className="text-right">{formatDateTime(a.slot_start)}</td>
<td><span dir="ltr">{formatDateTime(a.slot_start)}</span></td>
<td><span className="badge gray">{a.status}</span></td>
</tr>
))}