Files
clinicpro/assets/admin/pages/PracticeDomainSettingsPage.tsx
T
hamedandClaude Opus 5 c7fdb92df4 feat(admin): practice domain settings, treatment case list and platform domain CRUD
Completes the panel side. A clinic picks its practice domain in settings, where
the copy says plainly that this is not the specialty label the public site shows;
picking nothing stays valid and changes nothing.

Cases and the unbooked queue share one page rather than two, because they answer
the same question — which patient is where in their course and what is still
owed. The queue explains why booking is not automatic instead of leaving the
reader to wonder.

Platform admins get domain CRUD with a column showing whether a domain has a
dedicated workflow or falls back to the default, so the gap is visible rather
than guessed at; the code field is locked after creation because workflows bind
to it.

Also puts the staff session screens in the sidebar — they were reachable only by
typing the URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:51:21 +03:30

102 lines
4.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { api, ApiError, type ApiResponse } from '../lib/api';
import PageHeader from '../components/ui/PageHeader';
import SearchableSelect from '../components/ui/SearchableSelect';
import { useAuthStore } from '../stores/authStore';
import type { PracticeDomain } from '../types';
/**
* حوزهٔ فعالیت کلینیک.
*
* تخصصِ پزشکی نیست — آن برچسبی است برای سایت عمومی. این یک کلید پیکربندی است که
* تعیین می‌کند کدام workflow درمان صدا زده شود، پس کدش پایدار می‌ماند و فقط ادمین
* پلتفرم می‌تواند حوزهٔ تازه بسازد.
*/
export default function PracticeDomainSettingsPage() {
const qc = useQueryClient();
// محیط جاری با `db_uuid` شناخته می‌شود؛ همان uuid کلینیک است.
const clinicUuid = useAuthStore((s) => s.context?.db_uuid ?? null);
const { data: domainsData, isLoading } = useQuery({
queryKey: ['practice-domains'],
queryFn: () => api.get<ApiResponse<PracticeDomain[]>>('/api/v1/practice-domains'),
staleTime: 300_000,
});
const { data: clinicData } = useQuery({
queryKey: ['clinic-practice-domain', clinicUuid],
queryFn: () => api.get<ApiResponse<{ data: { practice_domain: PracticeDomain | null } }>>(
`/api/v1/clinic/${clinicUuid}`,
),
enabled: !!clinicUuid,
});
const current = clinicData?.data?.data?.practice_domain ?? null;
const [selected, setSelected] = useState<string | null>(null);
useEffect(() => setSelected(current?.uuid ?? null), [current]);
const save = useMutation({
mutationFn: (uuid: string | null) => api.patch<ApiResponse<unknown>>(
`/api/v1/clinic/${clinicUuid}`,
{ practice_domain_uuid: uuid ?? '' },
),
onSuccess: () => {
toast.success('حوزهٔ فعالیت ذخیره شد');
qc.invalidateQueries({ queryKey: ['clinic-practice-domain', clinicUuid] });
},
onError: (e) => toast.error(e instanceof ApiError ? e.message : 'ذخیرهٔ حوزهٔ فعالیت ناموفق بود'),
});
const domains = domainsData?.data ?? [];
const options = domains.map((d) => ({ value: d.uuid, label: d.name }));
const chosen = domains.find((d) => d.uuid === selected) ?? null;
return (
<>
<PageHeader title="حوزهٔ فعالیت" backTo="/admin/settings" />
<div className="card card-pad" style={{ display: 'grid', gap: 14, maxWidth: 560 }}>
<p style={{ margin: 0, fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.9 }}>
حوزهٔ فعالیت تعیین می‌کند سیستم چه فرآیند درمانی برای کلینیک شما اجرا کند. این با
«تخصص» فرق دارد: تخصص برچسبی است که در سایت عمومی دیده می‌شود، این یک تنظیم است.
انتخاب‌نکردنش خطا نیست و کلینیک مثل امروز کار می‌کند.
</p>
{isLoading ? (
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>در حال بارگذاری...</span>
) : (
<>
<SearchableSelect
options={options}
value={selected}
onChange={(v) => setSelected(v === null ? null : String(v))}
placeholder="حوزه‌ای انتخاب نشده"
isClearable
ariaLabel="حوزهٔ فعالیت کلینیک"
/>
{chosen && !chosen.has_workflow && (
<span style={{ fontSize: 12.5, color: 'var(--warning)' }}>
برای این حوزه هنوز فرآیند اختصاصی تعریف نشده است؛ رفتار پیش‌فرض اعمال می‌شود.
</span>
)}
<button
type="button"
className="btn primary"
disabled={save.isPending || !clinicUuid}
onClick={() => save.mutate(selected)}
style={{ justifySelf: 'start' }}
>
{save.isPending ? 'در حال ذخیره...' : 'ذخیره'}
</button>
</>
)}
</div>
</>
);
}