diff --git a/assets/admin/components/InvoiceSummaryModal.tsx b/assets/admin/components/InvoiceSummaryModal.tsx index 0a5b24ed..85253145 100644 --- a/assets/admin/components/InvoiceSummaryModal.tsx +++ b/assets/admin/components/InvoiceSummaryModal.tsx @@ -64,11 +64,13 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU const session = inv?.session ?? null; const paid = inv?.status === 'paid'; - // با session: مبالغ واقعی پرداخت؛ بدون آن (فاکتور قدیمی): heuristic قبلی. + // با session: مبالغ واقعی پرداخت (مبلغ نهایی منهای تخفیف و پرداخت‌ها)؛ بدون آن heuristic قبلی. const remaining = session - ? session.final_price_rials - session.paid_total_rials + ? Math.max(0, session.final_price_rials - (session.discount_rials ?? 0) - session.paid_total_rials) : inv ? (paid ? 0 : inv.patient_rials) : 0; const paidAmount = session ? session.paid_total_rials : inv ? inv.total_rials - remaining : 0; + // وضعیت واقعی پرداخت (مستقل از وضعیت فریزشده‌ی فاکتور): تسویه‌شده اگر مانده صفر. + const statusLabel = session ? (remaining <= 0 ? 'تسویه شده' : 'بدهکار') : (inv ? (STATUS_LABEL[inv.status] ?? inv.status) : ''); return ( @@ -82,7 +84,7 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU rows={[[ formatDate(session?.session_at ?? inv.issued_at), session?.paid_at ? formatDate(session.paid_at) : paid ? formatDate(inv.issued_at) : '—', - STATUS_LABEL[inv.status] ?? inv.status, + 0 ? '#d32f2f' : '#388e3c', fontWeight: 600 }}>{statusLabel}, ]]} /> [ - i + 1, - METHOD_LABELS[p.method] ?? p.method, - formatRial(p.amount_rials), - p.paid_at ? formatDateTime(p.paid_at) : '-', - p.created_by_name ?? '-', - ]) + ? [ + ...session.payments.map((p, i) => [ + i + 1, + METHOD_LABELS[p.method] ?? p.method, + formatRial(p.amount_rials), + p.paid_at ? formatDateTime(p.paid_at) : '-', + p.created_by_name ?? '-', + ]), + ['', مجموع پرداختی‌ها, {formatRial(session.paid_total_rials)}, '', ''], + ] : [['-', '-', '-', '-', '-']]} /> )} diff --git a/assets/admin/components/session/CreateStep.tsx b/assets/admin/components/session/CreateStep.tsx index b0a6fed2..6c8556eb 100644 --- a/assets/admin/components/session/CreateStep.tsx +++ b/assets/admin/components/session/CreateStep.tsx @@ -90,6 +90,11 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e const { data: sectionsData } = useQuery>({ queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'), }); + // همه‌ی سرویس‌ها (سراسری) تا بتوان بدون انتخاب بخش هم جستجو و انتخاب کرد. + const { data: allItemsData } = useQuery>({ + queryKey: ['service-items-all'], + queryFn: () => api.get('/api/v1/service-items'), + }); const { data: itemsData } = useQuery>({ queryKey: ['service-items-for-session', sectionUuid], queryFn: () => api.get(`/api/v1/service-items/${sectionUuid}`), @@ -144,7 +149,8 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e const suppOpts = contracts.filter(c => c.insurance_kind === 'supplementary').map(c => ({ value: String(c.insurance_id), label: c.insurance_name ?? `#${c.insurance_id}` })); const sectionOptions = (sectionsData?.data ?? []).map(s => ({ value: s.uuid, label: s.name })); - const serviceItems = (itemsData?.data ?? []).filter(i => i.active); + // با انتخاب بخش، فقط سرویس‌های همان بخش؛ بدون بخش، همه‌ی سرویس‌ها (قابل جستجو). + const serviceItems = (sectionUuid ? (itemsData?.data ?? []) : (allItemsData?.data ?? [])).filter(i => i.active); const itemOptions = serviceItems.map(i => ({ value: i.uuid, label: i.name })); const currentItem = serviceItems.find(i => i.uuid === itemUuid); @@ -325,7 +331,7 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
انتخاب سرویس - setItemUuid(v ? String(v) : '')} placeholder="انتخاب کنید..." isDisabled={!sectionUuid} /> + setItemUuid(v ? String(v) : '')} placeholder="جستجو و انتخاب سرویس..." />
diff --git a/docs/api/clinic-services.md b/docs/api/clinic-services.md index a6ef2077..bff99f14 100644 --- a/docs/api/clinic-services.md +++ b/docs/api/clinic-services.md @@ -76,6 +76,10 @@ --- +## GET /api/v1/service-items + +همه‌ی سرویس‌های owner در همه‌ی بخش‌ها (برای انتخاب/جستجوی سراسری در فرم ثبت/ویرایش مراجعه). پاسخ مثل لیست هر بخش (آرایه‌ی `ServiceItem::toArray`)، مرتب بر نام. + ## GET /api/v1/service-items/{sectionUuid} لیست سرویس‌های یک بخش. diff --git a/src/ClinicService/Controller/ClinicServiceController.php b/src/ClinicService/Controller/ClinicServiceController.php index b79565b1..a7d865be 100644 --- a/src/ClinicService/Controller/ClinicServiceController.php +++ b/src/ClinicService/Controller/ClinicServiceController.php @@ -122,6 +122,20 @@ class ClinicServiceController extends BaseController // ── Service Items ──────────────────────────────────────────────────────── + /** همه‌ی سرویس‌های owner در همه‌ی بخش‌ها — برای انتخاب/جستجوی سراسری. */ + #[Route('/api/v1/service-items', methods: ['GET'])] + public function listAllItems(#[CurrentUser] User $user): JsonResponse + { + [$entityType, $entityId] = $this->resolveEntity($user); + + $items = array_map( + fn(ServiceItem $i) => $i->toArray(), + $this->itemRepo->findByEntity($entityType, $entityId) + ); + + return $this->success($items); + } + #[Route('/api/v1/service-items/{sectionUuid}', methods: ['GET'])] public function listItems(string $sectionUuid, #[CurrentUser] User $user): JsonResponse { diff --git a/src/ClinicService/Repository/ServiceItemRepository.php b/src/ClinicService/Repository/ServiceItemRepository.php index 772df1a2..9720fa0b 100644 --- a/src/ClinicService/Repository/ServiceItemRepository.php +++ b/src/ClinicService/Repository/ServiceItemRepository.php @@ -29,6 +29,18 @@ class ServiceItemRepository extends ServiceEntityRepository ->getResult(); } + /** همه‌ی سرویس‌های یک owner (در همه‌ی بخش‌ها) — برای انتخاب/جستجوی سراسری. */ + public function findByEntity(string $entityType, int $entityId): array + { + return $this->createQueryBuilder('i') + ->join('i.section', 's') + ->where('s.entityType = :type')->setParameter('type', $entityType) + ->andWhere('s.entityId = :id')->setParameter('id', $entityId) + ->orderBy('i.name', 'ASC') + ->getQuery() + ->getResult(); + } + /** * Count services per section in a single query (avoids N+1 in the section list). * diff --git a/src/Patient/Controller/PatientController.php b/src/Patient/Controller/PatientController.php index 43e4f22a..d569cdd4 100644 --- a/src/Patient/Controller/PatientController.php +++ b/src/Patient/Controller/PatientController.php @@ -982,13 +982,9 @@ class PatientController extends BaseController $data['invoice_uuid'] = $invoice?->getUuid(); $data['invoice_status'] = $invoice?->getStatus(); - if ($data['is_paid']) { - $data['patient_debt_rials'] = 0; - } else { - // سهم بیمار (از فاکتور در صورت وجود) منهای تخفیف تسویه و پرداخت‌های جزئی - $share = $invoice !== null ? $invoice->getPatientRials() : $session->getFinalPriceRials(); - $data['patient_debt_rials'] = max(0, $share - $session->getDiscountRials() - $session->getPaidTotalRials()); - } + // مانده‌ی بدهی همیشه از خودِ مراجعه (سازگار با is_paid): مبلغ نهایی منهای تخفیف + // و پرداخت‌ها. اگر مراجعه پس از صدور فاکتور ویرایش شود، همین منبعِ واحد ملاک است. + $data['patient_debt_rials'] = $session->getRemainingRials(); return $data; }