Files
clinicpro/assets/admin/pages/RepresentationFinancePage.tsx
T
hamed 9603b702c1 feat: implement domain guard for commission calculation and enhance representation dashboard
- Added domain guard in CommissionService to ensure commission is calculated only when the appointment is booked under the same representation as the doctor.
- Updated RepresentationController to filter statistics by representation, ensuring accurate data is shown for each representative.
- Introduced new endpoints for the representation dashboard to provide summary statistics, doctor performance, and financial reports.
- Created new pages for RepresentationFinance and RepresentationSettlement to display financial data and allow for settlement requests.
- Added migration to include booking_representation_id in appointments for tracking the representative under which the appointment was booked.
2026-06-24 16:14:41 +03:30

105 lines
3.9 KiB
TypeScript

import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import { formatRial, formatDate } from '../lib/utils';
import Pagination from '../components/ui/Pagination';
interface FinanceRow {
uuid: string;
appointment_uuid: string | null;
doctor_name: string | null;
gross_rials: number;
tax_rials: number;
sms_fee_rials: number;
commission_percent: number;
representation_share_rials: number;
created_at: string;
}
type RangeKey = 'today' | 'week' | 'month' | 'all';
function rangeFrom(key: RangeKey): number | null {
const now = Math.floor(Date.now() / 1000);
if (key === 'today') return Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
if (key === 'week') return now - 7 * 86400;
if (key === 'month') return now - 30 * 86400;
return null;
}
const RANGE_LABEL: Record<RangeKey, string> = {
today: 'امروز', week: 'این هفته', month: 'این ماه', all: 'همه',
};
export default function RepresentationFinancePage() {
const [range, setRange] = useState<RangeKey>('month');
const [page, setPage] = useState(1);
const limit = 15;
const { data, isLoading } = useQuery({
queryKey: ['representation-finance', range, page],
queryFn: () => {
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
const from = rangeFrom(range);
if (from !== null) params.set('from', String(from));
return api.get<PaginatedResponse<FinanceRow>>(`/api/v1/representation/finance/report?${params}`);
},
});
const items: FinanceRow[] = data?.data ?? [];
const total = data?.meta?.totalRecords ?? 0;
return (
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">گزارش مالی</h1>
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>درآمد ثبت‌شده از پورسانت نوبت‌ها</div>
</div>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 'var(--gap)' }}>
{(['today', 'week', 'month', 'all'] as RangeKey[]).map(k => (
<button key={k} className={range === k ? 'on' : ''} onClick={() => { setRange(k); setPage(1); }}>
{RANGE_LABEL[k]}
</button>
))}
</div>
<div className="card card-pad">
<div style={{ overflowX: 'auto' }}>
<table className="tbl" style={{ width: '100%' }}>
<thead>
<tr>
<th>پزشک</th><th>مبلغ نوبت</th><th>مالیات</th><th>پیامک</th>
<th>درصد</th><th>سهم نماینده</th><th>تاریخ</th>
</tr>
</thead>
<tbody>
{isLoading && (
<tr><td colSpan={7} className="muted" style={{ textAlign: 'center', padding: 16 }}>در حال بارگذاری...</td></tr>
)}
{!isLoading && items.length === 0 && (
<tr><td colSpan={7} className="muted" style={{ textAlign: 'center', padding: 16 }}>درآمدی در این بازه ثبت نشده است</td></tr>
)}
{items.map(r => (
<tr key={r.uuid}>
<td>{r.doctor_name ?? '—'}</td>
<td>{formatRial(r.gross_rials)}</td>
<td>{formatRial(r.tax_rials)}</td>
<td>{formatRial(r.sms_fee_rials)}</td>
<td>{r.commission_percent}٪</td>
<td>{formatRial(r.representation_share_rials)}</td>
<td>{formatDate(r.created_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
</div>
);
}