From 57965f184de142df2abb310500167bc5b5413896 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 21 Jun 2026 13:26:47 +0330 Subject: [PATCH] feat: add social media fields to Clinic entity and update API documentation --- .claude/prompt/clinic-social-media-field.md | 101 ++++++++++++++++++++ assets/admin/pages/ClinicDetailPage.tsx | 42 +++++++- assets/admin/types/index.ts | 7 ++ docs/api/clinic.md | 8 ++ migrations/Version20260621094624.php | 31 ++++++ src/Clinic/Controller/ClinicController.php | 12 +++ src/Clinic/Entity/Clinic.php | 6 ++ 7 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 .claude/prompt/clinic-social-media-field.md create mode 100644 migrations/Version20260621094624.php diff --git a/.claude/prompt/clinic-social-media-field.md b/.claude/prompt/clinic-social-media-field.md new file mode 100644 index 00000000..cc4efbd7 --- /dev/null +++ b/.claude/prompt/clinic-social-media-field.md @@ -0,0 +1,101 @@ +# افزودن فیلد شبکه‌های اجتماعی به Clinic + +## پروژه + +`clinicpro` (Backend) — پیش‌نیاز پرامپت همتا در frontend: `nobat724_front/.claude/prompt/clinic-seo-schema-knowledge-panel.md`. آن پرامپت برای Knowledge Panel گوگل به `sameAs` در JSON-LD نیاز دارد که از همین فیلد جدید پر می‌شود. + +این تغییر دقیقاً همان الگویی است که قبلاً برای `Doctor` entity پیاده‌سازی شده (`clinicpro/.claude/prompt/doctor-social-media-field.md` — قبلاً اجرا و merge شده). همان ساختار را برای `Clinic` تکرار کن. + +## زمینه + +بررسی `src/Clinic/Entity/Clinic.php` و `docs/api/clinic.md` نشان داد کلینیک هیچ فیلد شبکه‌اجتماعی ندارد. کلینیک از قبل `latitude`/`longitude`/`address`/`telephone`/`working_days`/`is247` مستقیم روی خودش دارد (برخلاف Doctor که این‌ها در یک entity جدا — `DoctorAddress` — هستند)، پس برای کلینیک نیازی به fetch جدا نیست؛ فقط `socialMedia` کم است. + +## مشکل / هدف + +به `Clinic` entity یک فیلد JSON برای لینک شبکه‌های اجتماعی (اینستاگرام، تلگرام، آپارات، یوتیوب، لینکدین) اضافه شود، در پاسخ API برگردانده شود، و قابل ویرایش باشد. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/Clinic/Entity/Clinic.php` | فیلد `socialMedia` (JSON nullable) — دقیقاً کنار `imagesClinic` (خط ۷۱) | +| `src/Clinic/Controller/ClinicController.php` | متد `update()` (خط ۲۰۵) باید فیلد را بپذیرد و validate کند؛ `toDetailArray()`/`toListArray()` در entity باید آن را برگردانند | +| `migrations/` | migration جدید برای ستون `social_media` روی جدول `clinics` | +| `docs/api/clinic.md` | مستندسازی فیلد جدید | + +## وضعیت فعلی + +`src/Clinic/Entity/Clinic.php` الگوی دقیق برای فیلد JSON nullable از قبل دارد: + +```php +// خط ۷۱ — الگوی موجود +private ?array $imagesClinic = null; + +// خط ۱۴۴ +public function getImagesClinic(): ?array { return $this->imagesClinic; } + +// خط ۱۷۶ +public function setImagesClinic(?array $v): self { $this->imagesClinic = $v; $this->touch(); return $this; } +``` + +## وظایف + +### ۱. افزودن فیلد `socialMedia` + +```php +// src/Clinic/Entity/Clinic.php — کنار imagesClinic +#[ORM\Column(name: 'social_media', type: 'json', nullable: true)] +private ?array $socialMedia = null; + +public function getSocialMedia(): ?array { return $this->socialMedia; } +public function setSocialMedia(?array $v): self { $this->socialMedia = $v; $this->touch(); return $this; } +``` + +ساختار JSON (دقیقاً مشابه Doctor): + +```json +{ + "instagram": "https://instagram.com/clinic.example", + "telegram": "https://t.me/clinic_example", + "aparat": null, + "youtube": null, + "linkedin": null +} +``` + +### ۲. اضافه‌کردن به خروجی `toDetailArray()` و `toListArray()` + +در `Clinic.php`، هرجا `images_clinic` در آرایه‌ی خروجی است، `social_media` هم کنارش اضافه شود — هم در `toDetailArray()` (خط ۱۸۲) هم در `toListArray()` (خط ۲۲۴، اگر لیست هم باید نشانش بدهد؛ در غیر این صورت فقط در `toDetailArray()` کافی است چون JSON-LD فقط در صفحه‌ی تکی کلینیک لازم است). + +### ۳. Migration + +```bash +ddev exec php bin/console doctrine:migrations:diff --no-interaction +ddev exec php bin/console doctrine:migrations:migrate --no-interaction +``` + +### ۴. پذیرش در `ClinicController::update()` + +در متد `update()` (خط ۲۰۵)، همان الگوی validation که در `DoctorController::hydrateDoctor()` برای `social_media` استفاده شد را تکرار کن (`filter_var($value, FILTER_VALIDATE_URL)` برای هر کلید، در غیر این صورت `null`): + +```php +if (array_key_exists('social_media', $data) && is_array($data['social_media'])) { + $allowedKeys = ['instagram', 'telegram', 'aparat', 'youtube', 'linkedin']; + $socialMedia = []; + foreach ($allowedKeys as $key) { + $value = $data['social_media'][$key] ?? null; + $socialMedia[$key] = (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) + ? $value + : null; + } + $clinic->setSocialMedia($socialMedia); +} +``` + +بررسی کن این منطق دقیقاً کجای متد `update()` باید قرار گیرد (کنار جایی که `imagesClinic`/فیلدهای دیگر از `$data` خوانده می‌شوند). + +## نکات مهم + +- این فیلد nullable کامل است — کلینیک‌هایی بدون شبکه‌اجتماعی نباید خطا بگیرند. +- اگر در پنل ادمین (`assets/admin/`) فرم ویرایش کلینیک وجود دارد، در صورت وقت یک بخش مشابه فرم شبکه‌اجتماعی Doctor (در `DoctorDetailPage.tsx`) برایش اضافه کن؛ این بخش اختیاری است و در صورت نبود وقت می‌توان frontend عمومی (`nobat724_front`) را بدون این بخش هم اجرا کرد (فیلد در دیتابیس و API آماده می‌ماند، فقط فعلاً از پنل قابل تنظیم نیست). +- بعد از تغییر، `docs/api/clinic.md` را در همان session به‌روزرسانی کن (نمونه‌ی JSON پاسخ + جدول فیلدهای قابل ویرایش در POST/PATCH). diff --git a/assets/admin/pages/ClinicDetailPage.tsx b/assets/admin/pages/ClinicDetailPage.tsx index e82fe014..34976160 100644 --- a/assets/admin/pages/ClinicDetailPage.tsx +++ b/assets/admin/pages/ClinicDetailPage.tsx @@ -73,6 +73,8 @@ interface OptUuid { id: number; uuid: string; name: string; } // ── Edit form schema ─────────────────────────────────────────────────────── +const urlOrEmpty = z.string().refine(v => v === '' || /^https?:\/\/.+/.test(v), { message: 'آدرس URL معتبر نیست' }); + const editSchema = z.object({ name: z.string().min(1, 'نام الزامی است'), telephone: z.string().optional(), @@ -81,6 +83,11 @@ const editSchema = z.object({ specialties: z.array(z.number()), insurance: z.array(z.number()), doctor_services: z.array(z.number()), + sm_instagram: urlOrEmpty, + sm_telegram: urlOrEmpty, + sm_aparat: urlOrEmpty, + sm_youtube: urlOrEmpty, + sm_linkedin: urlOrEmpty, }); type EditForm = z.infer; @@ -282,6 +289,11 @@ function EditModal({ clinic, onClose, onSaved }: { 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)), + sm_instagram: clinic.social_media?.instagram ?? '', + sm_telegram: clinic.social_media?.telegram ?? '', + sm_aparat: clinic.social_media?.aparat ?? '', + sm_youtube: clinic.social_media?.youtube ?? '', + sm_linkedin: clinic.social_media?.linkedin ?? '', }, }); @@ -324,12 +336,19 @@ function EditModal({ clinic, onClose, onSaved }: { specialties: values.specialties, insurance: values.insurance, doctor_services: values.doctor_services, + social_media: { + instagram: values.sm_instagram || null, + telegram: values.sm_telegram || null, + aparat: values.sm_aparat || null, + youtube: values.sm_youtube || null, + linkedin: values.sm_linkedin || null, + }, }), onSuccess: () => { toast.success('اطلاعات کلینیک ذخیره شد'); onSaved(); onClose(); }, onError: (e: Error) => toast.error(e.message), }); - const [activeTab, setActiveTab] = useState<'basic' | 'tags'>('basic'); + const [activeTab, setActiveTab] = useState<'basic' | 'tags' | 'social'>('basic'); return (
@@ -343,7 +362,7 @@ function EditModal({ clinic, onClose, onSaved }: { {/* Tab switcher */}
- {([['basic', 'اطلاعات پایه'], ['tags', 'تخصص و بیمه']] as const).map(([id, label]) => ( + {([['basic', 'اطلاعات پایه'], ['tags', 'تخصص و بیمه'], ['social', 'شبکه‌های اجتماعی']] as const).map(([id, label]) => (