Files
clinicpro/assets/admin/pages/SecretaryEarningsPage.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

125 lines
5.9 KiB
TypeScript

import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import { formatDate, formatNumber, formatRial } from '../lib/utils';
import PageHeader from '../components/ui/PageHeader';
import StatCard from '../components/ui/StatCard';
import DataTable, { Column } from '../components/ui/DataTable';
import Pagination from '../components/ui/Pagination';
import PersianDateInput from '../components/ui/PersianDateInput';
interface EarningsSummary {
enabled: boolean;
share_percent: number;
relations: { relation_uuid: string; doctor_name: string; clinic_name: string | null; share_percent: number }[];
today_rials: number;
this_month_rials: number;
total_rials: number;
appointments_count: number;
wallet_balance_rials: number;
}
interface EarningRow {
uuid: string;
appointment_uuid: string | null;
doctor_name: string | null;
gross_rials: number;
sms_fee_rials: number;
tax_rials: number;
net_after_tax_rials: number;
share_percent: number;
share_rials: number;
created_at: number;
}
/** تاریخ ISO (Y-m-d) → Unix؛ رشتهٔ خالی یعنی بدون فیلتر. */
const toUnix = (iso: string): string => (iso ? String(Math.floor(new Date(iso).getTime() / 1000)) : '');
/** درآمد منشی از نوبت‌های آنلاین: خلاصهٔ روزانه/ماهانه + گزارش سطر-به-سطر. */
export default function SecretaryEarningsPage() {
const [page, setPage] = useState(1);
const [from, setFrom] = useState('');
const [to, setTo] = useState('');
const limit = 15;
const { data: summaryData } = useQuery<ApiResponse<{ data: EarningsSummary }>>({
queryKey: ['secretary-earnings-summary'],
queryFn: () => api.get('/api/v1/secretary/earnings/summary'),
});
const summary = summaryData?.data?.data;
const { data, isLoading } = useQuery({
queryKey: ['secretary-earnings-report', page, from, to],
queryFn: () => {
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
if (from) params.set('from', toUnix(from));
if (to) params.set('to', toUnix(to));
return api.get<PaginatedResponse<EarningRow>>(`/api/v1/secretary/earnings/report?${params}`);
},
});
const rows = data?.data ?? [];
const total = data?.meta?.totalRecords ?? 0;
const columns: Column<EarningRow>[] = [
{ key: 'created_at', header: 'تاریخ', render: (r) => formatDate(r.created_at) },
{ key: 'doctor_name', header: 'پزشک', render: (r) => r.doctor_name ?? '—' },
{ key: 'gross_rials', header: 'مبلغ نوبت', render: (r) => formatRial(r.gross_rials) },
{ key: 'sms_fee_rials', header: 'هزینه پیامک', render: (r) => (r.sms_fee_rials > 0 ? formatRial(r.sms_fee_rials) : '—') },
{ key: 'tax_rials', header: 'مالیات', render: (r) => (r.tax_rials > 0 ? formatRial(r.tax_rials) : '—') },
{ key: 'net_after_tax_rials', header: 'مبلغ خالص', render: (r) => formatRial(r.net_after_tax_rials) },
{ key: 'share_percent', header: 'درصد', render: (r) => `${formatNumber(r.share_percent)}٪` },
{ key: 'share_rials', header: 'سهم شما', render: (r) => (
<b style={{ color: 'var(--success)' }}>{formatRial(r.share_rials)}</b>
) },
];
return (
<div className="fade-in">
<PageHeader title="درآمد نوبت‌های آنلاین" description="سهم شما از نوبت‌هایی که آنلاین ثبت و پرداخت شده‌اند"tourId="secretary-earnings" />
{summary && !summary.enabled ? (
<div className="card" style={{ padding: 20, fontSize: 13, color: 'var(--text-2)', lineHeight: 1.9 }}>
محاسبه درآمد از نوبت‌های آنلاین برای شما فعال نیست. برای فعال‌سازی با مدیر سیستم تماس بگیرید.
</div>
) : (
<>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 'var(--gap)', marginBottom: 'var(--gap)' }}>
<StatCard label="امروز" value={formatRial(summary?.today_rials ?? 0)} tone="green" />
<StatCard label="۳۰ روز گذشته" value={formatRial(summary?.this_month_rials ?? 0)} tone="violet" />
<StatCard label="کل درآمد" value={formatRial(summary?.total_rials ?? 0)} tone="amber" />
<StatCard label="موجودی کیف پول" value={formatRial(summary?.wallet_balance_rials ?? 0)} tone="pink" />
</div>
<div className="card">
<div className="card-pad" style={{ paddingBottom: 0 }}>
<div className="toolbar" style={{ gap: 12, flexWrap: 'wrap' }}>
<div style={{ minWidth: 170 }}>
<label style={{ fontSize: 12.5, color: 'var(--text-3)' }}>از تاریخ</label>
<PersianDateInput value={from} onChange={(v) => { setFrom(v); setPage(1); }} placeholder="انتخاب" />
</div>
<div style={{ minWidth: 170 }}>
<label style={{ fontSize: 12.5, color: 'var(--text-3)' }}>تا تاریخ</label>
<PersianDateInput value={to} onChange={(v) => { setTo(v); setPage(1); }} placeholder="انتخاب" />
</div>
<div style={{ marginInlineStart: 'auto', fontSize: 12.5, color: 'var(--text-2)' }}>
{formatNumber(summary?.appointments_count ?? 0)} نوبت
</div>
</div>
</div>
<DataTable<EarningRow>
columns={columns}
data={rows}
loading={isLoading}
emptyMessage="هنوز درآمدی از نوبت آنلاین ثبت نشده است"
/>
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
</div>
</>
)}
</div>
);
}