diff --git a/docs/api/treatment.md b/docs/api/treatment.md index d6fbe087..dd4e5287 100644 --- a/docs/api/treatment.md +++ b/docs/api/treatment.md @@ -282,6 +282,62 @@ single-session again. Idempotent: deleting a service that has no protocol still --- +## GET `/api/v1/treatment-case/{uuid}/plan` + +تقویمِ **کل** دوره به‌علاوهٔ آنچه در هر جلسه انجام شده. مصرف‌کننده‌اش تب «نوبت‌های +بعدی» در پروندهٔ بیمار است. + +جدا از `GET /treatment-case/{uuid}` است نه اضافه به آن: آن پاسخ مصرف‌کنندهٔ دیگری +دارد (مودال ویرایش) که نه تقویم لازم دارد نه نواحی. + +| فیلد هر جلسه | توضیح | +|---|---| +| `planned_at` | زمان جلسه — قطعی یا تخمینی | +| `is_estimate` | `false` یعنی به واقعیتی گره خورده، `true` یعنی محاسبهٔ لحظهٔ نمایش | +| `areas[]` | نواحی با `parameters` (خوانده‌های دستگاه)، `resource`، `note`، زمان‌ها | + +**`planned_at` ذخیره نمی‌شود.** `TreatmentScheduler` فقط سررسید جلسهٔ بعدی را +می‌نویسد؛ بقیهٔ زنجیره را `TreatmentPlanProjector` در لحظهٔ خواندن می‌سازد. لنگرِ هر +جلسه به‌ترتیب: زمان اتمام، زمان نوبت، `due_at` نوشته‌شده. دوره‌ای که هیچ‌کدام را +ندارد از `opened_at` شروع می‌شود. + +یعنی تاریخ‌های تخمینی ممکن است بین دو بار خواندن عوض شوند — این عمدی است و در UI با +برچسب «تخمینی» اعلام می‌شود. + +### Response `200` (خروجی واقعی) + +```json +{ + "success": true, + "data": { + "case": { "uuid": "2a8b1e28-…", "status": "active", "patient": { "name": "محمد رستمی" } }, + "sessions": [ + { + "session_number": 1, + "status": "done", + "planned_at": 1786106340, + "is_estimate": false, + "areas": [ + { "area": { "name": "دست" }, "status": "completed", + "parameters": { "energy": 8, "pulse": 3, "shots": 23 } } + ] + }, + { "session_number": 2, "status": "planned", "planned_at": 1787402340, "is_estimate": false, "areas": [] }, + { "session_number": 3, "status": "planned", "planned_at": 1789994340, "is_estimate": true, "areas": [] } + ] + } +} +``` + +**Errors:** + +| Code | HTTP | شرط | +|---|---|---| +| ERR_NOT_FOUND_001 | 404 | پرونده یافت نشد یا مال محیط دیگری است | +| ERR_AUTH_001 | 401 | بدون توکن | + +--- + ## GET `/api/v1/treatment-session/{uuid}` یک جلسه به‌تنهایی، به‌علاوهٔ `case_uuid`، `service` و `patient` — برای فرمِ «ثبت نوبت این diff --git a/src/Treatment/Controller/TreatmentCaseController.php b/src/Treatment/Controller/TreatmentCaseController.php index 095102b2..28b73ae9 100644 --- a/src/Treatment/Controller/TreatmentCaseController.php +++ b/src/Treatment/Controller/TreatmentCaseController.php @@ -17,6 +17,7 @@ use App\Treatment\Repository\TreatmentSessionRepository; use App\Treatment\Service\NextSessionSlotFinder; use App\Treatment\Service\TreatmentCaseEditor; use App\Treatment\Service\TreatmentCaseOpener; +use App\Treatment\Service\TreatmentPlanProjector; use OpenApi\Attributes as OA; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -37,6 +38,7 @@ class TreatmentCaseController extends BaseController private readonly AddressResolver $branches, private readonly TreatmentCaseOpener $opener, private readonly TreatmentCaseEditor $editor, + private readonly TreatmentPlanProjector $planner, ) {} #[Route('/api/v1/treatment-cases', name: 'treatment_case_list', methods: ['GET'])] @@ -142,6 +144,32 @@ class TreatmentCaseController extends BaseController )); } + /** + * تقویمِ کل دوره + جزئیات آنچه انجام شده. + * + * جدا از `GET /treatment-case/{uuid}` است نه اضافه به آن: آن پاسخ مصرف‌کنندهٔ + * دیگری دارد (مودال ویرایش) که نه تقویم لازم دارد نه نواحی، و بزرگ‌ترش کردن یعنی + * هزینهٔ بی‌مصرف روی همان مسیر. + * + * `planned_at` تخمین است نه دادهٔ ذخیره‌شده — `is_estimate` تکلیفش را روشن می‌کند. + */ + #[Route('/api/v1/treatment-case/{uuid}/plan', name: 'treatment_case_plan', methods: ['GET'])] + public function plan(#[CurrentUser] User $user, string $uuid): JsonResponse + { + $case = $this->requireCase($user, $uuid); + + return $this->success([ + 'case' => $case->toArray(), + 'sessions' => array_map( + static fn (array $row): array => $row['session']->toArray(withAreas: true) + [ + 'planned_at' => $row['planned_at'], + 'is_estimate' => $row['is_estimate'], + ], + $this->planner->project($case), + ), + ]); + } + /** * یک جلسه به‌تنهایی — برای فرمِ «ثبت نوبت این جلسه». * diff --git a/tests/Treatment/TreatmentCaseEndpointsTest.php b/tests/Treatment/TreatmentCaseEndpointsTest.php index 98f1c75f..ff0c3631 100644 --- a/tests/Treatment/TreatmentCaseEndpointsTest.php +++ b/tests/Treatment/TreatmentCaseEndpointsTest.php @@ -204,4 +204,37 @@ class TreatmentCaseEndpointsTest extends ApiTestCase self::assertSame(404, $this->responseCode()); } + + /** تقویم دوره: همهٔ جلسات با تاریخ، و جزئیات نواحیِ جلسهٔ انجام‌شده. */ + public function testPlanReturnsEverySessionWithADate(): void + { + $s = $this->scenario(); + + $body = $this->authJson('GET', '/api/v1/treatment-case/' . $s['case']->getUuid() . '/plan', $s['user']); + + self::assertSame(200, $this->responseCode()); + self::assertSame( + $s['case']->getTotalSessions(), + count($body['data']['sessions']), + 'تقویم باید همهٔ جلسات را بدهد، نه فقط آن‌هایی که سررسید نوشته دارند', + ); + + foreach ($body['data']['sessions'] as $row) { + self::assertArrayHasKey('planned_at', $row); + self::assertArrayHasKey('is_estimate', $row); + // نواحی باید بیایند — «چه کاری انجام شد» همین است. + self::assertArrayHasKey('areas', $row); + } + } + + /** حالت مرزی: پروندهٔ محیط دیگر — تقویم هم مثل بقیه ۴۰۴ می‌گیرد نه ۴۰۳. */ + public function testPlanOfAnotherTenantIs404(): void + { + $mine = $this->scenario(); + $other = $this->scenario(); + + $this->authJson('GET', '/api/v1/treatment-case/' . $other['case']->getUuid() . '/plan', $mine['user']); + + self::assertSame(404, $this->responseCode()); + } }