feat: add social media links field to Doctor entity and update API documentation
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
# افزودن فیلد شبکههای اجتماعی به Doctor
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`clinicpro` (Backend) — این تغییر پیشنیاز پرامپت همتا در frontend است: `nobat724_front/.claude/prompt/doctor-seo-schema-knowledge-panel.md`. آن پرامپت برای Knowledge Panel گوگل به `sameAs` در JSON-LD نیاز دارد که باید از همین فیلد جدید پر شود.
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
برای فعالسازی Knowledge Panel گوگل برای صفحهی هر پزشک (هدف نهایی در `nobat724_front`)، schema.org نیاز به آرایهی `sameAs` دارد که لینک پروفایلهای شبکههای اجتماعی پزشک (اینستاگرام، تلگرام، آپارات، یوتیوب، لینکدین) را به گوگل معرفی میکند. بررسی `src/Doctor/Entity/Doctor.php` و `docs/api/doctor.md` نشان داد این داده هیچجا در API موجود نیست — نه در پاسخ `GET /api/v1/doctor/{uuid}` و نه در فرم ادمین (`assets/admin/pages/DoctorFormPage.tsx`).
|
||||||
|
|
||||||
|
نکتهی مهم: برخلاف انتظار اولیه، `DoctorAddress` (`src/Doctor/Entity/DoctorAddress.php`) از قبل `latitude`/`longitude` دارد و در `toArray()` بهصورت `map: { latitude, longitude }` برمیگرداند — یعنی GeoCoordinates برای LocalBusiness schema نیازی به تغییر backend ندارد. فقط شبکههای اجتماعی پزشک باقی میماند.
|
||||||
|
|
||||||
|
## مشکل / هدف
|
||||||
|
|
||||||
|
به `Doctor` entity یک فیلد JSON برای ذخیرهی لینک شبکههای اجتماعی (اینستاگرام، تلگرام، آپارات، یوتیوب، لینکدین، توییتر/X) اضافه شود، در پاسخ API برگردانده شود، و در فرم ادمین برای ویرایش در دسترس باشد.
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `src/Doctor/Entity/Doctor.php` | باید فیلد `socialMedia` (JSON nullable) اضافه شود |
|
||||||
|
| `src/Doctor/Controller/DoctorController.php` | پاسخ `GET /api/v1/doctor/{uuid}` باید `social_media` را برگرداند؛ متد update باید آن را بپذیرد |
|
||||||
|
| `migrations/` | migration جدید برای ستون `social_media` روی جدول `doctors` |
|
||||||
|
| `assets/admin/pages/DoctorFormPage.tsx` | فیلدهای ورودی لینکهای اجتماعی به فرم اضافه شود |
|
||||||
|
| `assets/admin/types/index.ts` | type پزشک باید `socialMedia` را شامل شود |
|
||||||
|
| `docs/api/doctor.md` | باید فیلد جدید مستند شود |
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
|
||||||
|
`src/Doctor/Entity/Doctor.php` فیلدهای پایه دارد (نام، تخصص، آدرسها) اما هیچ فیلد JSON برای دادهی نیمهساختیافته ندارد. الگوی مشابه را میتوان از `DoctorAddress::toArray()` که `map` را بهصورت nested object برمیگرداند الگو گرفت:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// src/Doctor/Entity/DoctorAddress.php — الگوی موجود برای nested object در toArray()
|
||||||
|
'map' => [
|
||||||
|
'latitude' => $this->latitude !== null ? (string) $this->latitude : null,
|
||||||
|
'longitude' => $this->longitude !== null ? (string) $this->longitude : null,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. افزودن فیلد `socialMedia` به `Doctor` entity
|
||||||
|
|
||||||
|
```php
|
||||||
|
// src/Doctor/Entity/Doctor.php
|
||||||
|
#[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 ذخیرهشده:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"instagram": "https://instagram.com/dr.example",
|
||||||
|
"telegram": "https://t.me/dr_example",
|
||||||
|
"aparat": "https://aparat.com/dr.example",
|
||||||
|
"youtube": "https://youtube.com/@dr.example",
|
||||||
|
"linkedin": "https://linkedin.com/in/dr-example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
هر کلید nullable است — پزشک ممکن است فقط بعضی شبکهها را داشته باشد. کلیدهای خالی/null باید در پاسخ API هم `null` بمانند (نه حذف شوند) تا frontend بهسادگی چک کند.
|
||||||
|
|
||||||
|
### ۲. Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ddev exec php bin/console doctrine:migrations:diff --no-interaction
|
||||||
|
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۳. بازگرداندن فیلد در پاسخ API
|
||||||
|
|
||||||
|
در `src/Doctor/Controller/DoctorController.php`، هرجا `toArray()`-مانند برای پزشک ساخته میشود (GET تکی، GET لیست)، کلید `social_media` اضافه شود:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'social_media' => $doctor->getSocialMedia(),
|
||||||
|
```
|
||||||
|
|
||||||
|
و در متد update، فیلد جدید از request body خوانده و validate شود (فقط باید URL معتبر یا null باشد برای هر کلید — از `InputValidator` یا یک Assert ساده استفاده کن، الگوی موجود validation در همان کنترلر را دنبال کن).
|
||||||
|
|
||||||
|
### ۴. فرم ادمین
|
||||||
|
|
||||||
|
در `assets/admin/pages/DoctorFormPage.tsx`، یک بخش جدید «شبکههای اجتماعی» با ۵ فیلد متنی (اینستاگرام، تلگرام، آپارات، یوتیوب، لینکدین) اضافه شود. الگوی `Field()` موجود در همین فایل (خط ۵۲) را برای ساخت input استفاده کن. مقدار اولیه از `doctor.socialMedia` پر شود، در submit به همان شکل JSON ارسال شود.
|
||||||
|
|
||||||
|
در `assets/admin/types/index.ts`، interface پزشک باید شامل شود:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
socialMedia?: {
|
||||||
|
instagram?: string | null;
|
||||||
|
telegram?: string | null;
|
||||||
|
aparat?: string | null;
|
||||||
|
youtube?: string | null;
|
||||||
|
linkedin?: string | null;
|
||||||
|
} | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- این فیلد باید **nullable** کامل باشد — پزشکانی که شبکه اجتماعی ندارند نباید خطا بگیرند.
|
||||||
|
- مقادیر باید URL کامل (با `https://`) ذخیره شوند، نه فقط username — frontend مستقیماً این مقدار را در `sameAs` آرایهی JSON-LD قرار میدهد بدون پردازش اضافی.
|
||||||
|
- بعد از این تغییر، `docs/api/doctor.md` را طبق قانون پروژه (بهروزرسانی مستندات همزمان با تغییر API) ویرایش کن — هم در نمونهی JSON پاسخ `GET /api/v1/doctor/{uuid}` و هم در بخش فیلدهای قابل ویرایش.
|
||||||
|
- migration را قبل از merge باید روی دیتابیس dev واقعی تست کنی (`ddev exec php bin/console doctrine:migrations:migrate --no-interaction`).
|
||||||
@@ -47,6 +47,10 @@ interface DoctorDetail {
|
|||||||
img: { url: string; fid: number }[];
|
img: { url: string; fid: number }[];
|
||||||
expertise: { id: string; uuid: string; name: string }[];
|
expertise: { id: string; uuid: string; name: string }[];
|
||||||
satisfaction: string; point: string;
|
satisfaction: string; point: string;
|
||||||
|
social_media: {
|
||||||
|
instagram: string | null; telegram: string | null; aparat: string | null;
|
||||||
|
youtube: string | null; linkedin: string | null;
|
||||||
|
} | null;
|
||||||
address: AddressData[];
|
address: AddressData[];
|
||||||
state: { id: string; name: string }[];
|
state: { id: string; name: string }[];
|
||||||
city: { id: string; name: string }[];
|
city: { id: string; name: string }[];
|
||||||
@@ -2116,6 +2120,8 @@ function EditSpecialtyPicker({ selected, onChange, specialties }: {
|
|||||||
|
|
||||||
// ── Edit schema ────────────────────────────────────────────────────────────
|
// ── Edit schema ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const urlOrEmpty = z.string().url('آدرس نامعتبر است').optional().or(z.literal(''));
|
||||||
|
|
||||||
const editSchema = z.object({
|
const editSchema = z.object({
|
||||||
name: z.string().min(2, 'نام حداقل ۲ کاراکتر'),
|
name: z.string().min(2, 'نام حداقل ۲ کاراکتر'),
|
||||||
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
|
gender: z.enum(['man', 'woman']).optional().or(z.literal('')),
|
||||||
@@ -2125,6 +2131,11 @@ const editSchema = z.object({
|
|||||||
info: z.string().max(2000).optional().or(z.literal('')),
|
info: z.string().max(2000).optional().or(z.literal('')),
|
||||||
specialties: z.array(z.number()).optional(),
|
specialties: z.array(z.number()).optional(),
|
||||||
services: z.array(z.number()).optional(),
|
services: z.array(z.number()).optional(),
|
||||||
|
social_instagram: urlOrEmpty,
|
||||||
|
social_telegram: urlOrEmpty,
|
||||||
|
social_aparat: urlOrEmpty,
|
||||||
|
social_youtube: urlOrEmpty,
|
||||||
|
social_linkedin: urlOrEmpty,
|
||||||
});
|
});
|
||||||
type EditForm = z.infer<typeof editSchema>;
|
type EditForm = z.infer<typeof editSchema>;
|
||||||
|
|
||||||
@@ -2310,6 +2321,11 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
info: doctor.detail ?? '',
|
info: doctor.detail ?? '',
|
||||||
specialties: doctor.specialties.map(s => Number(s.id)),
|
specialties: doctor.specialties.map(s => Number(s.id)),
|
||||||
services: doctor.expertise.map(s => Number(s.id)),
|
services: doctor.expertise.map(s => Number(s.id)),
|
||||||
|
social_instagram: doctor.social_media?.instagram ?? '',
|
||||||
|
social_telegram: doctor.social_media?.telegram ?? '',
|
||||||
|
social_aparat: doctor.social_media?.aparat ?? '',
|
||||||
|
social_youtube: doctor.social_media?.youtube ?? '',
|
||||||
|
social_linkedin: doctor.social_media?.linkedin ?? '',
|
||||||
});
|
});
|
||||||
setEditGender((doctor.gender as any) ?? '');
|
setEditGender((doctor.gender as any) ?? '');
|
||||||
}
|
}
|
||||||
@@ -2337,6 +2353,13 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
info: body.info || undefined,
|
info: body.info || undefined,
|
||||||
specialties: body.specialties ?? [],
|
specialties: body.specialties ?? [],
|
||||||
doctor_services: body.services ?? [],
|
doctor_services: body.services ?? [],
|
||||||
|
social_media: {
|
||||||
|
instagram: body.social_instagram || null,
|
||||||
|
telegram: body.social_telegram || null,
|
||||||
|
aparat: body.social_aparat || null,
|
||||||
|
youtube: body.social_youtube || null,
|
||||||
|
linkedin: body.social_linkedin || null,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('اطلاعات پزشک بروزرسانی شد');
|
toast.success('اطلاعات پزشک بروزرسانی شد');
|
||||||
@@ -2785,6 +2808,28 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Section: شبکههای اجتماعی ── */}
|
||||||
|
<div className="cp-card" style={{ padding: 'var(--card-pad)' }}>
|
||||||
|
<EditSectionHeader icon={<DocumentTextIcon style={{ width: 16, height: 16 }} />} title="شبکههای اجتماعی" />
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||||
|
<EditField label="اینستاگرام">
|
||||||
|
<input type="text" dir="ltr" className="cp-input" placeholder="https://instagram.com/..." {...register('social_instagram')} />
|
||||||
|
</EditField>
|
||||||
|
<EditField label="تلگرام">
|
||||||
|
<input type="text" dir="ltr" className="cp-input" placeholder="https://t.me/..." {...register('social_telegram')} />
|
||||||
|
</EditField>
|
||||||
|
<EditField label="آپارات">
|
||||||
|
<input type="text" dir="ltr" className="cp-input" placeholder="https://aparat.com/..." {...register('social_aparat')} />
|
||||||
|
</EditField>
|
||||||
|
<EditField label="یوتیوب">
|
||||||
|
<input type="text" dir="ltr" className="cp-input" placeholder="https://youtube.com/..." {...register('social_youtube')} />
|
||||||
|
</EditField>
|
||||||
|
<EditField label="لینکدین">
|
||||||
|
<input type="text" dir="ltr" className="cp-input" placeholder="https://linkedin.com/in/..." {...register('social_linkedin')} />
|
||||||
|
</EditField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── Section: تخصصها ── */}
|
{/* ── Section: تخصصها ── */}
|
||||||
<div className="cp-card" style={{ padding: 'var(--card-pad)' }}>
|
<div className="cp-card" style={{ padding: 'var(--card-pad)' }}>
|
||||||
<EditSectionHeader
|
<EditSectionHeader
|
||||||
|
|||||||
+24
-1
@@ -86,6 +86,13 @@ Get doctor detail with clinics.
|
|||||||
"degree": "specialist",
|
"degree": "specialist",
|
||||||
"detail": "...",
|
"detail": "...",
|
||||||
"img": [],
|
"img": [],
|
||||||
|
"social_media": {
|
||||||
|
"instagram": "https://instagram.com/dr.example",
|
||||||
|
"telegram": "https://t.me/dr_example",
|
||||||
|
"aparat": null,
|
||||||
|
"youtube": null,
|
||||||
|
"linkedin": null
|
||||||
|
},
|
||||||
"satisfaction": "60",
|
"satisfaction": "60",
|
||||||
"point": "3.5",
|
"point": "3.5",
|
||||||
"free_turn": "دوشنبه 09:00–13:00",
|
"free_turn": "دوشنبه 09:00–13:00",
|
||||||
@@ -216,7 +223,23 @@ Update doctor profile.
|
|||||||
| `uuid` | string (UUID) | Doctor UUID |
|
| `uuid` | string (UUID) | Doctor UUID |
|
||||||
|
|
||||||
### Request Body (`application/json`)
|
### Request Body (`application/json`)
|
||||||
Same fields as POST — all optional.
|
Same fields as POST (all optional), plus:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `social_media` | object | Keys: `instagram`, `telegram`, `aparat`, `youtube`, `linkedin`. Each value must be a full valid URL or `null`. Any value that fails `FILTER_VALIDATE_URL` is silently stored as `null`. |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"social_media": {
|
||||||
|
"instagram": "https://instagram.com/dr.example",
|
||||||
|
"telegram": "https://t.me/dr_example",
|
||||||
|
"aparat": null,
|
||||||
|
"youtube": null,
|
||||||
|
"linkedin": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Response `200`
|
### Response `200`
|
||||||
Updated doctor object (same structure as GET single).
|
Updated doctor object (same structure as GET single).
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260621084558 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE doctors ADD social_media JSON DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE doctors DROP social_media');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -278,6 +278,18 @@ class DoctorController extends BaseController
|
|||||||
items: new OA\Items(type: 'integer'),
|
items: new OA\Items(type: 'integer'),
|
||||||
nullable: true,
|
nullable: true,
|
||||||
),
|
),
|
||||||
|
new OA\Property(
|
||||||
|
property: 'social_media',
|
||||||
|
type: 'object',
|
||||||
|
nullable: true,
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'instagram', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'telegram', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'aparat', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'youtube', type: 'string', nullable: true),
|
||||||
|
new OA\Property(property: 'linkedin', type: 'string', nullable: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -725,6 +737,19 @@ class DoctorController extends BaseController
|
|||||||
$doctor->setImages($data['images']);
|
$doctor->setImages($data['images']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Social media links (each key nullable, must be a valid URL when present)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
$doctor->setSocialMedia($socialMedia);
|
||||||
|
}
|
||||||
|
|
||||||
// Specialties
|
// Specialties
|
||||||
if (array_key_exists('specialties', $data) && is_array($data['specialties'])) {
|
if (array_key_exists('specialties', $data) && is_array($data['specialties'])) {
|
||||||
$doctor->getSpecialties()->clear();
|
$doctor->getSpecialties()->clear();
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ class Doctor
|
|||||||
#[ORM\Column(type: 'json', nullable: true)]
|
#[ORM\Column(type: 'json', nullable: true)]
|
||||||
private ?array $images = null;
|
private ?array $images = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'social_media', type: 'json', nullable: true)]
|
||||||
|
private ?array $socialMedia = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'doctor_rate', type: 'float')]
|
#[ORM\Column(name: 'doctor_rate', type: 'float')]
|
||||||
private float $doctorRate = 3.5;
|
private float $doctorRate = 3.5;
|
||||||
|
|
||||||
@@ -139,6 +142,7 @@ class Doctor
|
|||||||
public function getDegree(): ?string { return $this->degree; }
|
public function getDegree(): ?string { return $this->degree; }
|
||||||
public function getInfo(): ?string { return $this->info; }
|
public function getInfo(): ?string { return $this->info; }
|
||||||
public function getImages(): ?array { return $this->images; }
|
public function getImages(): ?array { return $this->images; }
|
||||||
|
public function getSocialMedia(): ?array { return $this->socialMedia; }
|
||||||
public function getDoctorRate(): float { return $this->doctorRate; }
|
public function getDoctorRate(): float { return $this->doctorRate; }
|
||||||
public function getDoctorRatePercentage(): float { return $this->doctorRatePercentage; }
|
public function getDoctorRatePercentage(): float { return $this->doctorRatePercentage; }
|
||||||
public function isActiveDoctorAppointment(): bool { return $this->activeDoctorAppointment; }
|
public function isActiveDoctorAppointment(): bool { return $this->activeDoctorAppointment; }
|
||||||
@@ -160,6 +164,7 @@ class Doctor
|
|||||||
public function setDegree(?string $v): self { $this->degree = $v; $this->touch(); return $this; }
|
public function setDegree(?string $v): self { $this->degree = $v; $this->touch(); return $this; }
|
||||||
public function setInfo(?string $v): self { $this->info = $v; $this->touch(); return $this; }
|
public function setInfo(?string $v): self { $this->info = $v; $this->touch(); return $this; }
|
||||||
public function setImages(?array $v): self { $this->images = $v; $this->touch(); return $this; }
|
public function setImages(?array $v): self { $this->images = $v; $this->touch(); return $this; }
|
||||||
|
public function setSocialMedia(?array $v): self { $this->socialMedia = $v; $this->touch(); return $this; }
|
||||||
public function setDoctorRate(float $v): self { $this->doctorRate = $v; $this->touch(); return $this; }
|
public function setDoctorRate(float $v): self { $this->doctorRate = $v; $this->touch(); return $this; }
|
||||||
public function setDoctorRatePercentage(float $v): self { $this->doctorRatePercentage = $v; $this->touch(); return $this; }
|
public function setDoctorRatePercentage(float $v): self { $this->doctorRatePercentage = $v; $this->touch(); return $this; }
|
||||||
public function setActiveDoctorAppointment(bool $v): self { $this->activeDoctorAppointment = $v; $this->touch(); return $this; }
|
public function setActiveDoctorAppointment(bool $v): self { $this->activeDoctorAppointment = $v; $this->touch(); return $this; }
|
||||||
@@ -288,6 +293,7 @@ class Doctor
|
|||||||
], $this->specialties->toArray()),
|
], $this->specialties->toArray()),
|
||||||
'active' => $this->activeDoctorAppointment && $sf['has_schedule'],
|
'active' => $this->activeDoctorAppointment && $sf['has_schedule'],
|
||||||
'img' => $this->images ?? [],
|
'img' => $this->images ?? [],
|
||||||
|
'social_media' => $this->socialMedia,
|
||||||
'expertise' => array_map(fn(DoctorService $ds) => [
|
'expertise' => array_map(fn(DoctorService $ds) => [
|
||||||
'uuid' => $ds->getUuid(), 'id' => (string) $ds->getId(), 'name' => $ds->getName(),
|
'uuid' => $ds->getUuid(), 'id' => (string) $ds->getId(), 'name' => $ds->getName(),
|
||||||
], $this->services->toArray()),
|
], $this->services->toArray()),
|
||||||
|
|||||||
Reference in New Issue
Block a user