feat(patient): enhance session management with billing details and service tracking
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user