import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { ArrowRightIcon, PencilIcon, TrashIcon, XCircleIcon, PhoneIcon, CalendarIcon, ClipboardDocumentIcon, EllipsisVerticalIcon, StarIcon, BuildingOfficeIcon, MapPinIcon, AcademicCapIcon, UserIcon, PlusIcon, CameraIcon, ChevronDownIcon, ChevronRightIcon, ChevronLeftIcon, CheckCircleIcon, XMarkIcon, ExclamationTriangleIcon, HeartIcon, CheckIcon, IdentificationIcon, DocumentTextIcon, } from '@heroicons/react/24/outline'; import { StarIcon as StarSolid } from '@heroicons/react/24/solid'; import { toast } from 'sonner'; import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { api, ApiError } from '../lib/api'; import { useAuthStore } from '../stores/authStore'; import type { ApiResponse } from '../lib/api'; import { formatNumber } from '../lib/utils'; import Modal from '../components/ui/Modal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import NotificationMobileCard from '../components/ui/NotificationMobileCard'; import GlobalSearchableSelect from '../components/ui/SearchableSelect'; // Fix leaflet default marker icons delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', }); // ── Types ────────────────────────────────────────────────────────────────── interface DoctorDetail { id: string; uuid: string; name: string; gender: string | null; experience: number; activity_time: string | null; medical_system_code: string | null; detail: string | null; degree: string | null; active: boolean; specialties: { id: string; uuid: string; name: string; parent_id: string | null }[]; img: { url: string; fid: number }[]; expertise: { id: string; uuid: string; name: string }[]; satisfaction: string; point: string; address: AddressData[]; state: { id: string; name: string }[]; city: { id: string; name: string }[]; clinics: { id: string; uuid: string; name: string; address: string | null; telephone: string | null }[]; } interface SpecialtyOpt { id: number; uuid: string; name: string; parent_id: number | null; } interface ServiceOpt { id: number; uuid: string; name: string; specialty_id: number | null; } interface ProvinceOpt { id: number; uuid: string; name: string; } interface CityOpt { id: number; uuid: string; name: string; } interface ImageFileData { fid: number; uuid: string; url: string; filename: string; filemime: string; filesize: number; } interface AddressData { id: string; uuid: string; type: 'personal' | 'clinic'; clinic_id: string | null; clinic_name: string | null; name: string | null; address: string | null; telephone: string | null; map: { latitude: string | null; longitude: string | null }; city: { id: string; name: string } | null; province: { id: string; name: string } | null; } // ── Schedule types ───────────────────────────────────────────────────────── interface SlotConfig { start: string; end: string; duration: number; } interface DateOverrideData { uuid: string; doctor_uuid: string; date: number; active: boolean; reason: string | null; custom_slots: SessionConfig[]; } interface HolidayData { uuid: string; doctor_uuid: string; start_date: number; end_date: number; active: boolean; reason: string | null; } interface SessionConfig { active: boolean; location_id: number | null; start_time: string; end_time: string; duration_per_patient: number; has_rest: boolean; rest_interval: number; time_to_rest: number; patient_limit: number | null; } interface NewDayConfig { sessions: SessionConfig[]; } type NewScheduleMap = Record; interface WeeklyScheduleData { uuid: string; doctor_uuid: string; schedule: NewScheduleMap; } // ── Constants ────────────────────────────────────────────────────────────── const DEGREE_LABELS: Record = { general: 'عمومی', specialist: 'متخصص', expert: 'فوق تخصص', subspecialistplus: 'فلوشیپ', }; const DEGREE_OPTIONS = [ { value: 'general', label: 'عمومی' }, { value: 'specialist', label: 'متخصص' }, { value: 'expert', label: 'فوق تخصص' }, { value: 'subspecialistplus', label: 'فلوشیپ' }, ]; const GENDER_OPTIONS = [{ value: 'man', label: 'مرد' }, { value: 'woman', label: 'زن' }]; const AVATAR_COLORS = [ 'from-blue-500 to-cyan-500', 'from-violet-500 to-purple-600', 'from-emerald-500 to-teal-600', 'from-rose-500 to-pink-600', 'from-amber-500 to-orange-600', ]; const SPECIALTY_COLORS = [ 'bg-red-100 dark:bg-red-400/10 text-red-700 dark:text-red-300', 'bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300', 'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300', 'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300', 'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300', 'bg-pink-100 dark:bg-pink-400/10 text-pink-700 dark:text-pink-300', ]; // ── Persian (Jalali) date utilities ─────────────────────────────────────── // eslint-disable-next-line @typescript-eslint/no-require-imports const jalaali = require('jalaali-js') as { toJalaali: (gy: number, gm: number, gd: number) => { jy: number; jm: number; jd: number }; toGregorian: (jy: number, jm: number, jd: number) => { gy: number; gm: number; gd: number }; jalaaliMonthLength: (jy: number, jm: number) => number; }; const JALALI_MONTHS = ['فروردین','اردیبهشت','خرداد','تیر','مرداد','شهریور','مهر','آبان','آذر','دی','بهمن','اسفند']; const JALALI_DAYS_SHORT = ['ش','ی','د','س','چ','پ','ج']; function toPersianNums(n: number | string): string { return String(n).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]); } function gToJ(gDate: string): { jy: number; jm: number; jd: number } | null { if (!gDate) return null; const [gy, gm, gd] = gDate.split('-').map(Number); return jalaali.toJalaali(gy, gm, gd); } function jToG(jy: number, jm: number, jd: number): string { const { gy, gm, gd } = jalaali.toGregorian(jy, jm, jd); return `${gy}-${String(gm).padStart(2,'0')}-${String(gd).padStart(2,'0')}`; } function formatPersianDate(gDate: string): string { const j = gToJ(gDate); if (!j) return gDate; return `${toPersianNums(j.jd)} ${JALALI_MONTHS[j.jm - 1]} ${toPersianNums(j.jy)}`; } function jFirstDayOfWeek(jy: number, jm: number): number { const { gy, gm, gd } = jalaali.toGregorian(jy, jm, 1); const dow = new Date(gy, gm - 1, gd).getDay(); // 0=Sunday return (dow + 1) % 7; // 0=Saturday, 1=Sunday, ..., 6=Friday } function todayGregorian(): string { return new Date().toISOString().slice(0, 10); } // ── Persian Date Picker ──────────────────────────────────────────────────── function PersianDateInput({ value, onChange, minDate, placeholder }: { value: string; onChange: (v: string) => void; minDate?: string; placeholder?: string; }) { const [open, setOpen] = useState(false); const [viewJy, setViewJy] = useState(0); const [viewJm, setViewJm] = useState(0); const [rect, setRect] = useState(null); const btnRef = useRef(null); const dropRef = useRef(null); useEffect(() => { if (!open) return; const j = gToJ(value || todayGregorian()); if (j) { setViewJy(j.jy); setViewJm(j.jm); } }, [open, value]); useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { if (dropRef.current && !dropRef.current.contains(e.target as Node) && btnRef.current && !btnRef.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', onDown); return () => document.removeEventListener('mousedown', onDown); }, [open]); const openPicker = () => { if (!btnRef.current) return; setRect(btnRef.current.getBoundingClientRect()); setOpen(v => !v); }; const prevMonth = () => viewJm === 1 ? (setViewJy(y => y - 1), setViewJm(12)) : setViewJm(m => m - 1); const nextMonth = () => viewJm === 12 ? (setViewJy(y => y + 1), setViewJm(1)) : setViewJm(m => m + 1); const selectDay = (jd: number) => { onChange(jToG(viewJy, viewJm, jd)); setOpen(false); }; const selectedJ = gToJ(value); const minJ = minDate ? gToJ(minDate) : null; const todayJ = gToJ(todayGregorian()); const daysInMonth = viewJy && viewJm ? jalaali.jalaaliMonthLength(viewJy, viewJm) : 0; const firstDow = viewJy && viewJm ? jFirstDayOfWeek(viewJy, viewJm) : 0; const isSelected = (jd: number) => !!selectedJ && selectedJ.jy === viewJy && selectedJ.jm === viewJm && selectedJ.jd === jd; const isToday = (jd: number) => !!todayJ && todayJ.jy === viewJy && todayJ.jm === viewJm && todayJ.jd === jd; const isDisabled = (jd: number) => { if (!minJ) return false; if (viewJy < minJ.jy) return true; if (viewJy === minJ.jy && viewJm < minJ.jm) return true; if (viewJy === minJ.jy && viewJm === minJ.jm && jd < minJ.jd) return true; return false; }; const dropStyle: React.CSSProperties = rect ? { position: 'fixed', top: rect.bottom + 4, left: rect.left, zIndex: 9999, width: 288 } : {}; return (
{open && createPortal(
{viewJm > 0 ? JALALI_MONTHS[viewJm - 1] : ''} {toPersianNums(viewJy)}
{JALALI_DAYS_SHORT.map(d => (
{d}
))}
{Array.from({ length: firstDow }).map((_, i) =>
)} {Array.from({ length: daysInMonth }).map((_, i) => { const jd = i + 1; const sel = isSelected(jd); const tod = isToday(jd); const dis = isDisabled(jd); return ( ); })}
, document.body )}
); } const SCHEDULE_DAYS = [ { key: '0', label: 'شنبه' }, { key: '1', label: 'یکشنبه' }, { key: '2', label: 'دوشنبه' }, { key: '3', label: 'سه‌شنبه' }, { key: '4', label: 'چهارشنبه' }, { key: '5', label: 'پنجشنبه' }, { key: '6', label: 'جمعه' }, ]; const DURATION_OPTS = [10, 15, 20, 30, 45, 60]; const DEFAULT_SESSION: SessionConfig = { active: true, location_id: null, start_time: '09:00', end_time: '13:00', duration_per_patient: 20, has_rest: false, rest_interval: 60, time_to_rest: 10, patient_limit: null, }; const EMPTY_NEW_SCHEDULE: NewScheduleMap = Object.fromEntries( SCHEDULE_DAYS.map(d => [d.key, { sessions: [] }]) ); function parseMinutes(t: string): number { const [h, m] = t.split(':').map(Number); return h * 60 + m; } function hasOverlap(sessions: SessionConfig[]): boolean { const active = sessions.filter(s => s.active); const sorted = [...active].sort((a, b) => parseMinutes(a.start_time) - parseMinutes(b.start_time)); for (let i = 0; i < sorted.length - 1; i++) { if (parseMinutes(sorted[i].end_time) > parseMinutes(sorted[i + 1].start_time)) return true; } return false; } function addMinutes(time: string, mins: number): string { const total = Math.min(parseMinutes(time) + mins, 23 * 60 + 59); const h = Math.floor(total / 60).toString().padStart(2, '0'); const m = (total % 60).toString().padStart(2, '0'); return `${h}:${m}`; } function calcSlotCount(session: SessionConfig): number { const totalMins = parseMinutes(session.end_time) - parseMinutes(session.start_time); if (totalMins <= 0 || session.duration_per_patient <= 0) return 0; const dur = session.duration_per_patient; const limit = session.patient_limit ?? Infinity; let current = 0, elapsedWork = 0, count = 0; while (current + dur <= totalMins) { if (count >= limit) break; if (session.has_rest && session.rest_interval > 0 && elapsedWork > 0 && elapsedWork >= session.rest_interval) { current += session.time_to_rest; elapsedWork = 0; continue; } current += dur; elapsedWork += dur; count++; } return count; } function tsToDate(ts: number): string { return new Date(ts * 1000).toISOString().slice(0, 10); } // ── Image upload ─────────────────────────────────────────────────────────── async function uploadDoctorImage(file: File): Promise { const token = useAuthStore.getState().token; const res = await fetch('/file/upload/clinic_pro/doctor/field_image', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${file.name}"`, ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: file, }); const json = await res.json().catch(() => ({})); if (res.status === 401) { useAuthStore.getState().logout(); window.location.replace('/admin/login'); throw new Error('نشست منقضی شده است'); } if (!res.ok) throw new Error(json?.errors?.[0]?.message ?? 'خطا در آپلود تصویر'); return json.data as ImageFileData; } // ── Avatar with upload ───────────────────────────────────────────────────── function DoctorAvatar({ name, img, idx, onUpload, uploading }: { name: string; img: string | null; idx: number; onUpload?: (f: File) => void; uploading: boolean; }) { const ref = useRef(null); const initials = name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2) || 'Dr'; return (
onUpload && ref.current?.click()} > {img ? {name} :
{initials}
} {onUpload && (
{uploading ? : }
)} {onUpload && ( { const f = e.target.files?.[0]; if (f) onUpload(f); e.target.value = ''; }} /> )}
); } // ── Info Card ────────────────────────────────────────────────────────────── function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }: { icon: React.ElementType; label: string; value: React.ReactNode; mono?: boolean; copyable?: boolean; }) { return (

{label}

{value ?? ثبت نشده}

{copyable && typeof value === 'string' && ( )}
); } function StarRating({ rate }: { rate: number }) { return (
{Array.from({ length: 5 }).map((_, i) => i < Math.round(rate) ? : )} {rate.toFixed(1)}
); } // ── Map Picker ───────────────────────────────────────────────────────────── const IRAN_CENTER: [number, number] = [32.4279, 53.6880]; async function geocodeCityInIran(cityName: string): Promise<[number, number] | null> { try { const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(cityName + ',ایران')}&format=json&countrycodes=ir&limit=1`; const res = await fetch(url, { headers: { 'Accept-Language': 'fa' } }); const data = await res.json(); if (data?.[0]) return [parseFloat(data[0].lat), parseFloat(data[0].lon)]; return null; } catch { return null; } } function MapClickHandler({ onPick }: { onPick: (lat: number, lng: number) => void }) { useMapEvents({ click: e => onPick(e.latlng.lat, e.latlng.lng) }); return null; } function MapController({ flyTarget }: { flyTarget: [number, number] | null }) { const map = useMap(); useEffect(() => { if (flyTarget) map.flyTo(flyTarget, 12, { duration: 1.2 }); }, [flyTarget, map]); return null; } function MapPicker({ lat, lng, onChange, flyTarget }: { lat: number | null; lng: number | null; onChange: (lat: number, lng: number) => void; flyTarget?: [number, number] | null; }) { const pos: [number, number] | null = lat !== null && lng !== null ? [lat, lng] : null; const center: [number, number] = pos ?? IRAN_CENTER; return (
{pos && }
); } // ── Searchable Select ────────────────────────────────────────────────────── function SearchableSelect({ options, value, onChange, placeholder, disabled = false }: { options: { value: number; label: string }[]; value: number | null; onChange: (value: number | null, label: string | null) => void; placeholder?: string; disabled?: boolean; }) { const [open, setOpen] = useState(false); const [q, setQ] = useState(''); const [rect, setRect] = useState(null); const btnRef = useRef(null); const dropRef = useRef(null); const filtered = useMemo( () => q ? options.filter(o => o.label.includes(q)) : options, [options, q] ); const selected = useMemo(() => options.find(o => o.value === value) ?? null, [options, value]); const openDropdown = () => { if (disabled || !btnRef.current) return; setRect(btnRef.current.getBoundingClientRect()); setOpen(v => !v); setQ(''); }; useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { if ( dropRef.current && !dropRef.current.contains(e.target as Node) && btnRef.current && !btnRef.current.contains(e.target as Node) ) setOpen(false); }; document.addEventListener('mousedown', onDown); return () => document.removeEventListener('mousedown', onDown); }, [open]); const dropStyle: React.CSSProperties = rect ? { position: 'fixed', top: rect.bottom + 4, left: rect.left, width: rect.width, zIndex: 9999 } : {}; return (
{open && createPortal(
setQ(e.target.value)} placeholder="جستجو..." className="cp-input text-sm h-8" />
{filtered.map(o => ( ))} {filtered.length === 0 && (

نتیجه‌ای یافت نشد

)}
, document.body )}
); } // ── Hierarchical Specialty Picker ────────────────────────────────────────── function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: { selected: number[]; onChange: (ids: number[]) => void; specialties: SpecialtyOpt[]; }) { const [q, setQ] = useState(''); const [openParents, setOpenParents] = useState>(new Set()); const parents = useMemo(() => specialties.filter(s => s.parent_id === null), [specialties]); const childMap = useMemo(() => { const m = new Map(); specialties.filter(s => s.parent_id !== null).forEach(s => { const arr = m.get(s.parent_id!) ?? []; arr.push(s); m.set(s.parent_id!, arr); }); return m; }, [specialties]); const filteredFlat = useMemo(() => q ? specialties.filter(s => s.name.includes(q)) : [], [q, specialties]); const toggleParent = (id: number) => setOpenParents(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); const toggleSelect = (id: number) => onChange(selected.includes(id) ? selected.filter(x => x !== id) : [...selected, id]); const selectedLabels = useMemo( () => selected.map(id => ({ id, name: specialties.find(s => s.id === id)?.name ?? '' })).filter(x => x.name), [selected, specialties] ); return (
setQ(e.target.value)} placeholder="جستجوی تخصص..." className="cp-input text-sm h-8" />
{selectedLabels.length > 0 && (
{selectedLabels.map(({ id, name }) => ( {name} ))}
)}
{q ? ( filteredFlat.length > 0 ? filteredFlat.map(s => ( )) :

نتیجه‌ای یافت نشد

) : ( parents.map(parent => { const children = childMap.get(parent.id) ?? []; const isOpen = openParents.has(parent.id); const numSel = children.filter(c => selected.includes(c.id)).length; return (
children.length > 0 ? toggleParent(parent.id) : toggleSelect(parent.id)}> {children.length > 0 ? (isOpen ? : ) : } 0 ? 'text-primary-600 dark:text-primary-400' : 'text-slate-700 dark:text-slate-300'}`}> {parent.name} {numSel > 0 && ( {numSel} انتخاب )}
{isOpen && children.map(child => ( ))}
); }) )}
); } // ── Services Picker — filtered by selected specialties ───────────────────── function ServicesPicker({ selected, onChange, services, specialties, selectedSpecialtyIds }: { selected: number[]; onChange: (ids: number[]) => void; services: ServiceOpt[]; specialties: SpecialtyOpt[]; selectedSpecialtyIds: number[]; }) { const [q, setQ] = useState(''); const [activeTab, setActiveTab] = useState('all'); // Only services whose specialty is currently selected const relevantServices = useMemo(() => { if (selectedSpecialtyIds.length === 0) return []; return services.filter(s => s.specialty_id !== null && selectedSpecialtyIds.includes(s.specialty_id)); }, [services, selectedSpecialtyIds]); // Specialties that actually have services const tabs = useMemo(() => { const specIds = [...new Set(relevantServices.map(s => s.specialty_id!))]; return specIds.map(id => ({ id, name: specialties.find(s => s.id === id)?.name ?? String(id) })); }, [relevantServices, specialties]); const visibleServices = useMemo(() => { let pool = activeTab === 'all' ? relevantServices : relevantServices.filter(s => s.specialty_id === activeTab); return q ? pool.filter(s => s.name.includes(q)) : pool; }, [activeTab, relevantServices, q]); const toggle = (id: number) => onChange(selected.includes(id) ? selected.filter(x => x !== id) : [...selected, id]); const selectedLabels = useMemo( () => selected.map(id => ({ id, name: services.find(s => s.id === id)?.name ?? '' })).filter(x => x.name), [selected, services] ); if (selectedSpecialtyIds.length === 0) { return (
ابتدا تخصص‌هایی را انتخاب کنید تا خدمات نمایش داده شوند
); } if (relevantServices.length === 0) { return (
خدمتی برای تخصص‌های انتخاب‌شده یافت نشد
); } return (
{tabs.length > 1 && (
{[{ id: 'all' as const, name: 'همه' }, ...tabs].map(t => ( ))}
)}
setQ(e.target.value)} placeholder="جستجوی خدمت..." className="cp-input text-sm h-8" />
{selectedLabels.length > 0 && (
{selectedLabels.map(({ id, name }) => ( {name} ))}
)}
{visibleServices.map(svc => ( ))} {visibleServices.length === 0 && q && (

نتیجه‌ای یافت نشد

)}
); } // ── Address Modal ────────────────────────────────────────────────────────── const addrSchema = z.object({ name: z.string().optional(), address: z.string().min(1, 'آدرس کامل اجباری است'), telephone: z.string().min(1, 'تلفن اجباری است').max(20), province_id: z.number().nullable().optional(), city_id: z.number().nullable().optional(), latitude: z.string().optional(), longitude: z.string().optional(), }).superRefine((data, ctx) => { if (!data.province_id) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'استان اجباری است', path: ['province_id'] }); } if (!data.city_id) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'شهر اجباری است', path: ['city_id'] }); } }); type AddrForm = z.infer; function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: { open: boolean; onClose: () => void; existing: AddressData | null; doctorUuid: string; onSaved: () => void; }) { const { register, handleSubmit, watch, setValue, reset, formState: { errors } } = useForm({ resolver: zodResolver(addrSchema), }); const provinceId = watch('province_id'); const cityId = watch('city_id'); const latStr = watch('latitude'); const lngStr = watch('longitude'); const lat = latStr ? parseFloat(latStr) : null; const lng = lngStr ? parseFloat(lngStr) : null; const [mapFlyTarget, setMapFlyTarget] = useState<[number, number] | null>(null); const provincesQ = useQuery({ queryKey: ['provinces'], queryFn: () => api.get>('/api/v1/provinces'), staleTime: 600_000, enabled: open, }); const citiesQ = useQuery({ queryKey: ['cities', provinceId], queryFn: () => api.get>(`/api/v1/cities?province_id=${provinceId}`), enabled: open && !!provinceId, staleTime: 300_000, }); const provinces: ProvinceOpt[] = useMemo( () => provincesQ.data?.data?.data ?? provincesQ.data?.data ?? [], [provincesQ.data]); const cities: CityOpt[] = useMemo( () => citiesQ.data?.data?.data ?? citiesQ.data?.data ?? [], [citiesQ.data]); useEffect(() => { if (open) { reset({ name: existing?.name ?? '', address: existing?.address ?? '', telephone: existing?.telephone ?? '', province_id: existing?.province ? Number(existing.province.id) : null, city_id: existing?.city ? Number(existing.city.id) : null, latitude: existing?.map?.latitude ?? '', longitude: existing?.map?.longitude ?? '', }); } }, [open, existing, reset]); const saveMut = useMutation({ mutationFn: (values: AddrForm) => { const body: Record = { name: values.name || undefined, address: values.address || undefined, telephone: values.telephone || undefined, province_id: values.province_id ?? undefined, city_id: values.city_id ?? undefined, latitude: values.latitude ? parseFloat(values.latitude) : undefined, longitude: values.longitude ? parseFloat(values.longitude) : undefined, }; if (existing) return api.patch>(`/api/v1/clinic-pro/doctor-address/${existing.id}`, body); return api.post>('/api/v1/clinic-pro/doctor-address', { ...body, doctor_uuid: doctorUuid }); }, onSuccess: () => { toast.success(existing ? 'آدرس ویرایش شد' : 'آدرس اضافه شد'); onSaved(); onClose(); }, onError: (e: Error) => toast.error(e.message), }); return ( } >
saveMut.mutate(v))} className="space-y-4"> {/* Row 1: Name + Phone */}
{errors.telephone &&

{errors.telephone.message}

}
{/* Row 2: Full address */}