diff --git a/assets/admin/components/DiscountTab.tsx b/assets/admin/components/DiscountTab.tsx new file mode 100644 index 00000000..931e0382 --- /dev/null +++ b/assets/admin/components/DiscountTab.tsx @@ -0,0 +1,340 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; +import { toast } from 'sonner'; +import { api } from '../lib/api'; +import type { ApiResponse } from '../lib/api'; +import type { DiscountRule, DiscountRuleType } from '../types'; +import { formatRial, formatDate, tomanToRial, rialToToman, tehranWallClockToUnix } from '../lib/utils'; +import Modal from './ui/Modal'; +import ConfirmDialog from './ui/ConfirmDialog'; +import SearchableSelect from './ui/SearchableSelect'; +import PriceInput from './ui/PriceInput'; +import PersianDateInput from './ui/PersianDateInput'; + +const TYPE_LABELS: Record = { + patient_tag: 'تگ بیمار', + invoice_amount: 'مبلغ فاکتور', + specific_patient: 'بیمار خاص', + occasion: 'مناسبتی', + service: 'سرویس', + visit_count: 'تعداد مراجعات', +}; + +interface Option { uuid: string; name?: string } + +const unixToIso = (u: number | null): string => { + if (!u) return ''; + const d = new Date(u * 1000); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +}; +const isoToUnix = (iso: string): number | null => (iso ? tehranWallClockToUnix(iso, '00:00') : null); + +interface FormState { + name: string; + type: DiscountRuleType; + discount_type: 'percent' | 'fixed'; + value: number; // percent (0..100) or toman (fixed) + priority: number; + combinable: boolean; + active: boolean; + valid_from: string; // iso + valid_to: string; // iso + target_tag_uuid: string; + target_record_uuid: string; + target_service_item_uuid: string; + min_amount_toman: number; + min_visit_count: number; + occasion_kind: '' | 'birthday'; +} + +const emptyForm = (): FormState => ({ + name: '', type: 'invoice_amount', discount_type: 'percent', value: 0, priority: 0, + combinable: false, active: true, valid_from: '', valid_to: '', + target_tag_uuid: '', target_record_uuid: '', target_service_item_uuid: '', + min_amount_toman: 0, min_visit_count: 0, occasion_kind: '', +}); + +const fromRule = (r: DiscountRule): FormState => ({ + name: r.name, type: r.type, discount_type: r.discount_type, + value: r.discount_type === 'fixed' ? rialToToman(r.value) : r.value, + priority: r.priority, combinable: r.combinable, active: r.active, + valid_from: unixToIso(r.valid_from), valid_to: unixToIso(r.valid_to), + target_tag_uuid: r.target_tag_uuid ?? '', target_record_uuid: r.target_record_uuid ?? '', + target_service_item_uuid: r.target_service_item_uuid ?? '', + min_amount_toman: r.min_amount_rials ? rialToToman(r.min_amount_rials) : 0, + min_visit_count: r.min_visit_count ?? 0, + occasion_kind: r.occasion_kind === 'birthday' ? 'birthday' : '', +}); + +function labelStyle(): React.CSSProperties { return { fontSize: 12.5, color: 'var(--text-3)', display: 'block', marginBottom: 6 }; } + +export default function DiscountTab() { + const qc = useQueryClient(); + const [modal, setModal] = useState<'create' | DiscountRule | null>(null); + const [toDelete, setToDelete] = useState(null); + + const { data, isLoading } = useQuery({ + queryKey: ['admin-discount-rules'], + queryFn: () => api.get>('/api/v1/admin/discount-rules'), + }); + const rules: DiscountRule[] = (data?.data as any)?.data ?? (data?.data as any) ?? []; + + const removeMut = useMutation({ + mutationFn: (uuid: string) => api.delete(`/api/v1/admin/discount-rules/${uuid}`), + onSuccess: () => { toast.success('قانون حذف شد'); setToDelete(null); qc.invalidateQueries({ queryKey: ['admin-discount-rules'] }); }, + onError: (e: Error) => toast.error(e.message), + }); + + const discountDisplay = (r: DiscountRule) => + r.discount_type === 'percent' ? `${r.value}٪` : formatRial(r.value); + + return ( +
+
+ قوانین تخفیف عمومی — بر اساس تگ، مبلغ، بیمار، مناسبت، سرویس یا تعداد مراجعه + +
+ + {isLoading ? ( +
در حال بارگذاری...
+ ) : rules.length === 0 ? ( +
هنوز قانونی تعریف نشده است.
+ ) : ( +
+ + + + + + + + + + + + + + {rules.map((r) => ( + + + + + + + + + + ))} + +
نامنوعتخفیفاولویتترکیب‌پذیروضعیت
{r.name}{TYPE_LABELS[r.type]}{discountDisplay(r)}{r.priority}{r.combinable ? 'بله' : 'خیر'} + {r.active ? 'فعال' : 'غیرفعال'} + + + +
+
+ )} + + {modal && ( + setModal(null)} + onSaved={() => { setModal(null); qc.invalidateQueries({ queryKey: ['admin-discount-rules'] }); }} + /> + )} + + toDelete && removeMut.mutate(toDelete.uuid)} + onCancel={() => setToDelete(null)} + /> +
+ ); +} + +function RuleModal({ initial, onClose, onSaved }: { initial: DiscountRule | null; onClose: () => void; onSaved: () => void }) { + const [f, setF] = useState(initial ? fromRule(initial) : emptyForm()); + const set = (k: K, v: FormState[K]) => setF((s) => ({ ...s, [k]: v })); + + const tagsQ = useQuery({ + queryKey: ['tenant-tags'], queryFn: () => api.get>('/api/v1/tenant-tags'), + enabled: f.type === 'patient_tag', + }); + const sectionsQ = useQuery({ + queryKey: ['service-sections'], queryFn: () => api.get>('/api/v1/service-sections'), + enabled: f.type === 'service', + }); + const [sectionUuid, setSectionUuid] = useState(''); + const itemsQ = useQuery({ + queryKey: ['service-items', sectionUuid], queryFn: () => api.get>(`/api/v1/service-items/${sectionUuid}`), + enabled: f.type === 'service' && !!sectionUuid, + }); + const tags = (tagsQ.data?.data as any)?.data ?? (tagsQ.data?.data as any) ?? []; + const sections = (sectionsQ.data?.data as any)?.data ?? (sectionsQ.data?.data as any) ?? []; + const items = (itemsQ.data?.data as any)?.data ?? (itemsQ.data?.data as any) ?? []; + + const save = useMutation({ + mutationFn: () => { + const body: Record = { + name: f.name.trim(), + type: f.type, + discount_type: f.discount_type, + value: f.discount_type === 'fixed' ? tomanToRial(f.value) : f.value, + priority: f.priority, + combinable: f.combinable, + active: f.active, + valid_from: isoToUnix(f.valid_from), + valid_to: isoToUnix(f.valid_to), + target_tag_uuid: f.type === 'patient_tag' ? (f.target_tag_uuid || null) : null, + target_record_uuid: f.type === 'specific_patient' ? (f.target_record_uuid.trim() || null) : null, + target_service_item_uuid: f.type === 'service' ? (f.target_service_item_uuid || null) : null, + min_amount_rials: f.type === 'invoice_amount' ? tomanToRial(f.min_amount_toman) : null, + min_visit_count: f.type === 'visit_count' ? f.min_visit_count : null, + occasion_kind: f.type === 'occasion' ? (f.occasion_kind || null) : null, + }; + return initial + ? api.patch(`/api/v1/admin/discount-rules/${initial.uuid}`, body) + : api.post('/api/v1/admin/discount-rules', body); + }, + onSuccess: () => { toast.success('قانون ذخیره شد'); onSaved(); }, + onError: (e: Error) => toast.error(e.message), + }); + + const canSave = f.name.trim().length > 0 && f.value >= 0; + + return ( + +
+
+ + set('name', e.target.value)} placeholder="مثال: بیماران VIP" /> +
+ +
+
+ + ({ value: t, label: TYPE_LABELS[t] }))} + value={f.type} onChange={(v) => set('type', (v as DiscountRuleType) || 'invoice_amount')} height={38} + /> +
+
+ + set('discount_type', (v as 'percent' | 'fixed') || 'percent')} height={38} + /> +
+
+ +
+
+ + {f.discount_type === 'percent' + ? set('value', Number(e.target.value) || 0)} /> + : set('value', v)} />} +
+
+ + set('priority', Number(e.target.value) || 0)} /> +
+
+ + {/* target فیلد پویا بر اساس نوع */} + {f.type === 'patient_tag' && ( +
+ + ({ value: t.uuid, label: t.name ?? '' }))} + value={f.target_tag_uuid || null} onChange={(v) => set('target_tag_uuid', v ? String(v) : '')} + placeholder="انتخاب تگ" isLoading={tagsQ.isLoading} isClearable height={38} + /> +
+ )} + {f.type === 'invoice_amount' && ( +
+ + set('min_amount_toman', v)} /> +
+ )} + {f.type === 'specific_patient' && ( +
+ + set('target_record_uuid', e.target.value)} placeholder="record uuid" /> +
+ )} + {f.type === 'service' && ( +
+
+ + ({ value: s.uuid, label: s.name ?? '' }))} + value={sectionUuid || null} onChange={(v) => { setSectionUuid(v ? String(v) : ''); set('target_service_item_uuid', ''); }} + placeholder="انتخاب بخش" isLoading={sectionsQ.isLoading} isClearable height={38} + /> +
+
+ + ({ value: s.uuid, label: s.name ?? '' }))} + value={f.target_service_item_uuid || null} onChange={(v) => set('target_service_item_uuid', v ? String(v) : '')} + placeholder="انتخاب سرویس" isDisabled={!sectionUuid} isLoading={itemsQ.isLoading} isClearable height={38} + /> +
+
+ )} + {f.type === 'visit_count' && ( +
+ + set('min_visit_count', Number(e.target.value) || 0)} /> +
+ )} + {f.type === 'occasion' && ( +
+ + set('occasion_kind', (v as '' | 'birthday'))} height={38} + /> +
+ )} + + {/* بازه‌ی اعتبار (اختیاری؛ برای مناسبتی/موقت) */} +
+
+ + set('valid_from', v)} /> +
+
+ + set('valid_to', v)} /> +
+
+ +
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/assets/admin/pages/AdminSubscriptionPage.tsx b/assets/admin/pages/AdminSubscriptionPage.tsx index 6e3a8f37..dcd4d1cb 100644 --- a/assets/admin/pages/AdminSubscriptionPage.tsx +++ b/assets/admin/pages/AdminSubscriptionPage.tsx @@ -13,6 +13,7 @@ import Modal from '../components/ui/Modal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import PageHeader from '../components/ui/PageHeader'; import Pagination from '../components/ui/Pagination'; +import DiscountTab from '../components/DiscountTab'; // ── Types ───────────────────────────────────────────────────────────────── @@ -435,19 +436,21 @@ function ReportTab() { // ── Main ────────────────────────────────────────────────────────────────── export default function AdminSubscriptionPage() { - const [tab, setTab] = useState<'plans' | 'report'>('plans'); + const [tab, setTab] = useState<'plans' | 'report' | 'discounts'>('plans'); return ( <> - +
- - + + +
- {tab === 'plans' && } - {tab === 'report' && } + {tab === 'plans' && } + {tab === 'discounts' && } + {tab === 'report' && } ); } diff --git a/assets/admin/types/index.ts b/assets/admin/types/index.ts index 2a0a2b41..f4f6b01f 100644 --- a/assets/admin/types/index.ts +++ b/assets/admin/types/index.ts @@ -124,6 +124,46 @@ export interface AppointmentEvent { created_at: number; } +export type DiscountRuleType = + | 'patient_tag' + | 'invoice_amount' + | 'specific_patient' + | 'occasion' + | 'service' + | 'visit_count'; + +export interface DiscountRule { + uuid: string; + name: string; + type: DiscountRuleType; + discount_type: 'percent' | 'fixed'; + value: number; + priority: number; + combinable: boolean; + active: boolean; + valid_from: number | null; + valid_to: number | null; + target_tag_uuid: string | null; + target_record_uuid: string | null; + target_service_item_uuid: string | null; + min_amount_rials: number | null; + min_visit_count: number | null; + occasion_kind: string | null; + created_at: number; + updated_at: number; +} + +export interface DiscountSuggestion { + rule_uuid: string; + rule_name: string; + type: DiscountRuleType; + discount_type: 'percent' | 'fixed'; + value: number; + discount_rials: number; + combinable: boolean; + priority: number; +} + export type PaymentStatus = | "pending" | "success" diff --git a/docs/api/discount.md b/docs/api/discount.md index 45a1cac7..1d238b22 100644 --- a/docs/api/discount.md +++ b/docs/api/discount.md @@ -7,11 +7,11 @@ Owner is resolved from the authenticated user (`ROLE_DOCTOR` → doctor, `ROLE_C | type | target field(s) | meaning | |------|-----------------|---------| -| `patient_tag` | `target_tag_id` (TenantTag id) | patient carries the tag | +| `patient_tag` | `target_tag_uuid` (TenantTag uuid) | patient carries the tag | | `invoice_amount` | `min_amount_rials` | session `final_price_rials` ≥ threshold | -| `specific_patient` | `target_record_id` (PatientRecord id) | a specific patient's record | +| `specific_patient` | `target_record_uuid` (PatientRecord uuid) | a specific patient's record | | `occasion` | `valid_from`/`valid_to`, optional `occasion_kind: birthday` | date window; `birthday` also requires today == patient birthday (month/day) | -| `service` | `target_service_item_id` (ServiceItem id) | session contains that service (discount base = that service's line total) | +| `service` | `target_service_item_uuid` (ServiceItem uuid) | session contains that service (discount base = that service's line total) | | `visit_count` | `min_visit_count` | patient's session count ≥ threshold | Shared fields: `discount_type` (`percent`|`fixed`), `value` (percent 0..100 or rials), `priority` (int, higher first), `combinable` (bool), `active` (bool), `valid_from`/`valid_to` (unix, nullable). @@ -40,7 +40,7 @@ Create a rule. **Auth:** doctor/clinic. | `combinable` | bool | ❌ | default false | | `active` | bool | ❌ | default true | | `valid_from` / `valid_to` | int (unix) | ❌ | validity window | -| `target_tag_id` / `target_record_id` / `target_service_item_id` | int | ❌ | per-type target | +| `target_tag_uuid` / `target_record_uuid` / `target_service_item_uuid` | string (uuid) | ❌ | per-type target | | `min_amount_rials` / `min_visit_count` | int | ❌ | per-type threshold | | `occasion_kind` | string | ❌ | `birthday` or null | diff --git a/migrations/Version20260717082805.php b/migrations/Version20260717082805.php new file mode 100644 index 00000000..004b9a1d --- /dev/null +++ b/migrations/Version20260717082805.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE discount_rules ADD target_tag_uuid VARCHAR(36) DEFAULT NULL, ADD target_record_uuid VARCHAR(36) DEFAULT NULL, ADD target_service_item_uuid VARCHAR(36) DEFAULT NULL, DROP target_tag_id, DROP target_record_id, DROP target_service_item_id'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE discount_rules ADD target_tag_id INT DEFAULT NULL, ADD target_record_id INT DEFAULT NULL, ADD target_service_item_id INT DEFAULT NULL, DROP target_tag_uuid, DROP target_record_uuid, DROP target_service_item_uuid'); + } +} diff --git a/src/Discount/Controller/DiscountController.php b/src/Discount/Controller/DiscountController.php index c3796101..3ae76bbd 100644 --- a/src/Discount/Controller/DiscountController.php +++ b/src/Discount/Controller/DiscountController.php @@ -161,9 +161,9 @@ class DiscountController extends BaseController if (array_key_exists('active', $data)) { $rule->setActive((bool) $data['active']); } if (array_key_exists('valid_from', $data)) { $rule->setValidFrom($data['valid_from'] !== null ? (int) $data['valid_from'] : null); } if (array_key_exists('valid_to', $data)) { $rule->setValidTo($data['valid_to'] !== null ? (int) $data['valid_to'] : null); } - if (array_key_exists('target_tag_id', $data)) { $rule->setTargetTagId($data['target_tag_id'] !== null ? (int) $data['target_tag_id'] : null); } - if (array_key_exists('target_record_id', $data)) { $rule->setTargetRecordId($data['target_record_id'] !== null ? (int) $data['target_record_id'] : null); } - if (array_key_exists('target_service_item_id', $data)) { $rule->setTargetServiceItemId($data['target_service_item_id'] !== null ? (int) $data['target_service_item_id'] : null); } + if (array_key_exists('target_tag_uuid', $data)) { $rule->setTargetTagUuid($data['target_tag_uuid'] !== null ? (string) $data['target_tag_uuid'] : null); } + if (array_key_exists('target_record_uuid', $data)) { $rule->setTargetRecordUuid($data['target_record_uuid'] !== null ? (string) $data['target_record_uuid'] : null); } + if (array_key_exists('target_service_item_uuid', $data)) { $rule->setTargetServiceItemUuid($data['target_service_item_uuid'] !== null ? (string) $data['target_service_item_uuid'] : null); } if (array_key_exists('min_amount_rials', $data)) { $rule->setMinAmountRials($data['min_amount_rials'] !== null ? (int) $data['min_amount_rials'] : null); } if (array_key_exists('min_visit_count', $data)) { $rule->setMinVisitCount($data['min_visit_count'] !== null ? (int) $data['min_visit_count'] : null); } if (array_key_exists('occasion_kind', $data)) { $rule->setOccasionKind($data['occasion_kind'] !== null ? (string) $data['occasion_kind'] : null); } diff --git a/src/Discount/Entity/DiscountRule.php b/src/Discount/Entity/DiscountRule.php index 11d63a88..509c10ad 100644 --- a/src/Discount/Entity/DiscountRule.php +++ b/src/Discount/Entity/DiscountRule.php @@ -79,15 +79,15 @@ class DiscountRule #[ORM\Column(name: 'valid_to', type: 'integer', nullable: true)] private ?int $validTo = null; - // ── target fields (بسته به type فقط یکی معنی‌دار است) ────────────────────── - #[ORM\Column(name: 'target_tag_id', type: 'integer', nullable: true)] - private ?int $targetTagId = null; + // ── target fields (بسته به type فقط یکی معنی‌دار است؛ uuid برای سازگاری با UI) ── + #[ORM\Column(name: 'target_tag_uuid', type: 'string', length: 36, nullable: true)] + private ?string $targetTagUuid = null; - #[ORM\Column(name: 'target_record_id', type: 'integer', nullable: true)] - private ?int $targetRecordId = null; + #[ORM\Column(name: 'target_record_uuid', type: 'string', length: 36, nullable: true)] + private ?string $targetRecordUuid = null; - #[ORM\Column(name: 'target_service_item_id', type: 'integer', nullable: true)] - private ?int $targetServiceItemId = null; + #[ORM\Column(name: 'target_service_item_uuid', type: 'string', length: 36, nullable: true)] + private ?string $targetServiceItemUuid = null; #[ORM\Column(name: 'min_amount_rials', type: 'integer', nullable: true)] private ?int $minAmountRials = null; @@ -128,9 +128,9 @@ class DiscountRule public function isActive(): bool { return $this->active; } public function getValidFrom(): ?int { return $this->validFrom; } public function getValidTo(): ?int { return $this->validTo; } - public function getTargetTagId(): ?int { return $this->targetTagId; } - public function getTargetRecordId(): ?int { return $this->targetRecordId; } - public function getTargetServiceItemId(): ?int { return $this->targetServiceItemId; } + public function getTargetTagUuid(): ?string { return $this->targetTagUuid; } + public function getTargetRecordUuid(): ?string { return $this->targetRecordUuid; } + public function getTargetServiceItemUuid(): ?string { return $this->targetServiceItemUuid; } public function getMinAmountRials(): ?int { return $this->minAmountRials; } public function getMinVisitCount(): ?int { return $this->minVisitCount; } public function getOccasionKind(): ?string { return $this->occasionKind; } @@ -144,9 +144,9 @@ class DiscountRule public function setActive(bool $v): self { $this->active = $v; $this->touch(); return $this; } public function setValidFrom(?int $v): self { $this->validFrom = $v; $this->touch(); return $this; } public function setValidTo(?int $v): self { $this->validTo = $v; $this->touch(); return $this; } - public function setTargetTagId(?int $v): self { $this->targetTagId = $v; $this->touch(); return $this; } - public function setTargetRecordId(?int $v): self { $this->targetRecordId = $v; $this->touch(); return $this; } - public function setTargetServiceItemId(?int $v): self { $this->targetServiceItemId = $v; $this->touch(); return $this; } + public function setTargetTagUuid(?string $v): self { $this->targetTagUuid = $v; $this->touch(); return $this; } + public function setTargetRecordUuid(?string $v): self { $this->targetRecordUuid = $v; $this->touch(); return $this; } + public function setTargetServiceItemUuid(?string $v): self { $this->targetServiceItemUuid = $v; $this->touch(); return $this; } public function setMinAmountRials(?int $v): self { $this->minAmountRials = $v; $this->touch(); return $this; } public function setMinVisitCount(?int $v): self { $this->minVisitCount = $v; $this->touch(); return $this; } public function setOccasionKind(?string $v): self { $this->occasionKind = $v; $this->touch(); return $this; } @@ -166,10 +166,10 @@ class DiscountRule 'active' => $this->active, 'valid_from' => $this->validFrom, 'valid_to' => $this->validTo, - 'target_tag_id' => $this->targetTagId, - 'target_record_id' => $this->targetRecordId, - 'target_service_item_id' => $this->targetServiceItemId, - 'min_amount_rials' => $this->minAmountRials, + 'target_tag_uuid' => $this->targetTagUuid, + 'target_record_uuid' => $this->targetRecordUuid, + 'target_service_item_uuid' => $this->targetServiceItemUuid, + 'min_amount_rials' => $this->minAmountRials, 'min_visit_count' => $this->minVisitCount, 'occasion_kind' => $this->occasionKind, 'created_at' => $this->createdAt, diff --git a/src/Discount/Service/DiscountEngine.php b/src/Discount/Service/DiscountEngine.php index 62e01968..5b8cd978 100644 --- a/src/Discount/Service/DiscountEngine.php +++ b/src/Discount/Service/DiscountEngine.php @@ -91,15 +91,15 @@ class DiscountEngine $record = $session->getRecord(); return match ($rule->getType()) { - DiscountRule::TYPE_PATIENT_TAG => $this->hasTag($session, $rule->getTargetTagId()) ? $final : null, + DiscountRule::TYPE_PATIENT_TAG => $this->hasTag($session, $rule->getTargetTagUuid()) ? $final : null, DiscountRule::TYPE_INVOICE_AMOUNT => ($rule->getMinAmountRials() !== null && $final >= $rule->getMinAmountRials()) ? $final : null, - DiscountRule::TYPE_SPECIFIC_PATIENT => ($rule->getTargetRecordId() !== null && $record->getId() === $rule->getTargetRecordId()) ? $final : null, + DiscountRule::TYPE_SPECIFIC_PATIENT => ($rule->getTargetRecordUuid() !== null && $record->getUuid() === $rule->getTargetRecordUuid()) ? $final : null, DiscountRule::TYPE_OCCASION => $this->occasionMatches($rule, $session, $now) ? $final : null, - DiscountRule::TYPE_SERVICE => $this->serviceBase($session, $rule->getTargetServiceItemId()), + DiscountRule::TYPE_SERVICE => $this->serviceBase($session, $rule->getTargetServiceItemUuid()), DiscountRule::TYPE_VISIT_COUNT => ($rule->getMinVisitCount() !== null && $this->sessionRepo->countByRecord($record) >= $rule->getMinVisitCount()) ? $final : null, @@ -108,13 +108,13 @@ class DiscountEngine }; } - private function hasTag(PatientSession $session, ?int $tagId): bool + private function hasTag(PatientSession $session, ?string $tagUuid): bool { - if ($tagId === null) { + if ($tagUuid === null) { return false; } foreach ($session->getRecord()->getTags() as $tag) { - if ($tag->getId() === $tagId) { + if ($tag->getUuid() === $tagUuid) { return true; } } @@ -122,15 +122,15 @@ class DiscountEngine } /** مبنای تخفیف سرویس = جمع خطوطِ همان سرویس؛ null اگر سرویس در پرونده نباشد. */ - private function serviceBase(PatientSession $session, ?int $serviceItemId): ?int + private function serviceBase(PatientSession $session, ?string $serviceItemUuid): ?int { - if ($serviceItemId === null) { + if ($serviceItemUuid === null) { return null; } $sum = 0; foreach ($session->getServices() as $line) { /** @var SessionService $line */ - if ($line->getServiceItem()->getId() === $serviceItemId) { + if ($line->getServiceItem()->getUuid() === $serviceItemUuid) { $sum += $line->getLineTotalRials(); } }