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.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# معماری — تسک ۰۲
|
||||
|
||||
## ساختار فایل
|
||||
|
||||
```
|
||||
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 />` دارند
|
||||
@@ -0,0 +1,143 @@
|
||||
# دیتابیس — تسک ۰۲
|
||||
|
||||
## `resource_types`
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `id` | INT PK AI | |
|
||||
| `uuid` | VARCHAR(36) UNIQUE | |
|
||||
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
|
||||
| `code` | VARCHAR(40) NOT NULL | `doctor`, `staff`, `room`, `device`, … |
|
||||
| `name` | VARCHAR(100) NOT NULL | نام نمایشی فارسی |
|
||||
| `is_system` | TINYINT(1) NOT NULL DEFAULT 0 | نوعهای `doctor/staff/room` توسط backfill ساخته میشوند و حذف نمیشوند |
|
||||
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
|
||||
| `created_at`/`updated_at` | INT NOT NULL | |
|
||||
|
||||
```sql
|
||||
KEY idx_resource_types_tenant (entity_type, entity_id, active)
|
||||
UNIQUE KEY uniq_rt_tenant_code (entity_type, entity_id, code)
|
||||
```
|
||||
|
||||
## `clinic_resources`
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `id` | INT PK AI | |
|
||||
| `uuid` | VARCHAR(36) UNIQUE | |
|
||||
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | از `branch` مشتق میشود |
|
||||
| `branch_id` | INT NOT NULL | FK → `branches.id` ON DELETE RESTRICT |
|
||||
| `resource_type_id` | INT NOT NULL | FK → `resource_types.id` ON DELETE RESTRICT |
|
||||
| `name` | VARCHAR(150) NOT NULL | |
|
||||
| `capacity` | SMALLINT NOT NULL DEFAULT 1 | ظرفیت همزمان |
|
||||
| `setup_minutes` | SMALLINT NOT NULL DEFAULT 0 | آمادهسازی پیش از بیمار |
|
||||
| `cleanup_minutes` | SMALLINT NOT NULL DEFAULT 0 | تمیزکاری پس از بیمار |
|
||||
| `attributes` | JSON NULL | اسکالر فقط، حداکثر ۲۰ کلید |
|
||||
| `doctor_id` | INT NULL UNIQUE | FK → `doctors.id` ON DELETE CASCADE |
|
||||
| `staff_id` | INT NULL UNIQUE | FK → `clinic_staff.id` ON DELETE CASCADE |
|
||||
| `room_id` | INT NULL UNIQUE | FK → `rooms.id` ON DELETE CASCADE |
|
||||
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
|
||||
| `created_at`/`updated_at` | INT NOT NULL | |
|
||||
|
||||
```sql
|
||||
KEY idx_resources_tenant (entity_type, entity_id, active)
|
||||
KEY idx_resources_branch_type (branch_id, resource_type_id, active)
|
||||
UNIQUE KEY uniq_resource_doctor (doctor_id)
|
||||
UNIQUE KEY uniq_resource_staff (staff_id)
|
||||
UNIQUE KEY uniq_resource_room (room_id)
|
||||
```
|
||||
|
||||
سه کلید یکتای تهیپذیر تضمین میکنند یک پزشک/پرسنل/اتاق بیش از یک منبع نگیرد.
|
||||
MariaDB چند `NULL` را در UNIQUE میپذیرد، پس دستگاههای بدون پل مشکلی ندارند.
|
||||
|
||||
> **قید سطح اپلیکیشن (نه DB):** حداکثر یکی از `doctor_id`/`staff_id`/`room_id` غیر-NULL.
|
||||
> در سازندهٔ entity اجبار شود؛ MariaDB `CHECK` چندستونی را قابل اتکا اجرا نمیکند.
|
||||
|
||||
## `skills` و `resource_skills`
|
||||
|
||||
```sql
|
||||
CREATE TABLE skills (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
uuid VARCHAR(36) NOT NULL UNIQUE,
|
||||
entity_type VARCHAR(10) NOT NULL,
|
||||
entity_id INT NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at INT NOT NULL,
|
||||
updated_at INT NOT NULL,
|
||||
KEY idx_skills_tenant (entity_type, entity_id, active)
|
||||
);
|
||||
|
||||
CREATE TABLE resource_skills (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
resource_id INT NOT NULL,
|
||||
skill_id INT NOT NULL,
|
||||
level TINYINT NOT NULL DEFAULT 1, -- 1..5
|
||||
created_at INT NOT NULL,
|
||||
UNIQUE KEY uniq_resource_skill (resource_id, skill_id),
|
||||
KEY idx_resource_skills_skill (skill_id, level),
|
||||
CONSTRAINT fk_rs_resource FOREIGN KEY (resource_id) REFERENCES clinic_resources(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_rs_skill FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE RESTRICT
|
||||
);
|
||||
```
|
||||
|
||||
`idx_resource_skills_skill (skill_id, level)` عمدی است: پرسوجوی داغِ تسک ۰۶
|
||||
«کدام منابع مهارت X را دارند» از این سمت میآید.
|
||||
|
||||
`resource_skills` بدون ستون tenant — فرزند aggregate با ریشهٔ `ClinicResource`،
|
||||
و uuid از request نمیگیرد (همیشه از `/resource/{uuid}/skills`).
|
||||
|
||||
## `resource_pools` و `resource_pool_members`
|
||||
|
||||
```sql
|
||||
CREATE TABLE resource_pools (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
uuid VARCHAR(36) NOT NULL UNIQUE,
|
||||
entity_type VARCHAR(10) NOT NULL,
|
||||
entity_id INT NOT NULL,
|
||||
branch_id INT NOT NULL,
|
||||
resource_type_id INT NOT NULL,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at INT NOT NULL,
|
||||
updated_at INT NOT NULL,
|
||||
KEY idx_pools_tenant (entity_type, entity_id, active),
|
||||
KEY idx_pools_branch (branch_id, resource_type_id),
|
||||
CONSTRAINT fk_pool_branch FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pool_type FOREIGN KEY (resource_type_id) REFERENCES resource_types(id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE TABLE resource_pool_members (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
pool_id INT NOT NULL,
|
||||
resource_id INT NOT NULL,
|
||||
priority SMALLINT NOT NULL DEFAULT 0, -- ترتیب ترجیح در استراتژی انتخاب
|
||||
UNIQUE KEY uniq_pool_resource (pool_id, resource_id),
|
||||
KEY idx_pool_members_resource (resource_id),
|
||||
CONSTRAINT fk_pm_pool FOREIGN KEY (pool_id) REFERENCES resource_pools(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pm_resource FOREIGN KEY (resource_id) REFERENCES clinic_resources(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
## Migration و backfill
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console doctrine:migrations:diff --no-interaction
|
||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||
ddev exec php bin/console app:resource:backfill # dry-run
|
||||
ddev exec php bin/console app:resource:backfill --force
|
||||
```
|
||||
|
||||
`app:resource:backfill` برای هر محیط:
|
||||
1. سه `resource_type` سیستمی میسازد (`doctor`, `staff`, `room`) اگر نباشند
|
||||
2. هر `Room` فعال → یک منبع با `capacity` همان اتاق
|
||||
3. هر `ClinicStaff` فعال → یک منبع `type=staff` در شعبهٔ اول محیط
|
||||
4. هر `Doctor` دارای `WeeklySchedule` در آن محیط → یک منبع `type=doctor`
|
||||
|
||||
idempotent باشد: اجرای دوباره چیزی دوباره نمیسازد.
|
||||
|
||||
## طبقهبندی tenant
|
||||
|
||||
| جدول | وضعیت |
|
||||
|---|---|
|
||||
| `resource_types`, `clinic_resources`, `skills`, `resource_pools` | جفت tenant |
|
||||
| `resource_skills`, `resource_pool_members` | `AGGREGATE_CHILDREN` |
|
||||
@@ -0,0 +1,111 @@
|
||||
# نکات پیادهسازی — تسک ۰۲
|
||||
|
||||
## ۱. ظرفیت: یک ردیف با ظرفیت ۳، نه سه ردیف
|
||||
|
||||
مستند بند ۶ صریح است و دلیلش در تسک ۰۶ روشن میشود: با سه ردیف، موتور جستجو باید سه
|
||||
تقویم را ادغام کند و «کدام تخت» بشود یک تصمیم که هیچکس نمیخواهد بگیرد. با ظرفیت ۳،
|
||||
شرط اشغال یک شمارش ساده است:
|
||||
|
||||
```
|
||||
SELECT COUNT(*) FROM resource_occupancy
|
||||
WHERE resource_id = ? AND [بازهها تداخل دارند] AND status IN (…)
|
||||
→ اگر < capacity، جا هست
|
||||
```
|
||||
|
||||
این را همین حالا در `docs/api/resource.md` بنویس تا کسی وسوسه نشود «اتاق ۱ تخت ۲» بسازد.
|
||||
|
||||
## ۲. `ClinicResource` نه `Resource`
|
||||
|
||||
نام کلاس `Resource` در PHP مشکل ندارد ولی در این کدبیس با `App\Shared\…` و مفهوم
|
||||
«منبع API» قاطی میشود و در جستجوی کد نویز شدیدی میسازد. نام جدول `clinic_resources`
|
||||
هم به همان دلیل. متغیرها و متدها میتوانند `$resource` باشند.
|
||||
|
||||
## ۳. پلها و `active`
|
||||
|
||||
`ClinicStaff.active = false` باید `ClinicResource.active` را هم false کند، وگرنه پرسنل
|
||||
غیرفعال همچنان در جستجوی وقت ظاهر میشود. این را در `StaffService` (تسک موجود) با یک
|
||||
فراخوانی به `ResourceLinker::syncActive()` انجام بده — نه با Doctrine lifecycle callback،
|
||||
چون callback در `getArrayResult()` اجرا نمیشود و رفتار نامتقارن میسازد.
|
||||
|
||||
عکسش برقرار نیست: غیرفعال کردن منبع، پرسنل را غیرفعال نمیکند (پرسنل ممکن است فقط
|
||||
اداری باشد).
|
||||
|
||||
## ۴. `setup_minutes` / `cleanup_minutes` چه هستند و چه نیستند
|
||||
|
||||
- **جزو نوبت بیمار نیستند** — بیمار ساعت ۱۰:۰۰ میآید و ۱۰:۳۰ میرود
|
||||
- **منبع را اشغال میکنند** — یونیت از ۹:۵۵ تا ۱۰:۴۰ در دسترس نیست
|
||||
|
||||
پس در تسک ۰۷ بازهٔ ثبتشده در `resource_occupancy` گستردهتر از بازهٔ نوبت است. الان فقط
|
||||
ستون را بساز و در `docs/api/resource.md` این تفاوت را بنویس؛ محاسبهاش کار تسک ۰۶ است.
|
||||
|
||||
اشتباه رایج: این را با `WeeklySchedule.meta.buffer_minutes` موجود یکی گرفتن. آن یکی
|
||||
فاصلهٔ سراسری بین دو نوبتِ **پزشک** است؛ این یکی per منبع است. تا وقتی حالت `resource`
|
||||
نیامده، هر دو کنار هم زندگی میکنند و `buffer_minutes` دستنخورده میماند.
|
||||
|
||||
## ۵. مهارت را با `jobTitle` قاطی نکن
|
||||
|
||||
`ClinicStaff.jobTitle` متن آزاد و برای نمایش است. مهارت یک موجودیت با هویت است که در
|
||||
شرط نیازمندی (تسک ۰۵) و شرط قانون (تسک ۰۹) استفاده میشود. هیچجا `jobTitle` را برای
|
||||
تصمیمگیری parse نکن.
|
||||
|
||||
## ۶. edge case ها
|
||||
|
||||
| حالت | رفتار درست |
|
||||
|---|---|
|
||||
| منبع بدون هیچ پل (دستگاه) | معتبر — حالت عادی تجهیزات |
|
||||
| دو پل همزمان (`doctor_id` و `staff_id`) | `422` در سازنده |
|
||||
| حذف `resource_type` که منبع دارد | `422` |
|
||||
| حذف `resource_type` با `is_system=1` | `422` همیشه |
|
||||
| غیرفعال کردن منبعی که نوبت آیندهٔ فعال دارد | مجاز، ولی پاسخ شامل `warnings[]` با تعداد نوبتها |
|
||||
| استخر با صفر عضو | معتبر (در حال ساخت)، ولی تسک ۰۶ آن را «هیچ منبعی» میبیند |
|
||||
| `capacity` روی منبع `type=doctor` بزرگتر از ۱ | `422` — پزشک همزمان دو بیمار ندارد |
|
||||
| `level` خارج از ۱..۵ | `422` |
|
||||
| مهارت محیط A روی منبع محیط B | `404` (پیش از هر بررسی: `TenantOwnershipChecker`) |
|
||||
|
||||
## ۷. کارایی
|
||||
|
||||
پرسوجوی داغ تسک ۰۶: «منابع فعالِ شعبهٔ X از نوع Y که مهارت Z را دارند».
|
||||
|
||||
```php
|
||||
// ClinicResourceRepository::findEligible()
|
||||
// یک کوئری با JOIN به resource_skills، بدون N+1
|
||||
$qb->select('r')
|
||||
->from(ClinicResource::class, 'r')
|
||||
->join('r.skills', 'rs')
|
||||
->where('r.branch = :branch')
|
||||
->andWhere('r.type = :type')
|
||||
->andWhere('r.active = true')
|
||||
->andWhere('rs.skill IN (:skills)')
|
||||
->groupBy('r.id')
|
||||
->having('COUNT(DISTINCT rs.skill) = :skillCount'); // همهٔ مهارتها، نه یکی
|
||||
```
|
||||
|
||||
`HAVING COUNT(DISTINCT …)` عمدی است: نیازمندی «مهارت الف و ب» یعنی هر دو، نه یکی.
|
||||
|
||||
## ۸. تست
|
||||
|
||||
```
|
||||
tests/Resource/ResourceCrudTest.php
|
||||
- ساخت منبع + خواندن → tenant و branch درست
|
||||
- منبع با شعبهٔ محیط دیگر → 404
|
||||
- capacity=0 → 422 · capacity=2 روی type=doctor → 422
|
||||
- دو پل همزمان → 422
|
||||
tests/Resource/SkillAssignmentTest.php
|
||||
- PUT skills جایگزینی کامل (حذف ندادهها)
|
||||
- level خارج بازه → 422
|
||||
- حذف مهارتِ در استفاده → 422
|
||||
tests/Resource/ResourcePoolTest.php
|
||||
- عضو از شعبهٔ دیگر → 422
|
||||
- عضو از نوع دیگر → 422
|
||||
tests/Resource/ResourceEligibilityTest.php
|
||||
- findEligible با دو مهارت: منبعی که فقط یکی را دارد برنمیگردد
|
||||
tests/Resource/BackfillResourceTest.php
|
||||
- idempotent: دو بار اجرا = یک بار
|
||||
tests/Shared/TenantSchemaCoverageTest.php
|
||||
tests/Shared/TenantLookupInventoryTest.php ← شمارنده بهروز شود
|
||||
```
|
||||
|
||||
## ۹. مستندات
|
||||
|
||||
`docs/api/resource.md` بساز. در `docs/api/staff.md` یک بخش «رابطه با منبع» اضافه کن.
|
||||
`docs/architecture/tenancy.md` جدول طبقهبندی را بهروز کن.
|
||||
@@ -0,0 +1,75 @@
|
||||
# تسک ۰۲ — مدل منبع: نوع منبع، منبع، مهارت، استخر
|
||||
|
||||
**فاز:** ۱ (هسته) · **وابستگی:** ۰۱ · **زمان:** ۱۴-۱۸ ساعت
|
||||
|
||||
---
|
||||
|
||||
## هدف
|
||||
|
||||
قانون طلایی اول مستند: «تقویم مال منبع است، نه مال پزشک». امروز تنها موجودیتی که
|
||||
میتواند اشغال شود پزشک است و پرسنل (`ClinicStaff`) فقط یک برچسب روی سرویس و نوبت است.
|
||||
این تسک لایهٔ منبع را میسازد: هر چیزی که ممکن است اشغال باشد — پزشک، اپراتور، دستیار،
|
||||
دستگاه، اتاق، تخت، یونیت.
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
```php
|
||||
// src/Staff/Entity/ClinicStaff.php — تنها «منبع» امروز
|
||||
private string $fullName;
|
||||
private ?string $jobTitle; // متن آزاد، بدون معنای ساختاری
|
||||
private bool $active;
|
||||
private ?User $user; // برای ورود به پنل
|
||||
```
|
||||
|
||||
بدون تقویم، بدون ظرفیت، بدون مهارت. `ServiceItem.staffMembers` یک ManyToMany به همین است
|
||||
و `Appointment.staff_id` یک ارجاع تکی — هیچکدام تداخل زمانی را بررسی نمیکنند.
|
||||
|
||||
## دامنه
|
||||
|
||||
**هست:** `ResourceType`، `Resource`، `Skill`، `ResourceSkill`، `ResourcePool`،
|
||||
`ResourcePoolMember`، ویژگیهای آزاد (`attributes` JSON)، ظرفیت همزمان،
|
||||
زمان آمادهسازی/تمیزکاری per منبع، CRUD پنل، و **پل زدن `ClinicStaff` و `Doctor` و `Room`
|
||||
به `Resource`**.
|
||||
|
||||
**نیست:** تقویم و مرخصی (تسک ۰۳)، نیازمندی منبع per بخش (تسک ۰۵)، محاسبهٔ اشغال (تسک ۰۶/۰۷).
|
||||
|
||||
## Endpoint ها
|
||||
|
||||
| متد | مسیر | توضیح |
|
||||
|---|---|---|
|
||||
| GET/POST | `/api/v1/resource-types` | نوع منبع (کلینیک خودش تعریف میکند) |
|
||||
| PATCH/DELETE | `/api/v1/resource-type/{uuid}` | |
|
||||
| GET | `/api/v1/resources` | لیست با فیلتر `branch_uuid`, `type_uuid`, `active` |
|
||||
| POST | `/api/v1/resource` | ساخت منبع |
|
||||
| GET/PATCH/DELETE | `/api/v1/resource/{uuid}` | |
|
||||
| GET/POST | `/api/v1/skills` | مهارتها |
|
||||
| PATCH/DELETE | `/api/v1/skill/{uuid}` | |
|
||||
| PUT | `/api/v1/resource/{uuid}/skills` | جایگزینی کامل مهارتهای منبع |
|
||||
| GET/POST | `/api/v1/resource-pools` | استخر منابع قابل جایگزینی |
|
||||
| PATCH/DELETE | `/api/v1/resource-pool/{uuid}` | |
|
||||
| PUT | `/api/v1/resource-pool/{uuid}/members` | جایگزینی کامل اعضا |
|
||||
|
||||
## معیار پذیرش
|
||||
|
||||
- ✅ موفق: کلینیک نوع منبع «دستگاه لیزر» میسازد، سه منبع از آن نوع در شعبهٔ مرکزی ثبت
|
||||
میکند، یک استخر «لیزرهای آلکساندرایت» میسازد و هر سه را عضو میکند →
|
||||
`GET /api/v1/resource-pool/{uuid}` هر سه را با `branch_uuid` برمیگرداند.
|
||||
- ✅ موفق: مهارت «لیزر آلکساندرایت» ساخته و به دو اپراتور داده میشود →
|
||||
`GET /api/v1/resources?skill_uuid=…` فقط همان دو را برمیگرداند.
|
||||
- ✅ موفق: بعد از اجرای `app:resource:backfill`، هر `ClinicStaff` فعال یک `Resource` با
|
||||
`type=staff` و هر `Doctor` دارای برنامه یک `Resource` با `type=doctor` دارد.
|
||||
- ❌ خطا: منبع با `branch_uuid` متعلق به محیط دیگر → `404`.
|
||||
- ❌ خطا: عضو کردن منبعی از شعبهٔ A در استخری که منابعش در شعبهٔ B هستند → `422`
|
||||
«همهٔ اعضای استخر باید در یک شعبه باشند».
|
||||
- ⚠️ مرزی: `capacity = 3` روی اتاق تزریق → یک ردیف، نه سه. `GET` مقدار ۳ را برمیگرداند.
|
||||
- ⚠️ مرزی: `setup_minutes = 0, cleanup_minutes = 10` → معتبر.
|
||||
- ⚠️ مرزی: حذف مهارتی که به منبعی داده شده → `422`؛ باید اول از منابع برداشته شود.
|
||||
- ⚠️ مرزی: `attributes` با کلید ناشناخته → پذیرفته میشود (عمداً آزاد)، ولی مقدار غیر
|
||||
اسکالر → `422`.
|
||||
|
||||
## خروجی
|
||||
|
||||
- `src/Resource/` کامل با تست
|
||||
- صفحات پنل: `ResourcesPage`, `ResourceFormPage`, `ResourceTypesPage`, `SkillsPage`, `ResourcePoolsPage`
|
||||
- `docs/api/resource.md`
|
||||
- `app:resource:backfill` (dry-run پیشفرض)
|
||||
Reference in New Issue
Block a user