- 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.
125 lines
5.9 KiB
TypeScript
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>
|
|
);
|
|
}
|