feat: add ROLE_REPRESENTATION access to admin panel for managing doctors and clinics

- Updated authStore to include 'representation' role.
- Modified DoctorFormPage and DoctorsPage to handle different endpoints based on user role.
- Created new RepresentationActionController for handling doctor and clinic creation by representatives.
- Added new API endpoints for representatives to manage doctors, clinics, and view appointments.
- Updated documentation to reflect new role and API changes.
This commit is contained in:
hamed
2026-06-19 13:20:40 +03:30
parent a8d36d7455
commit fe73fa1a05
14 changed files with 778 additions and 19 deletions
+90 -4
View File
@@ -991,16 +991,102 @@ function SecretaryDashboard() {
);
}
function RepresentationDashboard() {
const now = new Date();
const jYear = Number(new Intl.DateTimeFormat('en-US-u-ca-persian', { year: 'numeric' }).format(now));
const jMonth = Number(new Intl.DateTimeFormat('en-US-u-ca-persian', { month: 'numeric' }).format(now));
const meQ = useQuery({
queryKey: ['representation-me'],
queryFn: () => api.get<ApiResponse<{ data: { uuid: string; full_name: string; commission_percent: string } }>>('/api/v1/representation/me'),
staleTime: 300_000,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rep = useMemo<any>(() => (meQ.data?.data as any)?.data ?? meQ.data?.data, [meQ.data]);
const repUuid: string | undefined = rep?.uuid;
const monthlyQ = useQuery({
queryKey: ['representation-monthly', repUuid, jYear, jMonth],
queryFn: () => api.get<ApiResponse<{ data: { stats: { total_appointments: number; total_revenue_rials: number; commission_rials: number } } }>>(
`/api/v1/representation/${repUuid}/dashboard/monthly?year=${jYear}&month=${jMonth}`,
),
enabled: !!repUuid,
staleTime: 120_000,
});
const yearlyQ = useQuery({
queryKey: ['representation-yearly', repUuid, jYear],
queryFn: () => api.get<ApiResponse<{ data: { stats: { total_appointments: number; total_revenue_rials: number; commission_rials: number } } }>>(
`/api/v1/representation/${repUuid}/dashboard/yearly?year=${jYear}`,
),
enabled: !!repUuid,
staleTime: 120_000,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const monthly = useMemo<any>(() => ((monthlyQ.data?.data as any)?.data ?? monthlyQ.data?.data)?.stats, [monthlyQ.data]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const yearly = useMemo<any>(() => ((yearlyQ.data?.data as any)?.data ?? yearlyQ.data?.data)?.stats, [yearlyQ.data]);
if (meQ.isLoading) return <LoadingSkeleton />;
const today = new Date().toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric' });
const cards = [
{ label: 'نوبت‌های این ماه', value: formatNumber(monthly?.total_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' },
{ label: 'کمیسیون این ماه', value: formatRial(monthly?.commission_rials ?? 0), icon: CreditCardIcon, color: 'var(--success)', bg: 'var(--success-bg)' },
{ label: 'نوبت‌های امسال', value: formatNumber(yearly?.total_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--info)', bg: 'var(--info-bg)' },
{ label: 'کمیسیون امسال', value: formatRial(yearly?.commission_rials ?? 0), icon: CreditCardIcon, 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: 2 }}>{today} · {rep?.full_name ?? ''}</div>
</div>
<button className="btn ghost sm" onClick={() => { monthlyQ.refetch(); yearlyQ.refetch(); }}>
<ArrowPathIcon style={{ width: 14, height: 14 }} />
بهروزرسانی
</button>
</div>
<div className="stat-grid" style={{ gridTemplateColumns: 'repeat(2, 1fr)' }}>
{cards.map(c => (
<div key={c.label} className="stat">
<div className="ico" style={{ background: c.bg, color: c.color }}>
<c.icon style={{ width: 21, height: 21 }} />
</div>
<div className="lbl">{c.label}</div>
<div className="val">{c.value}</div>
</div>
))}
</div>
<div className="card card-pad" style={{ marginTop: 'var(--gap)' }}>
<div className="card-title-row">
<h3 style={{ fontSize: 16 }}>دسترسی سریع</h3>
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<Link to="/admin/doctors" className="btn sm">پزشکان من</Link>
<Link to="/admin/clinics" className="btn sm">کلینیکها</Link>
<Link to="/admin/appointments" className="btn sm">نوبتها</Link>
</div>
</div>
</div>
);
}
// ── Main Dispatcher ───────────────────────────────────────────────────────
export default function DashboardPage() {
const primaryRole = useAuthStore(s => s.primaryRole);
if (!primaryRole) return <LoadingSkeleton />;
if (primaryRole === 'admin') return <AdminDashboard />;
if (primaryRole === 'clinic') return <ClinicDashboard />;
if (primaryRole === 'doctor') return <DoctorDashboard />;
if (primaryRole === 'secretary') return <SecretaryDashboard />;
if (primaryRole === 'admin') return <AdminDashboard />;
if (primaryRole === 'clinic') return <ClinicDashboard />;
if (primaryRole === 'doctor') return <DoctorDashboard />;
if (primaryRole === 'secretary') return <SecretaryDashboard />;
if (primaryRole === 'representation') return <RepresentationDashboard />;
return <AdminDashboard />;
}