- 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.
105 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|