- Add implementation notes for cancellation and waitlist features. - Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting. - Establish architecture for domain events and outbox pattern to ensure reliable event publishing. - Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports. - Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
141 lines
5.9 KiB
Markdown
141 lines
5.9 KiB
Markdown
# معماری — تسک ۰۲
|
|
|
|
## ساختار فایل
|
|
|
|
```
|
|
src/Resource/
|
|
├── Controller/
|
|
│ ├── ResourceController.php
|
|
│ ├── ResourceTypeController.php
|
|
│ ├── SkillController.php
|
|
│ └── ResourcePoolController.php
|
|
├── Entity/
|
|
│ ├── ResourceType.php
|
|
│ ├── ClinicResource.php # نامِ کلاس عمداً Resource نیست (تداخل با کلمهٔ رزرو PHP نیست، ولی با Symfony/Doctrine ابهام دارد)
|
|
│ ├── Skill.php
|
|
│ ├── ResourceSkill.php
|
|
│ ├── ResourcePool.php
|
|
│ └── ResourcePoolMember.php
|
|
├── Repository/…
|
|
├── Service/
|
|
│ ├── ResourceService.php
|
|
│ ├── SkillAssignmentService.php
|
|
│ ├── ResourcePoolService.php
|
|
│ └── ResourceLinker.php # پل بین Doctor/ClinicStaff/Room و ClinicResource
|
|
└── Command/
|
|
└── BackfillResourceCommand.php
|
|
```
|
|
|
|
## `ClinicResource`
|
|
|
|
```php
|
|
#[ORM\Entity(repositoryClass: ClinicResourceRepository::class)]
|
|
#[ORM\Table(name: 'clinic_resources')]
|
|
#[ORM\Index(columns: ['entity_type', 'entity_id', 'active'], name: 'idx_resources_tenant')]
|
|
class ClinicResource
|
|
{
|
|
use TenantOwnedTrait;
|
|
|
|
private string $uuid;
|
|
private Branch $branch; // منابع همیشه مال شعبهاند — فیزیکیاند
|
|
private ResourceType $type;
|
|
private string $name;
|
|
private int $capacity = 1; // ظرفیت همزمان
|
|
private int $setupMinutes = 0;
|
|
private int $cleanupMinutes = 0;
|
|
private array $attributes = []; // JSON آزاد: gender, device_model, floor
|
|
private bool $active = true;
|
|
|
|
// ── پل به موجودیتهای موجود؛ حداکثر یکی غیر-null است ──
|
|
private ?Doctor $doctor = null;
|
|
private ?ClinicStaff $staff = null;
|
|
private ?Room $room = null;
|
|
}
|
|
```
|
|
|
|
### چرا پل، نه ادغام
|
|
|
|
`Doctor` و `ClinicStaff` و `Room` هرکدام هویت مستقل و مصرفکنندهٔ زنده دارند
|
|
(`appointments.doctor_id`، `service_item_staff`، سایت عمومی). تبدیل آنها به زیرکلاس
|
|
`Resource` یعنی مهاجرت همزمان همهٔ آن مسیرها. بهجایش:
|
|
|
|
```
|
|
Doctor 1 ──0..1 ClinicResource (type=doctor)
|
|
ClinicStaff 1 ──0..1 ClinicResource (type=staff)
|
|
Room 1 ──0..1 ClinicResource (type=room)
|
|
ClinicResource بدون پل = دستگاه/تجهیزات
|
|
```
|
|
|
|
`ResourceLinker` تنها نقطهای است که این نگاشت را میداند:
|
|
|
|
```php
|
|
final class ResourceLinker
|
|
{
|
|
/** منبعِ متناظر با یک پرسنل؛ اگر نبود میسازد. */
|
|
public function forStaff(ClinicStaff $staff): ClinicResource { … }
|
|
|
|
public function forDoctor(Doctor $doctor, Branch $branch): ClinicResource { … }
|
|
|
|
/** برعکس: منبع → موجودیت اصلی، برای نمایش در UI. */
|
|
public function subject(ClinicResource $r): Doctor|ClinicStaff|Room|null { … }
|
|
}
|
|
```
|
|
|
|
هیچ سرویس دیگری نباید مستقیم `$resource->getStaff()` را برای تصمیمگیری بخواند.
|
|
|
|
## مهارتها — جدول، نه قانون
|
|
|
|
مستند بند ۶ صریح است: «کدام اپراتور مجاز است با کدام دستگاه کار کند» یک **اطلاعات** است،
|
|
نه یک قانون. با ۵۰ اپراتور و ۲۰۰ سرویس، سپردنش به موتور قوانین یعنی ۱۰٬۰۰۰ قانون.
|
|
|
|
```
|
|
skills (uuid, name, tenant)
|
|
resource_skills (resource_id, skill_id, level) ← جدول واسط ساده
|
|
```
|
|
|
|
`level` (۱..۵) از روز اول هست چون تسک ۰۶ استراتژی «حفظ متخصصها» را روی همین میسازد
|
|
و افزودنش بعداً یعنی backfill با حدس.
|
|
|
|
## استخر منابع
|
|
|
|
```php
|
|
class ResourcePool
|
|
{
|
|
use TenantOwnedTrait;
|
|
private Branch $branch; // استخر درون یک شعبه است — منبعِ شعبهٔ دیگر جایگزین نیست
|
|
private ResourceType $type; // اعضا باید همنوع باشند
|
|
private string $name;
|
|
private Collection $members; // ResourcePoolMember
|
|
}
|
|
```
|
|
|
|
قاعدهٔ اعتبار در `ResourcePoolService::replaceMembers()`:
|
|
همهٔ اعضا باید `branch` و `type` یکسان با خود استخر داشته باشند، وگرنه `422`.
|
|
دلیل: تسک ۰۶ فرض میکند «هر عضو استخر جایگزین کامل دیگری است» — اگر یکی در شعبهٔ
|
|
دیگری باشد، بیمار در ساختمان اشتباه میایستد.
|
|
|
|
## اعتبارسنجی `attributes`
|
|
|
|
JSON آزاد است ولی نه بیقید:
|
|
|
|
```php
|
|
// ResourceService::normalizeAttributes()
|
|
// - کلید: [a-z_]{1,40}
|
|
// - مقدار: string|int|bool|float فقط — نه آرایه، نه object
|
|
// - حداکثر ۲۰ کلید
|
|
```
|
|
|
|
دلیل محدودیت اسکالر: تسک ۰۵ قید `same_gender` و تسک ۰۹ شرطهای منبع را روی همین
|
|
مقادیر با مقایسهٔ ساده میسنجند. آرایهٔ تودرتو یعنی مقایسهٔ دلخواه، یعنی همان چیزی که
|
|
مستند بند ۸ ممنوع کرده.
|
|
|
|
کلیدهای شناختهشده (قرارداد، نه اجبار): `gender`, `device_model`, `floor`, `brand`.
|
|
|
|
## پنل ادمین
|
|
|
|
- `ResourcesPage.tsx` — `DataTable` با فیلتر شعبه/نوع/فعال، همه در URL (`useUrlState`)
|
|
- `ResourceFormPage.tsx` — `SearchableSelect` برای شعبه و نوع، چیپ برای مهارتها،
|
|
`PriceInput` لازم نیست، ولی برای `setup/cleanup` عدد ساده با پسوند «دقیقه»
|
|
- `SkillsPage.tsx`, `ResourceTypesPage.tsx`, `ResourcePoolsPage.tsx` — لیستهای ساده
|
|
- همهٔ زیرصفحهها `backTo` یا `<BackButton />` دارند
|