From 87966a90fa786165c6ac80d5b8166e81082d72bf Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 12 Jun 2026 22:39:11 +0330 Subject: [PATCH] feat: update ClinicDetailPage and ClinicsPage for address handling and UI improvements --- .claude/prompt/clinic-detail-fixes.md | 67 +++++ assets/admin/pages/ClinicDetailPage.tsx | 313 +++++++++----------- assets/admin/pages/ClinicsPage.tsx | 14 +- src/Admin/Controller/AdminApiController.php | 6 + src/Clinic/Controller/ClinicController.php | 9 +- 5 files changed, 235 insertions(+), 174 deletions(-) create mode 100644 .claude/prompt/clinic-detail-fixes.md diff --git a/.claude/prompt/clinic-detail-fixes.md b/.claude/prompt/clinic-detail-fixes.md new file mode 100644 index 00000000..351e0dff --- /dev/null +++ b/.claude/prompt/clinic-detail-fixes.md @@ -0,0 +1,67 @@ +# ClinicDetailPage — پنج اصلاح + +در `assets/admin/pages/ClinicDetailPage.tsx` پنج مشکل وجود دارد که باید همه را درست کنی: + +--- + +## ۱. شهر اشتباه نمایش داده می‌شود + +در خطوط حدود ۶۳۲–۶۳۳، `cityName` و `provinceName` از `clinic.city[0]` و `clinic.state[0]` +(روی Clinic entity) خوانده می‌شوند. منبع درست، `clinicAddresses[0].city` و +`clinicAddresses[0].province` است (جدول `doctor_addresses`). + +- ترجیحاً از `clinicAddresses[0]` بخواند و اگر خالی بود fallback به Clinic entity داشته باشد +- `InfoTile` آدرس هم باید از `clinicAddresses[0].address` بخواند نه `clinic.location` + +--- + +## ۲. هر کلینیک فقط یک آدرس + +**فرانت‌اند**: دکمه «افزودن آدرس» وقتی `clinicAddresses.length >= 1` است مخفی شود؛ +فقط دکمه «ویرایش» نمایش داده شود. + +**بک‌اند** (`src/Clinic/Controller/ClinicController.php`، متد `createAddress`): +قبل از ساخت آدرس جدید بررسی کند آیا این کلینیک قبلاً آدرس دارد. اگر داشت 409 برگرداند. +همچنین محدودیت حذف (`remaining <= 1`) را بردار — دیگر نیازی نیست چون کاربر می‌تواند ویرایش کند. + +--- + +## ۳. مودال آدرس — استان، شهر و نقشه + +فرم افزودن/ویرایش آدرس (مودال `addrFormOpen`) باید استان، شهر و نقشه داشته باشد: + +- `addrForm` state باید `province_id`, `city_id`, `latitude`, `longitude` هم داشته باشد +- هنگام باز کردن مودال برای ویرایش، مقادیر `addr.province`, `addr.city`, `addr.map` لود شوند +- در مودال، ابتدا استان انتخاب می‌شود، سپس شهرهای آن استان لود می‌شوند +- **وقتی شهر انتخاب می‌شود**: با `geocodeCity` (همان تابعی که در EditModal قبلاً بود)좌표 شهر گرفته شود و نقشه با `flyTo` روی آن زوم کند — دقیقاً همان رفتار تب location قبلی +- زیر dropdown شهر، `MapPicker` نمایش داده شود تا کاربر بتواند موقعیت دقیق را روی نقشه انتخاب کند +- در `saveAddrMutation` مقادیر `province_id`, `city_id`, `latitude`, `longitude` به payload اضافه شوند +- بک‌اند (`hydrateClinicAddress`) قبلاً همه این فیلدها را handle می‌کند — نیازی به تغییر ندارد + +--- + +## ۴. لوگو آپلود نمی‌شود + +در `handleLogoUpload` و `handleGalleryUpload` توکن اینطور خوانده می‌شود: + +```typescript +JSON.parse(localStorage.getItem('clinicpro-auth') ?? '{}')?.state?.token ?? '' +``` + +این روش ممکن است fail کند. باید از `useAuthStore` استفاده شود: + +```typescript +const token = useAuthStore(s => s.token); +``` + +و در fetch header از این `token` استفاده شود. + +--- + +## ۵. گالری فقط یک تصویر می‌گیرد + +`` بدون `multiple` است. + +- `multiple` به input اضافه شود +- `handleGalleryUpload` تغییر کند تا روی همه فایل‌های انتخاب‌شده (`e.target.files`) loop بزند + و همه را یکی‌یکی آپلود کند diff --git a/assets/admin/pages/ClinicDetailPage.tsx b/assets/admin/pages/ClinicDetailPage.tsx index 57a99ccf..0e73cb92 100644 --- a/assets/admin/pages/ClinicDetailPage.tsx +++ b/assets/admin/pages/ClinicDetailPage.tsx @@ -77,11 +77,6 @@ const editSchema = z.object({ telephone: z.string().optional(), info: z.string().optional(), is_247: z.boolean(), - address: z.string().optional(), - province_id: z.number().nullable(), - city_id: z.number().nullable(), - latitude: z.string().optional(), - longitude: z.string().optional(), specialties: z.array(z.number()), insurance: z.array(z.number()), doctor_services: z.array(z.number()), @@ -237,16 +232,18 @@ function MapFlyController({ target }: { target: [number, number] | null }) { useEffect(() => { if (target) map.flyTo(target, 12, { duration: 1.2 }); }, [target, map]); return null; } -function MapPicker({ lat, lng, onChange, flyTarget }: { +function MapPicker({ lat, lng, onChange, flyTarget, initialCenter }: { lat: number | null; lng: number | null; onChange: (lat: number, lng: number) => void; flyTarget?: [number, number] | null; + initialCenter?: [number, number] | null; }) { const pos: [number, number] | null = lat !== null && lng !== null ? [lat, lng] : null; - const center: [number, number] = pos ?? IRAN_CENTER; + const center: [number, number] = initialCenter ?? pos ?? IRAN_CENTER; + const zoom = initialCenter ?? pos ? 13 : 5; return (
- + @@ -271,12 +268,9 @@ async function geocodeCity(name: string): Promise<[number, number] | null> { // ── Edit Modal ───────────────────────────────────────────────────────────── -function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: { +function EditModal({ clinic, onClose, onSaved }: { clinic: ClinicDetail; onClose: () => void; onSaved: () => void; - initialTab?: 'basic' | 'location' | 'tags'; }) { - const [mapFlyTarget, setMapFlyTarget] = useState<[number, number] | null>(null); - const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm({ resolver: zodResolver(editSchema), defaultValues: { @@ -284,36 +278,16 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: { telephone: clinic.phone ?? clinic.phone_number ?? '', info: clinic.caption ?? '', is_247: clinic['24_7'] ?? false, - address: clinic.location ?? '', - province_id: clinic.state?.[0] ? Number(clinic.state[0].id) : null, - city_id: clinic.city?.[0] ? Number(clinic.city[0].id) : null, - latitude: clinic.map?.latitude ?? '', - longitude: clinic.map?.longitude ?? '', specialties: (clinic.specialties ?? []).map(s => Number(s.id)), insurance: (clinic.list_bime ?? []).map(s => Number(s.id)), doctor_services: (clinic.services ?? []).map(s => Number(s.id)), }, }); - const provinceId = watch('province_id'); - const cityId = watch('city_id'); - const lat = watch('latitude'); - const lng = watch('longitude'); - const latN = lat ? parseFloat(lat) : null; - const lngN = lng ? parseFloat(lng) : null; const watchedSpec = (watch('specialties') ?? []) as number[]; const watchedIns = (watch('insurance') ?? []) as number[]; const watchedSrv = (watch('doctor_services') ?? []) as number[]; - const provincesQ = useQuery({ - queryKey: ['provinces'], staleTime: 600_000, - queryFn: () => api.get>('/api/v1/provinces'), - }); - const citiesQ = useQuery({ - queryKey: ['cities', provinceId], staleTime: 300_000, - queryFn: () => api.get>(`/api/v1/cities?province_id=${provinceId}`), - enabled: !!provinceId, - }); const specialtiesQ = useQuery({ queryKey: ['specialties-all'], staleTime: 600_000, queryFn: () => api.get>('/api/v1/specialties'), @@ -327,8 +301,6 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: { queryFn: () => api.get>('/api/v1/doctor-services'), }); - const provinces: Opt[] = useMemo(() => (provincesQ.data?.data?.data ?? provincesQ.data?.data ?? []).map((p: any) => ({ id: Number(p.id ?? p.nid), name: p.name })), [provincesQ.data]); - const cities: Opt[] = useMemo(() => (citiesQ.data?.data?.data ?? citiesQ.data?.data ?? []).map((c: any) => ({ id: Number(c.id ?? c.nid), name: c.name })), [citiesQ.data]); const specialties: Opt[] = useMemo(() => { const raw = specialtiesQ.data?.data?.data ?? specialtiesQ.data?.data ?? []; return raw.map((s: any) => ({ id: Number(s.id), name: s.name })); @@ -348,11 +320,6 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: { telephone: values.telephone || undefined, info: values.info || undefined, '24_7': values.is_247, - address: values.address || undefined, - state: values.province_id ? [values.province_id] : undefined, - city: values.city_id ? [values.city_id] : undefined, - latitude: values.latitude ? parseFloat(values.latitude) : undefined, - longitude: values.longitude ? parseFloat(values.longitude) : undefined, specialties: values.specialties, insurance: values.insurance, doctor_services: values.doctor_services, @@ -361,7 +328,7 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: { onError: (e: Error) => toast.error(e.message), }); - const [activeTab, setActiveTab] = useState<'basic' | 'location' | 'tags'>(initialTab); + const [activeTab, setActiveTab] = useState<'basic' | 'tags'>('basic'); return (
@@ -375,7 +342,7 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: { {/* Tab switcher */}
- {([['basic', 'اطلاعات پایه'], ['location', 'موقعیت'], ['tags', 'تخصص و بیمه']] as const).map(([id, label]) => ( + {([['basic', 'اطلاعات پایه'], ['tags', 'تخصص و بیمه']] as const).map(([id, label]) => (