Files
clinicpro/assets/admin/pages/SecretaryDetailPage.tsx
T
hamed 8d2b0d908a feat: Add online share functionality for secretaries
- Introduced `online_share_enabled` and `online_share_percent` fields in the `doctor_secretaries` table to manage secretary shares from online appointments.
- Added `bank_account` field in the `profiles` table to store user-level IBANs for settlements.
- Created `secretary_earnings` table to track earnings per secretary from online appointments, including a foreign key relationship with `financial_breakdowns`.
- Implemented `SecretaryEarning` entity and repository for managing secretary earnings.
- Developed `SecretaryShareResolver` service to determine which secretaries earn from online payments.
- Added `UserIbanResolver` service to handle user IBAN retrieval and management.
- Created `HasIbansTrait` for entities to manage IBANs in a JSON format.
- Implemented tests for secretary earnings and API endpoints for managing secretary shares and IBANs.
2026-07-25 18:34:18 +03:30

145 lines
6.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 { useEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Link, useParams } from 'react-router-dom';
import { ChevronRightIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { Secretary } from '../types';
import { digitsOnly, formatDate, formatNumber, formatRial } from '../lib/utils';
import PageHeader from '../components/ui/PageHeader';
/** یک ردیف label:value با همان تم کارت‌های موجود. */
function Row({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, padding: '9px 0', borderBottom: '1px solid var(--border)' }}>
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>{label}</span>
<span style={{ fontSize: 13.5, color: 'var(--text)', fontWeight: 500 }}>{value}</span>
</div>
);
}
/**
* جزئیات یک رابطهٔ منشی–پزشک/کلینیک برای ادمین: مشخصات، سهم درآمد نوبت‌های آنلاین
* (فعال‌سازی + درصد) و خلاصهٔ درآمد.
*/
export default function SecretaryDetailPage() {
const { uuid = '' } = useParams();
const qc = useQueryClient();
const [enabled, setEnabled] = useState(false);
const [percent, setPercent] = useState('0');
const { data, isLoading } = useQuery<ApiResponse<{ data: Secretary }>>({
queryKey: ['admin-secretary', uuid],
queryFn: () => api.get(`/api/v1/admin/secretary/${uuid}`),
enabled: !!uuid,
});
const secretary = data?.data?.data;
useEffect(() => {
if (!secretary) return;
setEnabled(!!secretary.online_share_enabled);
setPercent(String(secretary.online_share_percent ?? 0));
}, [data]);
const save = useMutation({
mutationFn: () => api.put(`/api/v1/admin/secretary/${uuid}/online-share`, {
enabled,
percent: Number(percent) || 0,
}),
onSuccess: () => {
toast.success('سهم درآمد منشی ذخیره شد');
qc.invalidateQueries({ queryKey: ['admin-secretary', uuid] });
qc.invalidateQueries({ queryKey: ['secretaries'] });
},
onError: (e: Error) => toast.error(e.message),
});
const invalidPercent = Number(percent) > 100 || (enabled && Number(percent) <= 0);
if (isLoading || !secretary) {
return <div style={{ padding: 24, color: 'var(--text-3)', fontSize: 13 }}>در حال بارگذاری...</div>;
}
return (
<div className="fade-in">
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 14 }}>
<Link to="/admin/secretaries" className="btn sm ghost" style={{ color: 'var(--text-2)' }}>
<ChevronRightIcon style={{ width: 16 }} /> بازگشت
</Link>
</div>
<PageHeader title={secretary.user_name} description="جزئیات منشی و سهم درآمد نوبت‌های آنلاین" />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--gap)' }}>
<div className="card" style={{ padding: 20 }}>
<h2 style={{ fontSize: 15, fontWeight: 700, margin: '0 0 10px' }}>اطلاعات منشی</h2>
<Row label="نام" value={secretary.user_name} />
<Row label="موبایل" value={<span dir="ltr">{secretary.mobile_number}</span>} />
<Row label="پزشک" value={secretary.doctor_name} />
{secretary.clinic_name && <Row label="کلینیک" value={secretary.clinic_name} />}
<Row label="وضعیت" value={
<span style={{ color: secretary.is_active ? 'var(--success)' : 'var(--text-3)', fontWeight: 700 }}>
{secretary.is_active ? 'فعال' : 'غیرفعال'}
</span>
} />
<Row label="تاریخ ثبت" value={formatDate(secretary.created_at)} />
</div>
<div className="card" style={{ padding: 20 }}>
<h2 style={{ fontSize: 15, fontWeight: 700, margin: '0 0 6px' }}>سهم درآمد نوبت‌های آنلاین</h2>
<p style={{ margin: '0 0 14px', fontSize: 12, lineHeight: 1.9, color: 'var(--text-2)' }}>
درصد سهم از <b>مبلغ خالص</b> نوبت محاسبه می‌شود: ابتدا هزینهٔ پیامک و مالیات و
کسورات از مبلغ نوبت کم می‌شود، سپس این درصد اعمال می‌گردد. فقط نوبت‌هایی که
آنلاین ثبت و پرداخت می‌شوند سهم می‌سازند.
</p>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, cursor: 'pointer', marginBottom: 14 }}>
<span className="switch">
<input
type="checkbox"
aria-label="محاسبه درآمد منشی از نوبت‌های آنلاین"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
/>
<span className="switch-track"><span className="switch-thumb" /></span>
</span>
<span style={{ fontSize: 13 }}>محاسبه درآمد از نوبت‌های آنلاین فعال باشد</span>
</label>
<div className="form-row">
<label>درصد سهم منشی</label>
<input
className="input"
inputMode="numeric"
dir="ltr"
aria-label="درصد سهم منشی"
value={percent}
onChange={(e) => setPercent(digitsOnly(e.target.value, 3))}
placeholder="مثلاً: ۵"
/>
{Number(percent) > 100 && <p className="err-text">درصد نمی‌تواند بیشتر از ۱۰۰ باشد</p>}
{enabled && Number(percent) <= 0 && <p className="err-text">برای فعال‌سازی، درصد باید بیشتر از صفر باشد</p>}
</div>
<button
className="btn primary sm"
style={{ marginTop: 14 }}
disabled={save.isPending || invalidPercent}
onClick={() => save.mutate()}
>
{save.isPending ? 'در حال ذخیره...' : 'ذخیره'}
</button>
</div>
<div className="card" style={{ padding: 20 }}>
<h2 style={{ fontSize: 15, fontWeight: 700, margin: '0 0 10px' }}>خلاصه درآمد</h2>
<Row label="کل درآمد" value={formatRial(secretary.earnings?.total_rials ?? 0)} />
<Row label="۳۰ روز گذشته" value={formatRial(secretary.earnings?.this_month_rials ?? 0)} />
<Row label="تعداد نوبت" value={formatNumber(secretary.earnings?.appointments_count ?? 0)} />
</div>
</div>
</div>
);
}