fix(treatment): let the operator actually record an area

Starting a session created area records with no device, and the panel only ever
read the device it never set — so every "اتمام این ناحیه" came back 422 with
"دستگاه این ناحیه مشخص نیست". The backend tests passed because they sent
resource_uuid explicitly; from the UI the flow was unusable end to end.

The device now inherits from the appointment's resource, which the secretary
already chose at booking; asking the operator again is taking one decision
twice. The session screen offers a picker per area on top of that, because one
session really does run bikini on an alexandrite and underarms on a diode.

Treating without a device is allowed: botox is an injection, and requiring a
device would make clinics invent a fake resource per injection. Sending readings
with no device is still rejected — there would be no schema to validate against.

A protocol whose service has no ResourceServiceOffering rows now says so in the
tab where the manager is standing. It does not block booking: "no offering means
any resource" is a deliberate, tested rule. But silence meant the gap surfaced
only when the operator was already in front of a patient.

Also adds the live timer the spec asked for, and wires slot-suggestions into the
unbooked queue — the endpoint existed and tested green but no screen called it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 11:08:48 +03:30
co-authored by Claude Opus 5
parent c7fdb92df4
commit a63de2a52c
11 changed files with 412 additions and 37 deletions
@@ -74,10 +74,23 @@ class SessionExecutionController extends BaseController
{
$session = $this->requireSession($user, $uuid);
[$entityType, $entityId] = $this->pair($user);
$devices = $this->resources->findForPair($entityType, $entityId, ['active' => true]);
return $this->success($session->toArray(withAreas: true) + [
'case' => $session->getTreatmentCase()->toArray(),
// فرمِ هر ناحیه از نوع منبعش می‌آید؛ پنل نباید فیلدها را حدس بزند.
'forms' => $this->formsFor($session),
'case' => $session->getTreatmentCase()->toArray(),
// دستگاه‌های قابل انتخاب — اپراتور باید بتواند دستگاهِ یک ناحیه را عوض کند،
// مثل بیکینی با الکساندرایت و زیر بغل با دایود در همان جلسه.
'devices' => array_map(
static fn (ClinicResource $r): array => [
'uuid' => $r->getUuid(),
'name' => $r->getName(),
'type' => $r->getType()->getCode(),
],
$devices,
),
// فرمِ هر دستگاه از نوعش می‌آید؛ پنل نباید فیلدها را حدس بزند.
'forms' => $this->formsFor($devices),
]);
}
@@ -137,17 +150,16 @@ class SessionExecutionController extends BaseController
return $this->success($this->executor->skipArea($this->requireAreaRecord($user, $uuid))->toArray());
}
/** @return array<string, list<array<string, mixed>>> uuid منبع => تعریف فیلدها */
private function formsFor(TreatmentSession $session): array
/**
* @param list<ClinicResource> $devices
* @return array<string, list<array<string, mixed>>> uuid منبع => تعریف فیلدها
*/
private function formsFor(array $devices): array
{
$forms = [];
foreach ($session->getAreaRecords() as $record) {
$resource = $record->getResource();
if ($resource !== null) {
$forms[$resource->getUuid()] = $resource->getType()->getFieldSchema() ?? [];
}
foreach ($devices as $device) {
$forms[$device->getUuid()] = $device->getType()->getFieldSchema() ?? [];
}
return $forms;
@@ -9,6 +9,7 @@ use App\Doctor\Service\AddressResolver;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
use App\Resource\Repository\ResourceServiceOfferingRepository;
use App\Treatment\Repository\TreatmentProtocolRepository;
use App\Treatment\Service\TreatmentProtocolWriter;
use Doctrine\ORM\EntityManagerInterface;
@@ -27,6 +28,7 @@ class TreatmentProtocolController extends BaseController
private readonly TreatmentProtocolRepository $protocols,
private readonly ServiceItemRepository $items,
private readonly TreatmentProtocolWriter $writer,
private readonly ResourceServiceOfferingRepository $offerings,
private readonly AddressResolver $branches,
private readonly EntityManagerInterface $em,
) {}
@@ -35,9 +37,21 @@ class TreatmentProtocolController extends BaseController
#[Route('/api/v1/service-item/{uuid}/treatment-protocol', name: 'treatment_protocol_show', methods: ['GET'])]
public function show(#[CurrentUser] User $user, string $uuid): JsonResponse
{
$protocol = $this->protocols->findForService($this->requireItem($user, $uuid));
$service = $this->requireItem($user, $uuid);
$protocol = $this->protocols->findForService($service);
return $this->success($protocol?->toArray());
if ($protocol === null) {
return $this->success(null);
}
/**
* سرویسی که هیچ منبعی ارائه‌اش نمی‌دهد قفل نمی‌شود — قاعدهٔ «بدون offering
* یعنی همه مجاز» عمدی و مستند است. ولی مدیر باید ببیند، وگرنه تازه وقتی
* اپراتور جلوی بیمار می‌رسد معلوم می‌شود دستگاهی وصل نشده.
*/
return $this->success($protocol->toArray() + [
'service_has_resources' => $this->offerings->hasAnyFor($service),
]);
}
#[Route('/api/v1/service-item/{uuid}/treatment-protocol', name: 'treatment_protocol_replace', methods: ['PUT'])]