From 666833fc699b4544652ea7e59745db46c961cf20 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 17 Jul 2026 14:44:23 +0330 Subject: [PATCH] feat(patient): soft-archive sessions with active/all/archived filter Add archived + archived_at columns to PatientSession (setArchived stamps the time). findByRecord/countByRecord accept an archived filter (default all to keep existing callers, incl. the discount visit-count, unchanged); the sessions GET reads ?filter=active|all|archived, defaulting to active so archived visits are hidden. updateSession accepts { archived }. Records are kept, only hidden. Verified end-to-end; docs updated. Co-Authored-By: Claude Fable 5 --- docs/api/patient.md | 3 +- migrations/Version20260717111223.php | 29 ++++++++++++++++++ src/Patient/Controller/PatientController.php | 9 ++++-- src/Patient/Entity/PatientSession.php | 12 ++++++++ .../Repository/PatientSessionRepository.php | 30 ++++++++++++------- 5 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 migrations/Version20260717111223.php diff --git a/docs/api/patient.md b/docs/api/patient.md index 50e50d7e..522b86a9 100644 --- a/docs/api/patient.md +++ b/docs/api/patient.md @@ -280,7 +280,7 @@ GET /api/v1/patient/{uuid}/sessions Returns paginated sessions for a patient record. -**Query params:** `page`, `limit` (same as list) +**Query params:** `page`, `limit`, و `filter` = `active` (پیش‌فرض — آرشیوها مخفی) | `all` | `archived`. مقادیر نامعتبر به `active` برمی‌گردند. هر session کلیدهای `archived` (bool) و `archived_at` (unix|null) را هم دارد. **Response 200:** @@ -488,6 +488,7 @@ Updates mutable fields on a session. - **تخفیف تسویه (دستی):** `discount_type` = `percent` (۰..۱۰۰) یا `fixed` (ریال، حداکثر برابر مبلغ نهایی) یا `null` (حذف تخفیف). مبلغ محاسبه‌شده در `discount_rials` برمی‌گردد. تخفیف نمی‌تواند از «مبلغ نهایی منهای پرداخت‌های ثبت‌شده» بیشتر شود. تخفیفی که مانده را صفر کند مراجعه را تسویه‌شده می‌کند (`is_paid`, `paid_at`). - **تخفیف بر اساس قانون:** `discount_rule_uuid` (رشته) → قانون تخفیف (owner-scoped) اعمال می‌شود؛ مقدار ریالی از خود قانون توسط موتور محاسبه می‌گردد (نوع/مبنا بر اساس قانون). `''`/`null` → حذف تخفیف. اولویت بر `discount_type` دستی. قانونِ نامعتبر → `404`. منبع اعمال‌شده در پاسخ به‌صورت `applied_discount_rule_id` و `applied_discount_rule_label` (audit) برمی‌گردد. قوانین قابل‌اعمال از `GET /api/v1/session/{uuid}/discount-suggestions` (نگاه کنید به `discount.md`). - `paid_at`: unix timestamp زمان تسویه. +- **آرشیو:** `archived` (bool) → آرشیو نرم مراجعه؛ `true` آن را از لیست پیش‌فرض (`filter=active`) مخفی می‌کند و `archived_at` را ست می‌کند، `false` بازمی‌گرداند. سابقه (فاکتور/پرداخت‌ها) حذف نمی‌شود. **Response 200:** diff --git a/migrations/Version20260717111223.php b/migrations/Version20260717111223.php new file mode 100644 index 00000000..2475859c --- /dev/null +++ b/migrations/Version20260717111223.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE patient_sessions ADD archived TINYINT DEFAULT 0 NOT NULL, ADD archived_at INT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE patient_sessions DROP archived, DROP archived_at'); + } +} diff --git a/src/Patient/Controller/PatientController.php b/src/Patient/Controller/PatientController.php index b0a8d70f..151a590f 100644 --- a/src/Patient/Controller/PatientController.php +++ b/src/Patient/Controller/PatientController.php @@ -922,8 +922,10 @@ class PatientController extends BaseController $page = max(1, (int) $request->query->get('page', 1)); $limit = min(50, max(10, (int) $request->query->get('limit', 20))); - $sessions = $this->sessionRepo->findByRecord($record, $page, $limit); - $total = $this->sessionRepo->countByRecord($record); + // فیلتر آرشیو: active (پیش‌فرض — آرشیوها مخفی) | all | archived + $filter = in_array($request->query->get('filter'), ['all', 'archived'], true) ? $request->query->get('filter') : 'active'; + $sessions = $this->sessionRepo->findByRecord($record, $page, $limit, $filter); + $total = $this->sessionRepo->countByRecord($record, $filter); return $this->paginated( array_map(fn($s) => $this->sessionWithBilling($s), $sessions), @@ -1047,6 +1049,9 @@ class PatientController extends BaseController if (isset($data['notes'])) { $session->setNotes($data['notes']); } + // آرشیو نرم: مخفی‌سازی مراجعه‌ی اشتباه بدون حذف سابقه. + if (array_key_exists('archived', $data)) { $session->setArchived((bool) $data['archived']); } + // تخفیف بر اساس قانون (discount_rule_uuid): مقدار از خود قانون، با ثبت منبع. // '' یا null → حذف تخفیف. اولویت بر تخفیف دستی. if (array_key_exists('discount_rule_uuid', $data)) { diff --git a/src/Patient/Entity/PatientSession.php b/src/Patient/Entity/PatientSession.php index e2db5923..e5a64b80 100644 --- a/src/Patient/Entity/PatientSession.php +++ b/src/Patient/Entity/PatientSession.php @@ -91,6 +91,13 @@ class PatientSession #[ORM\Column(type: 'text', nullable: true)] private ?string $notes = null; + /** آرشیو نرم: مراجعه‌ی اشتباه از لیست پیش‌فرض مخفی می‌شود ولی سابقه حفظ می‌گردد. */ + #[ORM\Column(type: 'boolean', options: ['default' => false])] + private bool $archived = false; + + #[ORM\Column(name: 'archived_at', type: 'integer', nullable: true)] + private ?int $archivedAt = null; + #[ORM\Column(name: 'created_at', type: 'integer')] private int $createdAt; @@ -217,6 +224,9 @@ class PatientSession public function setInventoryPackage(?InventoryPackage $v): self { $this->inventoryPackage = $v; $this->updatedAt = time(); return $this; } public function setPaidAt(?int $v): self { $this->paidAt = $v; $this->updatedAt = time(); return $this; } public function setNotes(?string $v): self { $this->notes = $v; $this->updatedAt = time(); return $this; } + public function isArchived(): bool { return $this->archived; } + public function setArchived(bool $v): self { $this->archived = $v; $this->archivedAt = $v ? time() : null; $this->updatedAt = time(); return $this; } + public function getArchivedAt(): ?int { return $this->archivedAt; } public function toArray(): array { @@ -259,6 +269,8 @@ class PatientSession ), 'consumables_total_rials' => $this->getConsumablesTotalRials(), 'notes' => $this->notes, + 'archived' => $this->archived, + 'archived_at' => $this->archivedAt, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, ]; diff --git a/src/Patient/Repository/PatientSessionRepository.php b/src/Patient/Repository/PatientSessionRepository.php index f7bcb18c..f7c8c315 100644 --- a/src/Patient/Repository/PatientSessionRepository.php +++ b/src/Patient/Repository/PatientSessionRepository.php @@ -19,26 +19,36 @@ class PatientSessionRepository extends ServiceEntityRepository return $this->findOneBy(['uuid' => $uuid]); } - public function findByRecord(PatientRecord $record, int $page = 1, int $limit = 20): array + /** $filter: all | active | archived. پیش‌فرض all برای حفظ رفتار callerهای موجود. */ + public function findByRecord(PatientRecord $record, int $page = 1, int $limit = 20, string $filter = 'all'): array { - return $this->createQueryBuilder('s') + $qb = $this->createQueryBuilder('s') ->where('s.record = :record') ->setParameter('record', $record) ->orderBy('s.id', 'DESC') ->setFirstResult(($page - 1) * $limit) - ->setMaxResults($limit) - ->getQuery() - ->getResult(); + ->setMaxResults($limit); + $this->applyArchivedFilter($qb, $filter); + return $qb->getQuery()->getResult(); } - public function countByRecord(PatientRecord $record): int + public function countByRecord(PatientRecord $record, string $filter = 'all'): int { - return (int) $this->createQueryBuilder('s') + $qb = $this->createQueryBuilder('s') ->select('COUNT(s.id)') ->where('s.record = :record') - ->setParameter('record', $record) - ->getQuery() - ->getSingleScalarResult(); + ->setParameter('record', $record); + $this->applyArchivedFilter($qb, $filter); + return (int) $qb->getQuery()->getSingleScalarResult(); + } + + private function applyArchivedFilter(\Doctrine\ORM\QueryBuilder $qb, string $filter): void + { + if ($filter === 'active') { + $qb->andWhere('s.archived = false'); + } elseif ($filter === 'archived') { + $qb->andWhere('s.archived = true'); + } } public function sumRevenue(string $entityType, int $entityId, int $from, int $to): int