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]) => (
setActiveTab(id)}
style={{
padding: '10px 14px', fontSize: 13, fontWeight: 600, border: 'none', background: 'none',
@@ -413,59 +380,6 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
>
)}
- {/* ── Location tab ── */}
- {activeTab === 'location' && (
- <>
-
-
- استان
- { setValue('province_id', val); setValue('city_id', null); }}
- />
-
-
- شهر
- {
- setValue('city_id', val);
- if (label) geocodeCity(label).then(c => { if (c) setMapFlyTarget(c); });
- }}
- />
-
-
-
- آدرس
-
-
-
-
- موقعیت روی نقشه
- {latN && lngN && (
- {latN.toFixed(4)}, {lngN.toFixed(4)}
- )}
-
-
برای تعیین موقعیت روی نقشه کلیک کنید
-
{ setValue('latitude', String(lt)); setValue('longitude', String(ln)); }}
- />
- {latN && lngN && (
- { setValue('latitude', ''); setValue('longitude', ''); }}>
- حذف موقعیت
-
- )}
-
- >
- )}
-
{/* ── Tags tab ── */}
{activeTab === 'tags' && (
<>
@@ -525,10 +439,10 @@ export default function ClinicDetailPage() {
const qc = useQueryClient();
const primaryRole = useAuthStore(s => s.primaryRole);
const dbUuid = useAuthStore(s => s.dbUuid);
+ const authToken = useAuthStore(s => s.token);
const isOwner = primaryRole === 'clinic' && dbUuid === uuid;
const [editOpen, setEditOpen] = useState(false);
- const [editInitialTab, setEditInitialTab] = useState<'basic' | 'location' | 'tags'>('basic');
const [deleteOpen, setDeleteOpen] = useState(false);
const [inviteOpen, setInviteOpen] = useState(false);
const [doctorsTab, setDoctorsTab] = useState<'doctors' | 'invitations'>('doctors');
@@ -562,11 +476,39 @@ export default function ClinicDetailPage() {
});
const clinicAddresses: ClinicAddress[] = (clinicAddressesQ.data?.data as any)?.data ?? clinicAddressesQ.data?.data ?? [];
- const [addrFormOpen, setAddrFormOpen] = useState(false);
+ const [addrFormOpen, setAddrFormOpen] = useState(false);
const [editingClinicAddr, setEditingClinicAddr] = useState(null);
const [deleteAddrConfirm, setDeleteAddrConfirm] = useState(null);
- const [addrForm, setAddrForm] = useState({ name: '', address: '', telephone: '' });
+ const emptyAddrForm = { name: '', address: '', telephone: '', province_id: null as number | null, city_id: null as number | null, latitude: null as number | null, longitude: null as number | null };
+ const [addrForm, setAddrForm] = useState(emptyAddrForm);
+ const [addrMapFlyTarget, setAddrMapFlyTarget] = useState<[number, number] | null>(null);
+
+ const provincesQ = useQuery({
+ queryKey: ['provinces'], staleTime: 600_000,
+ queryFn: () => api.get>('/api/v1/provinces'),
+ });
+ const citiesQ = useQuery({
+ queryKey: ['cities', addrForm.province_id], staleTime: 300_000,
+ queryFn: () => api.get>(`/api/v1/cities?province_id=${addrForm.province_id}`),
+ enabled: !!addrForm.province_id,
+ });
+ 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 addrCities: Opt[] = useMemo(() => (citiesQ.data?.data?.data ?? citiesQ.data?.data ?? []).map((c: any) => ({ id: Number(c.id ?? c.nid), name: c.name })), [citiesQ.data]);
+
+ const openAddrForm = (addr: ClinicAddress | null) => {
+ setEditingClinicAddr(addr);
+ const lat = addr?.map?.latitude ? parseFloat(addr.map.latitude) : null;
+ const lng = addr?.map?.longitude ? parseFloat(addr.map.longitude) : null;
+ setAddrForm(addr ? {
+ name: addr.name ?? '', address: addr.address ?? '', telephone: addr.telephone ?? '',
+ province_id: addr.province ? Number(addr.province.id) : null,
+ city_id: addr.city ? Number(addr.city.id) : null,
+ latitude: lat, longitude: lng,
+ } : emptyAddrForm);
+ setAddrMapFlyTarget(lat && lng ? [lat, lng] : null);
+ setAddrFormOpen(true);
+ };
const saveAddrMutation = useMutation({
mutationFn: (payload: typeof addrForm) => {
@@ -580,7 +522,7 @@ export default function ClinicDetailPage() {
qc.invalidateQueries({ queryKey: ['clinic-addresses', uuid] });
setAddrFormOpen(false);
setEditingClinicAddr(null);
- setAddrForm({ name: '', address: '', telephone: '' });
+ setAddrForm(emptyAddrForm);
},
onError: () => toast.error('خطا در ذخیره آدرس'),
});
@@ -607,10 +549,7 @@ export default function ClinicDetailPage() {
const invitationList: ClinicInvitation[] = invitationsQ.data?.data ?? [];
- const openEdit = (tab: 'basic' | 'location' | 'tags' = 'basic') => {
- setEditInitialTab(tab);
- setEditOpen(true);
- };
+ const openEdit = () => setEditOpen(true);
const toggleMut = useMutation({
mutationFn: () => api.patch>(`/api/v1/admin/clinic/${uuid}/status`, {}),
@@ -655,7 +594,7 @@ export default function ClinicDetailPage() {
headers: {
'Content-Disposition': `filename="${file.name}"`,
'Content-Type': file.type || 'application/octet-stream',
- Authorization: `Bearer ${JSON.parse(localStorage.getItem('clinicpro-auth') ?? '{}')?.state?.token ?? ''}`,
+ Authorization: `Bearer ${authToken ?? ''}`,
},
body: file,
});
@@ -670,24 +609,27 @@ export default function ClinicDetailPage() {
finally { setLogoUploading(false); }
};
- const handleGalleryUpload = async (file: File) => {
+ const handleGalleryUpload = async (files: FileList) => {
setGalleryUploading(true);
try {
- const res = await fetch('/file/upload/clinic_pro/clinic/field_image_clinic', {
- method: 'POST',
- headers: {
- 'Content-Disposition': `filename="${file.name}"`,
- 'Content-Type': file.type || 'application/octet-stream',
- Authorization: `Bearer ${JSON.parse(localStorage.getItem('clinicpro-auth') ?? '{}')?.state?.token ?? ''}`,
- },
- body: file,
- });
- const json = await res.json();
- const url = json?.data?.url;
- if (url && clinic) {
+ const uploaded: { url: string }[] = [];
+ for (const file of Array.from(files)) {
+ const res = await fetch('/file/upload/clinic_pro/clinic/field_image_clinic', {
+ method: 'POST',
+ headers: {
+ 'Content-Disposition': `filename="${file.name}"`,
+ 'Content-Type': file.type || 'application/octet-stream',
+ Authorization: `Bearer ${authToken ?? ''}`,
+ },
+ body: file,
+ });
+ const json = await res.json();
+ if (json?.data?.url) uploaded.push({ url: json.data.url });
+ }
+ if (uploaded.length > 0 && clinic) {
const existing = (clinic.images_clinic ?? []).filter(img => img?.url);
- await api.patch(`/api/v1/clinic/${uuid}`, { image_clinic: [...existing, { url }] });
- toast.success('تصویر اضافه شد');
+ await api.patch(`/api/v1/clinic/${uuid}`, { image_clinic: [...existing, ...uploaded] });
+ toast.success(`${uploaded.length} تصویر اضافه شد`);
qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] });
}
} catch (e: any) { toast.error(e.message); }
@@ -696,10 +638,14 @@ export default function ClinicDetailPage() {
const hue = HUES_LIST[(uuid?.charCodeAt(0) ?? 0) % HUES_LIST.length];
const logo = clinic?.logo ?? clinic?.clinic_logo;
- const lat = clinic?.map?.latitude ? parseFloat(clinic.map.latitude) : null;
- const lng = clinic?.map?.longitude ? parseFloat(clinic.map.longitude) : null;
- const cityName = clinic?.city?.[0]?.name;
- const provinceName = clinic?.state?.[0]?.name;
+ // شهر و استان از آدرس DoctorAddress (منبع واقعی) نه از Clinic entity
+ const primaryAddr = clinicAddresses[0] ?? null;
+ const cityName = primaryAddr?.city?.name ?? clinic?.city?.[0]?.name;
+ const provinceName = primaryAddr?.province?.name ?? clinic?.state?.[0]?.name;
+ const lat = primaryAddr?.map?.latitude ? parseFloat(primaryAddr.map.latitude)
+ : clinic?.map?.latitude ? parseFloat(clinic.map.latitude) : null;
+ const lng = primaryAddr?.map?.longitude ? parseFloat(primaryAddr.map.longitude)
+ : clinic?.map?.longitude ? parseFloat(clinic.map.longitude) : null;
if (isLoading) {
return (
@@ -741,7 +687,7 @@ export default function ClinicDetailPage() {
-
openEdit('basic')}>
+ openEdit()}>
ویرایش
{primaryRole === 'admin' && (
@@ -806,7 +752,7 @@ export default function ClinicDetailPage() {
label="شهر"
value={[cityName, provinceName].filter(Boolean).join('، ') || '—'} />
}
- label="آدرس" value={clinic.location ?? '—'} fullWidth />
+ label="آدرس" value={primaryAddr?.address ?? clinic.location ?? '—'} fullWidth />
{clinic.caption && (
@@ -944,8 +890,8 @@ export default function ClinicDetailPage() {
{galleryUploading ? 'در حال آپلود...' : 'افزودن تصویر'}
- e.target.files?.[0] && handleGalleryUpload(e.target.files[0])} />
+ e.target.files?.length && handleGalleryUpload(e.target.files)} />
{(clinic.images_clinic ?? []).length === 0 ? (
@@ -984,7 +930,7 @@ export default function ClinicDetailPage() {
تخصصها، بیمه و خدمات
-
openEdit('tags')}>
+ openEdit()}>
ویرایش
@@ -1045,12 +991,8 @@ export default function ClinicDetailPage() {
آدرسهای کلینیک ({formatNumber(clinicAddresses.length)})
- {isOwner && (
-
{
- setEditingClinicAddr(null);
- setAddrForm({ name: '', address: '', telephone: '' });
- setAddrFormOpen(true);
- }}>
+ {(isOwner || primaryRole === 'admin') && clinicAddresses.length === 0 && (
+ openAddrForm(null)}>
افزودن آدرس
@@ -1084,13 +1026,9 @@ export default function ClinicDetailPage() {
)}
- {isOwner && (
+ {(isOwner || primaryRole === 'admin') && (
-
{
- setEditingClinicAddr(addr);
- setAddrForm({ name: addr.name ?? '', address: addr.address ?? '', telephone: addr.telephone ?? '' });
- setAddrFormOpen(true);
- }}>
+ openAddrForm(addr)}>
{/* Clinic Address Form Modal */}
- {addrFormOpen && (
- setAddrFormOpen(false)}>
-
e.stopPropagation()}>
-
-
{editingClinicAddr ? 'ویرایش آدرس' : 'افزودن آدرس جدید'}
-
setAddrFormOpen(false)}>
-
+ {addrFormOpen && createPortal(
+ setAddrFormOpen(false)}>
+
e.stopPropagation()}>
+
+ {editingClinicAddr ? 'ویرایش آدرس' : 'افزودن آدرس جدید'}
+ setAddrFormOpen(false)}>
+
-
نام شعبه / عنوان
-
- setAddrForm(f => ({ ...f, name: e.target.value }))} />
+ نام شعبه / عنوان
+ setAddrForm(f => ({ ...f, name: e.target.value }))} />
+
+
+
+ استان
+ setAddrForm(f => ({ ...f, province_id: val, city_id: null }))}
+ />
+
+
+ شهر
+ {
+ setAddrForm(f => ({ ...f, city_id: val }));
+ if (label) geocodeCity(label).then(c => { if (c) setAddrMapFlyTarget(c); });
+ }}
+ />
-
آدرس
-
- setAddrForm(f => ({ ...f, address: e.target.value }))} />
-
+
آدرس کامل
+
-
تلفن
-
-
setAddrForm(f => ({ ...f, telephone: e.target.value }))} />
+
+ موقعیت روی نقشه
+ {addrForm.latitude && addrForm.longitude && (
+
+ {addrForm.latitude.toFixed(4)}, {addrForm.longitude.toFixed(4)}
+
+ )}
+
برای تعیین موقعیت دقیق روی نقشه کلیک کنید
+
setAddrForm(f => ({ ...f, latitude: lt, longitude: ln }))}
+ />
+ {addrForm.latitude && addrForm.longitude && (
+ setAddrForm(f => ({ ...f, latitude: null, longitude: null }))}>
+ حذف موقعیت
+
+ )}
+
+
+ تلفن
+ setAddrForm(f => ({ ...f, telephone: e.target.value }))} />
-
- setAddrFormOpen(false)}>انصراف
-
+ setAddrFormOpen(false)}>انصراف
+ saveAddrMutation.mutate(addrForm)}>
{saveAddrMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
-
+
,
+ document.body,
)}
{/* Delete Address Confirm */}
@@ -1170,7 +1150,6 @@ export default function ClinicDetailPage() {
{editOpen && createPortal(
setEditOpen(false)}
onSaved={() => { qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] }); qc.invalidateQueries({ queryKey: ['admin-clinics'] }); }}
/>,
diff --git a/assets/admin/pages/ClinicsPage.tsx b/assets/admin/pages/ClinicsPage.tsx
index 6270c637..fdca3c61 100644
--- a/assets/admin/pages/ClinicsPage.tsx
+++ b/assets/admin/pages/ClinicsPage.tsx
@@ -22,6 +22,7 @@ import ConfirmDialog from '../components/ui/ConfirmDialog';
const HUES_LIST = [256, 205, 162, 295, 272];
const addSchema = z.object({
+ owner_mobile: z.string().min(10, 'شماره موبایل معتبر نیست'),
name: z.string().min(2, 'نام الزامی است'),
telephone: z.string().optional(),
});
@@ -69,7 +70,7 @@ export default function ClinicsPage() {
const addForm = useForm({ resolver: zodResolver(addSchema) });
const addMutation = useMutation({
- mutationFn: (d: AddForm) => api.post>('/api/v1/clinic', d),
+ mutationFn: (d: AddForm) => api.post>('/api/v1/admin/clinic', d),
onSuccess: (res) => {
toast.success('کلینیک اضافه شد');
setAddOpen(false);
@@ -237,7 +238,16 @@ export default function ClinicsPage() {
- نام کلینیک
+ شماره موبایل صاحب کلینیک *
+
+
+ {addForm.formState.errors.owner_mobile && (
+
{addForm.formState.errors.owner_mobile.message}
+ )}
+
+
+
+ نام کلینیک *
{addForm.formState.errors.name && (
diff --git a/src/Admin/Controller/AdminApiController.php b/src/Admin/Controller/AdminApiController.php
index cd18b3f4..1da7ff0c 100644
--- a/src/Admin/Controller/AdminApiController.php
+++ b/src/Admin/Controller/AdminApiController.php
@@ -504,6 +504,12 @@ class AdminApiController extends BaseController
$this->em->persist($user);
}
+ $roles = $user->getRoles();
+ if (!in_array('ROLE_CLINIC', $roles, true)) {
+ $roles[] = 'ROLE_CLINIC';
+ $user->setRoles(array_values(array_unique($roles)));
+ }
+
$clinic = new Clinic($user);
$clinic->setName($name);
if (!empty($data['telephone'])) $clinic->setTelephone($data['telephone']);
diff --git a/src/Clinic/Controller/ClinicController.php b/src/Clinic/Controller/ClinicController.php
index 6dd85bee..9833b16a 100644
--- a/src/Clinic/Controller/ClinicController.php
+++ b/src/Clinic/Controller/ClinicController.php
@@ -567,6 +567,10 @@ class ClinicController extends BaseController
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
}
+ if ($this->addressRepo->countByClinic($clinic->getId()) > 0) {
+ return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این کلینیک قبلاً آدرس دارد. برای ویرایش از endpoint PATCH استفاده کنید', 409);
+ }
+
$data = json_decode($request->getContent(), true) ?? [];
$address = DoctorAddress::forClinic($clinic->getId());
$this->hydrateClinicAddress($address, $data);
@@ -618,11 +622,6 @@ class ClinicController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'آدرس یافت نشد', 404);
}
- $remaining = $this->addressRepo->countByClinic($clinic->getId());
- if ($remaining <= 1) {
- return $this->error(ErrorCodes::ERR_CONFLICT_001, 'کلینیک باید حداقل یک آدرس داشته باشد', 409);
- }
-
$this->addressRepo->remove($address);
return $this->success(['message' => 'آدرس با موفقیت حذف شد']);