feat(patient): enhance session management with billing details and service tracking

This commit is contained in:
hamed
2026-07-12 22:09:13 +03:30
parent fdce888e0e
commit f15c57d73c
4 changed files with 148 additions and 3 deletions
+97 -2
View File
@@ -111,7 +111,8 @@ function MyPatientsPageInner() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [detailTab, setDetailTab] = useState<"info" | "sessions">("info");
const [detailTab, setDetailTab] = useState<"info" | "services" | "sessions">("info");
const [invoiceUuid, setInvoiceUuid] = useState<string | null>(null);
const [sessionPage, setSessionPage] = useState(1);
const [sessionModal, setSessionModal] = useState(false);
const [editSession, setEditSession] = useState<PatientSession | null>(null);
@@ -218,6 +219,13 @@ function MyPatientsPageInner() {
enabled: !!selectedRecord,
});
const { data: invoiceData } = useQuery<ApiResponse<any>>({
queryKey: ["invoice", invoiceUuid],
queryFn: () => api.get(`/api/v1/billing/invoices/${invoiceUuid}`),
enabled: !!invoiceUuid,
});
const invoice = (invoiceData?.data as any)?.data ?? (invoiceData?.data as any) ?? null;
const records = recordsData?.data ?? EMPTY_RECORDS;
const sessions = sessionsData?.data ?? EMPTY_SESSIONS;
const totalRec = recordsData?.meta?.totalRecords ?? 0;
@@ -325,6 +333,16 @@ function MyPatientsPageInner() {
});
};
const settleSessionMut = useMutation({
mutationFn: (uuid: string) =>
api.patch(`/api/v1/session/${uuid}`, { payment_method: "cash" }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["patient-sessions", selectedRecord?.uuid] });
toast.success("پرداخت ثبت شد");
},
onError: (e: any) => toast.error(e?.message || "خطا در ثبت پرداخت"),
});
const createSessionMut = useMutation({
mutationFn: (body: object) =>
api.post(`/api/v1/patient/${selectedRecord!.uuid}/session`, body),
@@ -968,7 +986,7 @@ function MyPatientsPageInner() {
</div>
<div className="cp-tabs" style={{ display: "flex", gap: 4, borderBottom: "1px solid var(--border)", marginBottom: 16 }}>
{([["info", "اطلاعات پرونده"], ["sessions", `تاریخچه مراجعات (${totalSes})`]] as const).map(([key, label]) => (
{([["info", "اطلاعات پرونده"], ["services", "سرویس‌ها"], ["sessions", `تاریخچه مراجعات (${totalSes})`]] as const).map(([key, label]) => (
<button
key={key}
onClick={() => setDetailTab(key)}
@@ -1102,6 +1120,83 @@ function MyPatientsPageInner() {
</div>
</Modal>
{detailTab === "services" && (
sessionsLoading ? (
<div style={{ padding: 32, textAlign: "center", color: "var(--text-3)" }}>در حال بارگذاری</div>
) : sessions.length === 0 ? (
<div className="card" style={{ padding: 32, textAlign: "center", color: "var(--text-3)" }}>
هنوز سرویسی برای این بیمار ثبت نشده است.
</div>
) : (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 16 }}>
{sessions.map((s) => {
const svcNames = (s.services ?? []).map((x) => x.service_name).join("، ") || "ویزیت";
const debt = s.patient_debt_rials ?? 0;
return (
<div key={s.uuid} className="card" style={{ padding: 16, display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
<div style={{ fontWeight: 700, fontSize: 14, lineHeight: 1.5 }}>{svcNames}</div>
{s.is_paid
? <span className="badge green"><span className="bdot" />تسویه شده</span>
: <span className="badge amber"><span className="bdot" />تسویه نشده</span>}
</div>
<div className="cp-info-row"><span className="cp-info-label">انجامدهنده</span><span className="cp-info-value">{s.doctor_name || "—"}</span></div>
<div className="cp-info-row"><span className="cp-info-label">تاریخ</span><span className="cp-info-value">{formatDate(s.created_at)}</span></div>
<div className="cp-info-row"><span className="cp-info-label">هزینه</span><span className="cp-info-value">{formatRial(s.final_price_rials)}</span></div>
<div className="cp-info-row" style={{ borderBottom: "none" }}>
<span className="cp-info-label">مانده بدهی</span>
<span className="cp-info-value" style={{ color: debt > 0 ? "var(--danger)" : "var(--success)" }}>
{debt > 0 ? formatRial(debt) : "ندارد"}
</span>
</div>
{s.is_paid ? (
s.invoice_uuid ? (
<button className="cp-btn-secondary" style={{ width: "100%", marginTop: 4 }} onClick={() => setInvoiceUuid(s.invoice_uuid!)}>مشاهده فاکتور</button>
) : (
<button className="cp-btn-secondary" style={{ width: "100%", marginTop: 4 }} disabled>پرداخت شده</button>
)
) : (
<button
className="cp-btn-primary"
style={{ width: "100%", marginTop: 4 }}
disabled={settleSessionMut.isPending}
onClick={() => settleSessionMut.mutate(s.uuid)}
>
تکمیل پرداخت
</button>
)}
</div>
);
})}
</div>
)
)}
<Modal open={!!invoiceUuid} title="فاکتور" size="md" onClose={() => setInvoiceUuid(null)}>
{!invoice ? (
<div style={{ padding: 24, textAlign: "center", color: "var(--text-3)" }}>در حال بارگذاری</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<div className="cp-info-row"><span className="cp-info-label">وضعیت</span><span className="cp-info-value">{invoice.status}</span></div>
<div className="cp-info-row"><span className="cp-info-label">مبلغ کل</span><span className="cp-info-value">{formatRial(invoice.total_rials ?? 0)}</span></div>
<div className="cp-info-row"><span className="cp-info-label">سهم بیمه پایه</span><span className="cp-info-value">{formatRial(invoice.base_insurance_rials ?? 0)}</span></div>
<div className="cp-info-row"><span className="cp-info-label">سهم بیمه تکمیلی</span><span className="cp-info-value">{formatRial(invoice.supplementary_rials ?? 0)}</span></div>
<div className="cp-info-row" style={{ borderBottom: "none" }}><span className="cp-info-label">سهم بیمار</span><span className="cp-info-value">{formatRial(invoice.patient_rials ?? 0)}</span></div>
{Array.isArray(invoice.items) && invoice.items.length > 0 && (
<div style={{ marginTop: 12 }}>
<div className="cp-section-title">اقلام</div>
{invoice.items.map((it: any, i: number) => (
<div key={i} className="cp-info-row">
<span className="cp-info-label">{it.title}</span>
<span className="cp-info-value">{formatRial(it.total_rials ?? 0)}</span>
</div>
))}
</div>
)}
</div>
)}
</Modal>
{detailTab === "sessions" && (
<div className="card">
<div
+19
View File
@@ -489,10 +489,24 @@ export interface PatientRecord {
profile?: PatientProfile | null;
}
export interface SessionServiceLine {
uuid: string;
service_item_uuid: string;
service_name: string;
staff_uuid: string | null;
staff_name: string | null;
price_rials: number;
quantity: number;
line_total_rials: number;
created_at: number;
}
export interface PatientSession {
uuid: string;
record_uuid: string;
appointment_uuid: string | null;
doctor_uuid?: string | null;
doctor_name?: string | null;
insurance_base_id: number | null;
insurance_supplementary_id: number | null;
visit_price_rials: number;
@@ -501,6 +515,11 @@ export interface PatientSession {
services_total_rials: number;
final_price_rials: number;
payment_method: string;
is_paid?: boolean;
services?: SessionServiceLine[];
invoice_uuid?: string | null;
invoice_status?: string | null;
patient_debt_rials?: number;
notes: string | null;
created_at: number;
updated_at: number;
+27 -1
View File
@@ -5,6 +5,7 @@ namespace App\Patient\Controller;
use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Auth\Repository\UserRepository;
use App\Billing\Repository\InvoiceRepository;
use App\Billing\Service\ClaimService;
use App\Billing\Service\InvoiceService;
use Psr\Log\LoggerInterface;
@@ -45,6 +46,7 @@ class PatientController extends BaseController
private readonly \App\Insurance\Repository\InsuranceRepository $insuranceRepo,
private readonly InvoiceService $invoiceService,
private readonly ClaimService $claimService,
private readonly InvoiceRepository $invoiceRepo,
private readonly LoggerInterface $logger,
) {}
@@ -309,13 +311,37 @@ class PatientController extends BaseController
$total = $this->sessionRepo->countByRecord($record);
return $this->paginated(
array_map(fn($s) => $s->toArray(), $sessions),
array_map(fn($s) => $this->sessionWithBilling($s), $sessions),
$total,
$page,
$limit
);
}
/**
* خروجی session به‌همراه خلاصه‌ی صورتحساب: uuid فاکتور (در صورت وجود) و
* مانده‌ی بدهیِ سهم بیمار. اگر session تسویه شده باشد (payment_method != pending)
* بدهی صفر است؛ در غیر این صورت سهم بیمار از فاکتور یا کل مبلغ نهایی.
*/
private function sessionWithBilling(\App\Patient\Entity\PatientSession $session): array
{
$data = $session->toArray();
$invoice = $session->getId() !== null ? $this->invoiceRepo->findBySession($session->getId()) : null;
$data['invoice_uuid'] = $invoice?->getUuid();
$data['invoice_status'] = $invoice?->getStatus();
if ($data['is_paid']) {
$data['patient_debt_rials'] = 0;
} elseif ($invoice !== null) {
$data['patient_debt_rials'] = $invoice->getPatientRials();
} else {
$data['patient_debt_rials'] = $session->getFinalPriceRials();
}
return $data;
}
#[Route('/api/v1/patient/{uuid}/session', methods: ['POST'])]
public function createSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
+5
View File
@@ -127,6 +127,11 @@ class PatientSession
'services_total_rials' => $this->servicesTotalRials,
'final_price_rials' => $this->finalPriceRials,
'payment_method' => $this->paymentMethod,
'is_paid' => $this->paymentMethod !== 'pending',
'services' => array_map(
fn(SessionService $s) => $s->toArray(),
$this->services->toArray()
),
'notes' => $this->notes,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt,