Files
clinicpro/docs/new_feture/taskes/task-02-resource-model/architecture.md
T
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- 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.
2026-07-30 11:43:58 +03:30

5.9 KiB

معماری — تسک ۰۲

ساختار فایل

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

#[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 تنها نقطه‌ای است که این نگاشت را می‌داند:

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 با حدس.

استخر منابع

class ResourcePool
{
    use TenantOwnedTrait;
    private Branch $branch;        // استخر درون یک شعبه است — منبعِ شعبهٔ دیگر جایگزین نیست
    private ResourceType $type;    // اعضا باید هم‌نوع باشند
    private string $name;
    private Collection $members;   // ResourcePoolMember
}

قاعدهٔ اعتبار در ResourcePoolService::replaceMembers(): همهٔ اعضا باید branch و type یکسان با خود استخر داشته باشند، وگرنه 422. دلیل: تسک ۰۶ فرض می‌کند «هر عضو استخر جایگزین کامل دیگری است» — اگر یکی در شعبهٔ دیگری باشد، بیمار در ساختمان اشتباه می‌ایستد.

اعتبارسنجی attributes

JSON آزاد است ولی نه بی‌قید:

// ResourceService::normalizeAttributes()
// - کلید: [a-z_]{1,40}
// - مقدار: string|int|bool|float فقط — نه آرایه، نه object
// - حداکثر ۲۰ کلید

دلیل محدودیت اسکالر: تسک ۰۵ قید same_gender و تسک ۰۹ شرط‌های منبع را روی همین مقادیر با مقایسهٔ ساده می‌سنجند. آرایهٔ تودرتو یعنی مقایسهٔ دلخواه، یعنی همان چیزی که مستند بند ۸ ممنوع کرده.

کلیدهای شناخته‌شده (قرارداد، نه اجبار): gender, device_model, floor, brand.

پنل ادمین

  • ResourcesPage.tsxDataTable با فیلتر شعبه/نوع/فعال، همه در URL (useUrlState)
  • ResourceFormPage.tsxSearchableSelect برای شعبه و نوع، چیپ برای مهارت‌ها، PriceInput لازم نیست، ولی برای setup/cleanup عدد ساده با پسوند «دقیقه»
  • SkillsPage.tsx, ResourceTypesPage.tsx, ResourcePoolsPage.tsx — لیست‌های ساده
  • همهٔ زیرصفحه‌ها backTo یا <BackButton /> دارند