- Added new configuration keys for appointment and upgrade commissions, tax settings, and SMS panel fee in SiteConfigController and SiteConfigRepository. - Introduced CommissionService to handle commission calculations for appointments and subscriptions, including tax deductions and SMS fees. - Created FinancialBreakdown entity and repository to log financial transactions. - Updated PaymentController to process commissions upon successful payments for appointments and subscriptions. - Developed FinancialReportPage in the admin panel to display financial breakdowns and summaries. - Added database migration for the new financial_breakdowns table.
124 lines
5.4 KiB
TypeScript
124 lines
5.4 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 DataTable, { Column } from '../components/ui/DataTable';
|
|
import Pagination from '../components/ui/Pagination';
|
|
|
|
interface Breakdown {
|
|
uuid: string;
|
|
order_id: string;
|
|
source: 'appointment' | 'subscription';
|
|
gross_rials: number;
|
|
sms_fee_rials: number;
|
|
tax_percent: number;
|
|
tax_rials: number;
|
|
net_after_tax_rials: number;
|
|
commission_percent: number;
|
|
representation_share_rials: number;
|
|
system_share_rials: number;
|
|
representation_id: number | null;
|
|
representation_name: string | null;
|
|
doctor_id: number | null;
|
|
clinic_id: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
interface Summary {
|
|
total_gross: number;
|
|
total_representation_income: number;
|
|
total_tax_collected: number;
|
|
total_sms_fee: number;
|
|
total_system_share: number;
|
|
}
|
|
|
|
const SOURCE_LABEL: Record<string, string> = {
|
|
appointment: 'نوبت',
|
|
subscription: 'ارتقاء اشتراک',
|
|
};
|
|
|
|
export default function FinancialReportPage() {
|
|
const [page, setPage] = useState(1);
|
|
const [source, setSource] = useState('');
|
|
const limit = 15;
|
|
|
|
const summaryQ = useQuery({
|
|
queryKey: ['financial-summary'],
|
|
queryFn: () => api.get<ApiResponse<Summary>>('/api/v1/admin/financial-summary'),
|
|
staleTime: 30_000,
|
|
});
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const summary: Summary | undefined = (summaryQ.data?.data as any)?.data ?? summaryQ.data?.data;
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['financial-breakdowns', page, source],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
|
if (source) params.set('source', source);
|
|
return api.get<PaginatedResponse<Breakdown>>(`/api/v1/admin/financial-breakdowns?${params}`);
|
|
},
|
|
});
|
|
|
|
const items: Breakdown[] = data?.data ?? [];
|
|
const total = data?.meta?.totalRecords ?? 0;
|
|
|
|
const cards = [
|
|
{ label: 'مجموع ناخالص', value: summary?.total_gross, color: 'var(--text-2)', bg: 'var(--surface-3)' },
|
|
{ label: 'درآمد نمایندگان', value: summary?.total_representation_income, color: 'var(--primary)', bg: 'var(--primary-soft)' },
|
|
{ label: 'مالیات دریافتشده', value: summary?.total_tax_collected, color: 'var(--warning)', bg: 'var(--warning-bg)' },
|
|
{ label: 'هزینه پنل پیامک', value: summary?.total_sms_fee, color: 'var(--violet)', bg: 'var(--violet-bg)' },
|
|
{ label: 'سهم سیستم', value: summary?.total_system_share, color: 'var(--success)', bg: 'var(--success-bg)' },
|
|
];
|
|
|
|
const columns: Column<Breakdown>[] = [
|
|
{ key: 'order_id', header: 'شناسه سفارش', render: (r) => <span dir="ltr" style={{ fontSize: 12 }}>{r.order_id}</span> },
|
|
{ key: 'source', header: 'نوع', render: (r) => SOURCE_LABEL[r.source] ?? r.source },
|
|
{ key: 'representation_name', header: 'نماینده', render: (r) => r.representation_name ?? '—' },
|
|
{ key: 'gross_rials', header: 'ناخالص', render: (r) => formatRial(r.gross_rials) },
|
|
{ key: 'sms_fee_rials', header: 'پیامک', render: (r) => formatRial(r.sms_fee_rials) },
|
|
{ key: 'tax_rials', header: 'مالیات', render: (r) => `${formatRial(r.tax_rials)} (${r.tax_percent}٪)` },
|
|
{ key: 'representation_share_rials', header: 'سهم نماینده', render: (r) => `${formatRial(r.representation_share_rials)} (${r.commission_percent}٪)` },
|
|
{ key: 'system_share_rials', header: 'سهم سیستم', render: (r) => formatRial(r.system_share_rials) },
|
|
{ key: 'created_at', header: 'تاریخ', render: (r) => formatDate(r.created_at) },
|
|
];
|
|
|
|
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: 3 }}>تفکیک پورسانت، مالیات و هزینه پنل پیامک هر تراکنش</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-grid" style={{ gridTemplateColumns: 'repeat(5,1fr)', marginBottom: 'var(--gap)' }}>
|
|
{cards.map((c) => (
|
|
<div key={c.label} className="stat" style={{ background: c.bg }}>
|
|
<div className="stat-label">{c.label}</div>
|
|
<div className="stat-value" style={{ color: c.color, fontSize: 15 }}>
|
|
{c.value === undefined ? '—' : formatRial(c.value)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 8, 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>
|
|
|
|
<DataTable
|
|
columns={columns}
|
|
data={items}
|
|
loading={isLoading}
|
|
emptyMessage="هنوز تراکنش مالی ثبت نشده است"
|
|
/>
|
|
|
|
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
|
|
</div>
|
|
);
|
|
}
|