feat(admin): implement ServiceVisitCard component for improved session display

This commit is contained in:
hamed
2026-07-13 09:57:28 +03:30
parent 3cf27aa8e4
commit 5c09fe8ac3
+136 -45
View File
@@ -2,7 +2,9 @@ import {
Bars3Icon,
CheckCircleIcon,
ChevronRightIcon,
ClockIcon,
FolderOpenIcon,
FunnelIcon,
MagnifyingGlassIcon,
PencilIcon,
PhoneIcon,
@@ -1126,52 +1128,37 @@ function MyPatientsPageInner() {
{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>
<>
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
<button
className="cp-btn-primary"
onClick={() => navigate(`/admin/my-patients/${selectedRecord.uuid}/session/new`)}
>
<PlusIcon style={{ width: 16 }} /> سرویس جدید
</button>
<button className="cp-btn-secondary" style={{ padding: "0 12px" }} title="فیلتر">
<FunnelIcon style={{ width: 18 }} />
</button>
</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(277px, 1fr))", gap: 16 }}>
{sessions.map((s) => (
<ServiceVisitCard
key={s.uuid}
session={s}
settling={settleSessionMut.isPending}
onSettle={(uuid) => settleSessionMut.mutate(uuid)}
onViewInvoice={(uuid) => setInvoiceUuid(uuid)}
/>
))}
</div>
)}
</>
)
)}
@@ -1719,6 +1706,110 @@ function PatientRow({
);
}
// کارت هر مراجعه در تب «سرویس‌ها»: اول مراجعه (سرویس‌ها در سرِ کارت)، سپس جزئیات و پرداخت.
function ServiceVisitCard({
session,
settling,
onSettle,
onViewInvoice,
}: {
session: PatientSession;
settling: boolean;
onSettle: (uuid: string) => void;
onViewInvoice: (uuid: string) => void;
}) {
const services = session.services ?? [];
const title = services[0]?.service_name || "ویزیت";
const subtitle = services.length
? services.map((s) => s.service_name).join(" - ")
: "ویزیت";
const assistant = services.find((s) => s.staff_name)?.staff_name || null;
const debt = session.patient_debt_rials ?? 0;
const paid = !!session.is_paid;
const row = (
label: string,
value: React.ReactNode,
danger = false,
) => (
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{label}</span>
<span style={{ color: danger ? "var(--danger)" : "var(--text-2)", fontSize: 14, fontWeight: 500 }}>{value}</span>
</div>
);
const divider = <div style={{ height: 1, background: "var(--border)", margin: "12px 0" }} />;
return (
<div
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
borderRadius: 8,
boxShadow: "0 1px 24.8px rgba(204,204,204,0.18)",
padding: 12,
display: "flex",
flexDirection: "column",
}}
>
{/* سرِ کارت: سرویس‌ها + وضعیت + منو */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
<div style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 6, textAlign: "right" }}>
<span style={{ fontWeight: 700, fontSize: 14, color: "var(--text)", lineHeight: 1.5 }}>{title}</span>
<span style={{ fontSize: 12, color: "var(--text-2)", lineHeight: 1.5 }}>{subtitle}</span>
</div>
<div
style={{
width: 36, height: 36, borderRadius: 8, flexShrink: 0,
display: "grid", placeItems: "center",
background: paid ? "rgba(60,154,79,0.15)" : "rgba(241,119,50,0.15)",
}}
>
{paid
? <CheckCircleIcon style={{ width: 20, color: "#3C9A4F" }} />
: <ClockIcon style={{ width: 20, color: "#F17732" }} />}
</div>
</div>
<EllipsisHorizontalIcon style={{ width: 20, color: "var(--text-3)", flexShrink: 0 }} />
</div>
{divider}
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{row("انجام دهنده:", session.doctor_name || "—")}
{row("دستیار:", assistant || "—")}
{row("تاریخ:", formatDate(session.created_at))}
{session.notes && (
<div style={{ fontSize: 12, color: "var(--text-3)", textAlign: "right", lineHeight: 1.6 }}>
توضیحات:{" "}
<span style={{ color: "var(--text-2)", fontWeight: 500 }}>{session.notes}</span>
</div>
)}
</div>
{divider}
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{row("هزینه:", formatRial(session.final_price_rials))}
{row("مانده بدهی:", debt > 0 ? formatRial(debt) : "ندارد", debt > 0)}
</div>
<div style={{ marginTop: 12 }}>
{paid ? (
session.invoice_uuid ? (
<button className="cp-btn-secondary" style={{ width: "100%" }} onClick={() => onViewInvoice(session.invoice_uuid!)}>مشاهده فاکتور</button>
) : (
<button className="cp-btn-secondary" style={{ width: "100%" }} disabled>پرداخت شده</button>
)
) : (
<button className="cp-btn-primary" style={{ width: "100%" }} disabled={settling} onClick={() => onSettle(session.uuid)}>تکمیل پرداخت</button>
)}
</div>
</div>
);
}
function SessionRow({
session,
onEdit,