feat(treatment): open a treatment case with snapshotted areas and its sessions

Opening a case copies what must not move afterwards — the session count and the
list of body areas, each with its category name — because a treatment record is
a medical document and editing settings tomorrow must not rewrite what was done
yesterday. The areas are the leaf categories under the service's own category:
"توتال" contains bikini, leg and hand, and treatment happens on those three, not
on the grouping node above them. A category with no children is its own single
area, so "لیزر دست" gets one area rather than none.

Every session in the course is created up front so that "session 5 of 8" has
somewhere to live, but none of them is booked: creating eight real appointments
would lock eight months of slots for a patient who may not attend session three.

CategoryClosureResolver gains leaves(); the graph walk it already does is what
tells a leaf from a grouping node, so this belongs next to descendants() rather
than in a second traversal elsewhere.

TreatmentCase and TreatmentSession carry no money field, and must not: billing
lives on PatientSession, which is created when an appointment is confirmed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-06 16:53:50 +03:30
co-authored by Claude Opus 5
parent e2e3e6b43b
commit 6847a473d4
13 changed files with 1278 additions and 0 deletions
@@ -0,0 +1,57 @@
<?php
namespace App\Treatment\Repository;
use App\ClinicService\Entity\ServiceItem;
use App\Patient\Entity\PatientRecord;
use App\Treatment\Entity\TreatmentCase;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TreatmentCase>
*/
class TreatmentCaseRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TreatmentCase::class);
}
public function findByUuid(string $uuid): ?TreatmentCase
{
return $this->findOneBy(['uuid' => $uuid]);
}
/**
* پروندهٔ بازِ همین بیمار برای همین سرویس.
*
* نقطهٔ تصمیمِ «پروندهٔ دوم نساز»: بیماری که وسط دورهٔ لیزرش نوبت دیگری از همان
* سرویس می‌گیرد، باید جلسهٔ همان دوره را بگیرد، نه یک دورهٔ موازی.
*/
public function findOpenFor(PatientRecord $record, ServiceItem $service): ?TreatmentCase
{
return $this->findOneBy([
'patientRecord' => $record,
'serviceItem' => $service,
'status' => TreatmentCase::STATUS_ACTIVE,
]);
}
/** @return TreatmentCase[] */
public function findForTenant(string $entityType, int $entityId, ?string $status = null): array
{
$qb = $this->createQueryBuilder('c')
->where('c.entityType = :type')
->andWhere('c.entityId = :id')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->orderBy('c.openedAt', 'DESC');
if ($status !== null) {
$qb->andWhere('c.status = :status')->setParameter('status', $status);
}
return $qb->getQuery()->getResult();
}
}