From 0e6111f1be3c9a14970d4322f372b93457d3ebd6 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Tue, 23 Jun 2026 22:13:51 +0330 Subject: [PATCH] feat(patients): service quantity in visit session Co-Authored-By: Claude Opus 4.8 --- assets/admin/pages/NewSessionPage.tsx | 28 ++++++++++++++--------- docs/api/patient.md | 7 +++--- migrations/Version20260623173907.php | 31 ++++++++++++++++++++++++++ src/Patient/Entity/SessionService.php | 10 ++++++++- src/Patient/Service/PatientService.php | 8 ++++--- 5 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 migrations/Version20260623173907.php diff --git a/assets/admin/pages/NewSessionPage.tsx b/assets/admin/pages/NewSessionPage.tsx index 92f5c219..290831d5 100644 --- a/assets/admin/pages/NewSessionPage.tsx +++ b/assets/admin/pages/NewSessionPage.tsx @@ -44,7 +44,7 @@ export default function NewSessionPage() { const nav = useNavigate(); const qc = useQueryClient(); - const [selectedServices, setSelectedServices] = useState<{ uuid: string; name: string; price: number }[]>([]); + const [selectedServices, setSelectedServices] = useState<{ uuid: string; name: string; price: number; qty: number }[]>([]); const [sectionUuid, setSectionUuid] = useState(''); const [itemUuid, setItemUuid] = useState(''); const [baseId, setBaseId] = useState(''); @@ -107,14 +107,17 @@ export default function NewSessionPage() { if (!itemUuid) return; const found = itemsData?.data?.find(i => i.uuid === itemUuid); if (!found || selectedServices.some(s => s.uuid === found.uuid)) return; - setSelectedServices(p => [...p, { uuid: found.uuid, name: found.name, price: found.price_rials }]); + setSelectedServices(p => [...p, { uuid: found.uuid, name: found.name, price: found.price_rials, qty: 1 }]); setItemUuid(''); }; + const setQty = (uuid: string, qty: number) => + setSelectedServices(p => p.map(s => s.uuid === uuid ? { ...s, qty: Math.max(1, qty) } : s)); + const visit = Number(form.watch('visit_price_rials')) || 0; const base = Number(form.watch('base_insurance_discount_percent')) || 0; const supp = Number(form.watch('supplementary_discount_percent')) || 0; - const servicesTotal = useMemo(() => selectedServices.reduce((s, x) => s + x.price, 0), [selectedServices]); + const servicesTotal = useMemo(() => selectedServices.reduce((s, x) => s + x.price * x.qty, 0), [selectedServices]); const afterBase = Math.round(visit * (1 - base / 100)); const afterSupp = Math.round(afterBase * (1 - supp / 100)); const finalPrice = calcFinal(visit, base, supp, servicesTotal); @@ -130,7 +133,7 @@ export default function NewSessionPage() { }); const submit = form.handleSubmit((d) => { - createMut.mutate({ ...d, services: selectedServices.map(s => ({ service_item_uuid: s.uuid })) }); + createMut.mutate({ ...d, services: selectedServices.map(s => ({ service_item_uuid: s.uuid, quantity: s.qty })) }); }); const summaryRow = (label: string, val: number, strong = false) => ( @@ -196,16 +199,21 @@ export default function NewSessionPage() { {selectedServices.length > 0 && ( -
+
{selectedServices.map(svc => ( - - {svc.name} - {formatRial(svc.price)} +
+ {svc.name} +
+ + {svc.qty} + +
+ {formatRial(svc.price * svc.qty)} - +
))}
)} diff --git a/docs/api/patient.md b/docs/api/patient.md index 07e5de50..b89e6260 100644 --- a/docs/api/patient.md +++ b/docs/api/patient.md @@ -235,7 +235,8 @@ Creates a new visit session for a patient record. "services": [ { "service_item_uuid": "...", - "staff_uuid": null + "staff_uuid": null, + "quantity": 2 } ] } @@ -244,8 +245,8 @@ Creates a new visit session for a patient record. **Field notes:** - `payment_method`: `cash` | `card` | `insurance` | `online` | `pending` -- `services`: array of service items to attach; `price_rials` is snapshot-copied from ServiceItem -- `final_price_rials` is computed: `(visit_price × (1 - base%) × (1 - supp%)) + services_total` +- `services`: array of service items to attach; `price_rials` snapshot از ServiceItem؛ `quantity` (پیش‌فرض ۱) → `line_total_rials = price_rials × quantity`. هر `SessionService` در پاسخ `quantity` و `line_total_rials` دارد. +- `final_price_rials` is computed: `(visit_price × (1 - base%) × (1 - supp%)) + services_total` که `services_total = Σ(price × quantity)` **Response 201:** diff --git a/migrations/Version20260623173907.php b/migrations/Version20260623173907.php new file mode 100644 index 00000000..83f7dabe --- /dev/null +++ b/migrations/Version20260623173907.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE session_services ADD quantity INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE session_services DROP quantity'); + } +} diff --git a/src/Patient/Entity/SessionService.php b/src/Patient/Entity/SessionService.php index 8d0ec45f..3f75eace 100644 --- a/src/Patient/Entity/SessionService.php +++ b/src/Patient/Entity/SessionService.php @@ -35,16 +35,20 @@ class SessionService #[ORM\Column(name: 'price_rials', type: 'integer')] private int $priceRials; + #[ORM\Column(type: 'integer')] + private int $quantity = 1; + #[ORM\Column(name: 'created_at', type: 'integer')] private int $createdAt; - public function __construct(PatientSession $session, ServiceItem $serviceItem, ?ClinicStaff $staff = null) + public function __construct(PatientSession $session, ServiceItem $serviceItem, ?ClinicStaff $staff = null, int $quantity = 1) { $this->uuid = Uuid::v4()->toRfc4122(); $this->session = $session; $this->serviceItem = $serviceItem; $this->staff = $staff; $this->priceRials = $serviceItem->getPriceRials(); + $this->quantity = max(1, $quantity); $this->createdAt = time(); } @@ -54,6 +58,8 @@ class SessionService public function getServiceItem(): ServiceItem { return $this->serviceItem; } public function getStaff(): ?ClinicStaff { return $this->staff; } public function getPriceRials(): int { return $this->priceRials; } + public function getQuantity(): int { return $this->quantity; } + public function getLineTotalRials(): int { return $this->priceRials * $this->quantity; } public function getCreatedAt(): int { return $this->createdAt; } public function toArray(): array @@ -65,6 +71,8 @@ class SessionService 'staff_uuid' => $this->staff?->getUuid(), 'staff_name' => $this->staff?->getFullName(), 'price_rials' => $this->priceRials, + 'quantity' => $this->quantity, + 'line_total_rials' => $this->getLineTotalRials(), 'created_at' => $this->createdAt, ]; } diff --git a/src/Patient/Service/PatientService.php b/src/Patient/Service/PatientService.php index f72808d9..c6e5269f 100644 --- a/src/Patient/Service/PatientService.php +++ b/src/Patient/Service/PatientService.php @@ -105,12 +105,13 @@ class PatientService $session->setPaymentMethod($data['payment_method'] ?? 'pending'); $session->setNotes($data['notes'] ?? null); - // جمع‌آوری service items + // جمع‌آوری service items (با احتساب تعداد) $serviceItemsData = []; foreach (($data['services'] ?? []) as $svc) { $item = $this->serviceItemRepo->findByUuid($svc['service_item_uuid'] ?? ''); if ($item !== null) { - $serviceItemsData[] = ['price_rials' => $item->getPriceRials()]; + $qty = max(1, (int) ($svc['quantity'] ?? 1)); + $serviceItemsData[] = ['price_rials' => $item->getPriceRials() * $qty]; } } @@ -133,7 +134,8 @@ class PatientService continue; } $staff = !empty($svc['staff_uuid']) ? $this->staffRepo->findByUuid($svc['staff_uuid']) : null; - $ss = new SessionService($session, $item, $staff); + $qty = max(1, (int) ($svc['quantity'] ?? 1)); + $ss = new SessionService($session, $item, $staff, $qty); $this->sessionServiceRepo->save($ss); }