Files
clinicpro/assets/admin/pages/RepresentationFinancePage.tsx
T
hamed ecdefa3c24 feat(tour): add onboarding tours for various admin pages
- Integrated TourButton component into SettingsMenuPage, SkillsPage, SmsWalletPage, StaffPage, StaffSessionDetailPage, StaffTreatmentSessionsPage, SubscriptionPage, TagsSettingsPage, TreatmentCasesPage to enhance user onboarding experience.
- Created new tour definitions for appointments, clinics, staff management, financial management, and patient management, ensuring comprehensive guidance for users navigating the admin panel.
- Updated documentation to reflect the addition of tours and their implementation details.
2026-08-10 10:10:20 +03:30

186 lines
7.8 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';
import TourButton from '../components/ui/TourButton';
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>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">گزارش مالی</h1><TourButton tourId="representation-finance" ready /></div>
<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>
);
}