Files
clinicpro/assets/admin/pages/RepresentationFinancePage.tsx
T
hamed 1c81c6f2e5 Add JSON files for Payment Gateway Interface and API documentation updates
- Created a new JSON file for the PaymentGatewayInterface.php, detailing its methods and return types.
- Added a JSON file for payment.md, documenting various API endpoints and their parameters, responses, and errors.
- Introduced a JSON file for fix-admin-modal-and-calendar.md, outlining issues and solutions related to admin modal and calendar functionalities.
2026-07-02 21:22:14 +03:30

185 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
BanknotesIcon, ReceiptPercentIcon, ChatBubbleLeftRightIcon,
CalculatorIcon, ArrowPathIcon, DocumentTextIcon,
} from '@heroicons/react/24/outline';
import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import { formatRial, formatNumber, 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 RANGES: { key: RangeKey; label: string }[] = [
{ key: 'today', label: 'امروز' },
{ key: 'week', label: '۷ روز اخیر' },
{ key: 'month', label: '۳۰ روز اخیر' },
{ key: 'all', label: 'همه' },
];
export default function RepresentationFinancePage() {
const [range, setRange] = useState<RangeKey>('month');
const [page, setPage] = useState(1);
const limit = 15;
const { data, isLoading, isFetching, refetch } = 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;
// جمعِ ردیف‌های صفحه‌ی جاری (نمایش سریع؛ دقیق برای همین صفحه).
const pageSums = useMemo(() => items.reduce(
(acc, r) => {
acc.share += r.representation_share_rials;
acc.gross += r.gross_rials;
acc.tax += r.tax_rials;
acc.sms += r.sms_fee_rials;
return acc;
},
{ share: 0, gross: 0, tax: 0, sms: 0 },
), [items]);
const cards = [
{ label: 'سهم نماینده (این صفحه)', value: formatRial(pageSums.share), icon: BanknotesIcon, color: 'var(--primary)', bg: 'var(--primary-soft)' },
{ label: 'مبلغ نوبت‌ها', value: formatRial(pageSums.gross), icon: CalculatorIcon, color: 'var(--info)', bg: 'var(--info-bg)' },
{ label: 'مالیات', value: formatRial(pageSums.tax), icon: ReceiptPercentIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' },
{ label: 'هزینه پیامک', value: formatRial(pageSums.sms), icon: ChatBubbleLeftRightIcon, color: 'var(--violet)', bg: 'var(--violet-bg)' },
];
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>
<button className="btn ghost sm" onClick={() => refetch()} disabled={isFetching}>
<ArrowPathIcon style={{ width: 14, height: 14 }} />
به‌روزرسانی
</button>
</div>
{/* فیلتر بازه — segmented */}
<div className="seg" style={{ marginBottom: 'var(--gap)' }}>
{RANGES.map(r => (
<button
key={r.key}
className={range === r.key ? 'on' : ''}
onClick={() => { setRange(r.key); setPage(1); }}
>
{r.label}
</button>
))}
</div>
{/* کارت‌های خلاصه */}
<div className="stat-grid" style={{ marginBottom: 'var(--gap)' }}>
{cards.map(c => (
<div key={c.label} className="stat">
<div className="ico" style={{ background: c.bg, color: c.color }}>
<c.icon style={{ width: 20, height: 20 }} />
</div>
<div className="lbl">{c.label}</div>
<div className="val" style={{ fontSize: 15, color: c.color }}>
{isLoading ? '—' : c.value}
</div>
</div>
))}
</div>
{/* جدول تراکنش‌ها */}
<div className="card" style={{ overflow: 'hidden' }}>
<div className="card-pad" style={{ borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h3 style={{ fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 }}>
<DocumentTextIcon style={{ width: 18, height: 18, color: 'var(--text-3)' }} />
تراکنش‌ها
</h3>
{!isLoading && <span className="muted" style={{ fontSize: 12.5 }}>{formatNumber(total)} ردیف</span>}
</div>
<div style={{ overflowX: 'auto' }}>
<div className="table-wrap"><table className="tbl tbl-zebra" style={{ width: '100%', minWidth: 720 }}>
<thead>
<tr>
<th>پزشک</th>
<th>مبلغ نوبت</th>
<th>مالیات</th>
<th>هزینه پیامک</th>
<th>درصد پورسانت</th>
<th>سهم نماینده</th>
<th>تاریخ</th>
</tr>
</thead>
<tbody>
{isLoading && Array.from({ length: 5 }).map((_, i) => (
<tr key={i}>
{Array.from({ length: 7 }).map((__, j) => (
<td key={j}><div className="skeleton" style={{ height: 13, borderRadius: 6, width: `${50 + (j * 13) % 40}%` }} /></td>
))}
</tr>
))}
{!isLoading && items.length === 0 && (
<tr>
<td colSpan={7}>
<div style={{ textAlign: 'center', padding: '40px 16px', color: 'var(--text-3)' }}>
<BanknotesIcon style={{ width: 36, height: 36, margin: '0 auto 10px', opacity: .4 }} />
<div style={{ fontSize: 14 }}>در این بازه درآمدی ثبت نشده است</div>
<div style={{ fontSize: 12.5, marginTop: 4 }}>با ثبت و پرداخت نوبت از دامنه‌ی شما، پورسانت اینجا نمایش داده می‌شود.</div>
</div>
</td>
</tr>
)}
{!isLoading && items.map(r => (
<tr key={r.uuid}>
<td style={{ fontWeight: 500 }}>{r.doctor_name ?? '—'}</td>
<td>{formatRial(r.gross_rials)}</td>
<td className="muted">{formatRial(r.tax_rials)}</td>
<td className="muted">{formatRial(r.sms_fee_rials)}</td>
<td><span className="badge gray">{formatNumber(r.commission_percent)}٪</span></td>
<td style={{ fontWeight: 700, color: 'var(--primary)' }}>{formatRial(r.representation_share_rials)}</td>
<td className="muted" style={{ whiteSpace: 'nowrap' }}>{formatDate(r.created_at)}</td>
</tr>
))}
</tbody>
</table></div>
</div>
</div>
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
</div>
);
}