feat(financial-report): add filters for representation, date range, and improve data fetching
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user