import React, { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { ArrowRightIcon, UserPlusIcon, PhoneIcon, IdentificationIcon, AcademicCapIcon, DocumentTextIcon, HeartIcon, ChevronLeftIcon, CheckIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import SearchableSelect from '../components/ui/SearchableSelect'; import MobileInput from '../components/ui/MobileInput'; import PersianDatePicker from '../components/ui/PersianDatePicker'; import { iranMobileSchema } from '../lib/utils'; import { useAuthStore } from '../stores/authStore'; // ── Types ──────────────────────────────────────────────────────────────────── interface SpecialtyOption { id: number; uuid: string; name: string; parent_id: number | null } // ── Schema ─────────────────────────────────────────────────────────────────── const schema = z.object({ mobile: iranMobileSchema, name: z.string().min(2, 'نام حداقل ۲ کاراکتر'), gender: z.enum(['man', 'woman']).optional().or(z.literal('')), degree: z.string().optional().or(z.literal('')), medical_system_code: z.string().max(30).optional().or(z.literal('')), info: z.string().max(2000).optional().or(z.literal('')), }); type FormValues = z.infer; // ── Constants ───────────────────────────────────────────────────────────────── const DEGREE_OPTIONS = [ { value: 'general', label: 'پزشک عمومی' }, { value: 'specialist', label: 'متخصص' }, { value: 'expert', label: 'فوق تخصص' }, { value: 'subspecialistplus', label: 'فلوشیپ' }, ]; const GENDER_OPTIONS = [ { value: 'man', label: 'مرد', icon: '♂' }, { value: 'woman', label: 'زن', icon: '♀' }, ]; // ── Field ───────────────────────────────────────────────────────────────────── function Field({ label, required, error, hint, children }: { label: string; required?: boolean; error?: string; hint?: string; children: React.ReactNode; }) { return (
{children} {hint && !error &&

{hint}

} {error &&

{error}

}
); } // ── Specialty Picker ────────────────────────────────────────────────────────── // // منطق انتخاب: // • تخصص با فرزند → کلیک روی فرزند: parent_id + child_id هر دو ذخیره می‌شوند // در هر گروه فقط یک فرزند قابل انتخاب است (جایگزین می‌شود) // • تخصص بدون فرزند → فقط خودش انتخاب می‌شود // • حذف chip: parent + child هر دو پاک می‌شوند interface SelectedEntry { parentId: number | null; childId: number } function SpecialtyPicker({ selected, onChange, specialties }: { selected: number[]; onChange: (ids: number[]) => void; specialties: SpecialtyOption[]; }) { const [activeParentId, setActiveParentId] = useState(null); const parents = useMemo(() => specialties.filter(s => s.parent_id === null), [specialties]); const childMap = useMemo(() => { const m: Record = {}; specialties.forEach(s => { if (s.parent_id !== null) { if (!m[s.parent_id]) m[s.parent_id] = []; m[s.parent_id].push(s); } }); return m; }, [specialties]); const activeChildren = activeParentId !== null ? (childMap[activeParentId] ?? []) : []; // برای هر گروه، child انتخاب‌شده را پیدا می‌کند const selectedChildOfParent = (parentId: number): number | null => { const kids = childMap[parentId] ?? []; return kids.find(k => selected.includes(k.id))?.id ?? null; }; // آیا یک والد بدون فرزند انتخاب شده (مستقیم) const isRootSelected = (parentId: number): boolean => !(childMap[parentId]?.length) && selected.includes(parentId); const selectChild = (child: SpecialtyOption) => { const parentId = child.parent_id!; if (selected.includes(child.id)) { onChange([]); } else { onChange([parentId, child.id]); } }; const selectRoot = (root: SpecialtyOption) => { if (selected.includes(root.id)) { onChange([]); } else { setActiveParentId(null); onChange([root.id]); } }; const removeEntry = () => { onChange([]); setActiveParentId(null); }; // ساخت chip ها: از selected فقط child‌ها + root‌های مستقیم را نمایش بده const chips: SelectedEntry[] = useMemo(() => { return selected .map(id => { const s = specialties.find(x => x.id === id); if (!s) return null; if (s.parent_id !== null) return { parentId: s.parent_id, childId: s.id }; // اگر parent است و child‌ای از آن انتخاب نشده → root مستقیم const hasSelectedChild = (childMap[s.id] ?? []).some(k => selected.includes(k.id)); if (!hasSelectedChild && !(childMap[s.id]?.length)) return { parentId: null, childId: s.id }; return null; // parent‌هایی که فرزند دارند و فرزندشان انتخاب شده: در chip فرزند نمایش داده می‌شوند }) .filter(Boolean) as SelectedEntry[]; }, [selected, specialties, childMap]); return (
{/* Selected chips */} {chips.length > 0 && (
{chips.map(({ parentId, childId }) => { const child = specialties.find(x => x.id === childId); const parent = parentId !== null ? specialties.find(x => x.id === parentId) : null; if (!child) return null; const label = parent ? `${parent.name} — ${child.name}` : child.name; return ( {label} ); })}
)}
{/* Parents column */}
گروه تخصصی
{parents.map(p => { const hasChildren = (childMap[p.id]?.length ?? 0) > 0; const childSel = hasChildren ? selectedChildOfParent(p.id) : null; const rootSel = !hasChildren && isRootSelected(p.id); const isMarked = childSel !== null || rootSel; const isActive = activeParentId === p.id; const hasSelection = selected.length > 0; const isDisabled = hasSelection && !isMarked; return ( ); })}
{/* Children column */}
{activeParentId === null ? (
یک گروه تخصصی انتخاب کنید
) : activeChildren.length === 0 ? (
زیرمجموعه‌ای یافت نشد
) : ( <>
انتخاب تخصص — یک مورد
{activeChildren.map(s => { const checked = selected.includes(s.id); return ( ); })} )}
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function DoctorFormPage() { const navigate = useNavigate(); const primaryRole = useAuthStore(s => s.primaryRole); const createDoctorEndpoint = primaryRole === 'representation' ? '/api/v1/representation/doctor' : '/api/v1/admin/doctors'; const [selectedSpecialties, setSelectedSpecialties] = useState([]); const [gender, setGender] = useState<'man' | 'woman' | ''>(''); const [activityDate, setActivityDate] = useState(''); const specialtiesQ = useQuery({ queryKey: ['specialties-list'], queryFn: () => api.get>('/api/v1/specialties'), staleTime: 300_000, }); const specialties: SpecialtyOption[] = useMemo( () => (specialtiesQ.data?.data as any)?.data ?? specialtiesQ.data?.data ?? [], [specialtiesQ.data], ); const { register, handleSubmit, control, formState: { errors } } = useForm({ resolver: zodResolver(schema), }); const createMut = useMutation({ mutationFn: (values: FormValues) => api.post>(createDoctorEndpoint, { mobile: values.mobile, name: values.name, gender: gender || undefined, degree: values.degree || undefined, medical_system_code: values.medical_system_code || undefined, info: values.info || undefined, ...(activityDate ? { activity_time: Math.floor(new Date(`${activityDate}T12:00:00`).getTime() / 1000) } : {}), specialties: selectedSpecialties, }), onSuccess: (res) => { const uuid = (res?.data as any)?.uuid ?? res?.data?.uuid; toast.success('پزشک با موفقیت اضافه شد'); if (uuid) navigate(`/admin/doctors/${uuid}`); else navigate('/admin/doctors'); }, onError: (e: Error) => toast.error(e.message), }); const onSubmit = (values: FormValues) => createMut.mutate(values); return (
{/* Breadcrumb */}
/ افزودن پزشک جدید
{/* ── Section: اطلاعات حساب ──────────────────────────────── */}
} title="اطلاعات حساب" />
{/* ── Section: اطلاعات حرفه‌ای ────────────────────────────── */}
} title="اطلاعات حرفه‌ای" /> {/* Gender toggle */}
{GENDER_OPTIONS.map(o => ( ))}
( field.onChange(v ?? '')} placeholder="انتخاب کنید" isClearable /> )} />