Files
clinicpro/assets/admin/pages/MyPaymentDetailPage.tsx
T
hamed 55ab2f5dfc Implement comprehensive dark/light mode overhaul for admin panel
- Refactor color palette in `ui-design-spec.md` to utilize CSS variables exclusively, eliminating fixed hex values and Tailwind utility classes.
- Complete dark mode implementation in `uiStore.ts`, ensuring proper theme application via `applyTheme()` and `applyBrand()`.
- Create `admin-theme-dark-light-audit.md` to document the transition process, outlining issues with inline styles and fixed colors.
- Introduce `theme-tokens.test.ts` to enforce rules against fixed hex colors and ensure compliance with the design system.
- Update various components and styles to replace inline styles and fixed colors with CSS variables, ensuring consistent theming across light and dark modes.
- Ensure all changes maintain visual integrity in both light and dark modes, with a focus on accessibility and contrast standards.
2026-07-27 16:41:52 +03:30

192 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ChevronRightIcon, ChevronDownIcon, PlusIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import PageHeader from '../components/ui/PageHeader';
import Pagination from '../components/ui/Pagination';
import StatCard from '../components/ui/StatCard';
import StatusBadge from '../components/ui/StatusBadge';
import DataTable, { type Column } from '../components/ui/DataTable';
import { formatRial, formatDate, formatNumber } from '../lib/utils';
import { paymentMethodLabel } from '../lib/paymentMethods';
import {
usePatientInvoices,
MY_PAYMENTS_LIMIT,
type PatientInvoiceRow,
} from '../hooks/useMyPayments';
const EMPTY: PatientInvoiceRow[] = [];
/** HH:MM (Persian digits) from a unix timestamp. */
function formatTime(unix: number): string {
return new Intl.DateTimeFormat('fa-IR', { hour: '2-digit', minute: '2-digit' }).format(new Date(unix * 1000));
}
function Avatar({ name }: { name: string | null }) {
return (
<div style={{
width: 44, height: 44, borderRadius: '50%', flexShrink: 0,
background: 'linear-gradient(145deg, var(--primary), var(--primary-700, var(--primary)))',
display: 'grid', placeItems: 'center', color: 'var(--on-primary)', fontSize: 17, fontWeight: 700,
}}>
{(name ?? '؟').charAt(0)}
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div style={{ minWidth: 220, flex: '1 1 260px' }}>
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-3)', marginBottom: 8 }}>{title}</div>
{children}
</div>
);
}
/** آیتم‌های صورتحساب و پرداخت‌های ثبت‌شده‌ی آن، کنار هم در ردیف بازشونده. */
function InvoiceBreakdown({ row }: { row: PatientInvoiceRow }) {
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}>
<Section title="آیتم‌های صورتحساب">
{row.items.length === 0 ? (
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>آیتمی ثبت نشده است.</span>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{row.items.map((it) => (
<div key={it.uuid} style={{ display: 'flex', justifyContent: 'space-between', gap: 12, fontSize: 13 }}>
<span style={{ color: 'var(--text-2)' }}>
{it.title}{it.quantity > 1 ? ` × ${formatNumber(it.quantity)}` : ''}
</span>
<span style={{ fontWeight: 600 }}>{formatRial(it.total_rials)}</span>
</div>
))}
</div>
)}
</Section>
<Section title="روش‌های پرداخت">
{row.payments.length === 0 ? (
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>پرداختی ثبت نشده است.</span>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{row.payments.map((p, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', gap: 12, fontSize: 13 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--text-2)' }}>
<span className="badge gray"><span className="bdot" />{paymentMethodLabel(p.method)}</span>
<span style={{ fontSize: 11.5, color: 'var(--text-3)' }} dir="ltr">{formatDate(p.paid_at)}</span>
</span>
<span style={{ fontWeight: 600 }}>{formatRial(p.amount_rials)}</span>
</div>
))}
</div>
)}
</Section>
</div>
);
}
/**
* پرداخت‌های ثبت‌شده‌ی یک بیمار — سربرگ بیمار، کارت‌های آمار و جدول صورتحساب‌ها
* با ردیف بازشونده‌ی آیتم‌ها و روش‌های پرداخت.
*/
export default function MyPaymentDetailPage() {
const { patientUuid } = useParams<{ patientUuid: string }>();
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [expanded, setExpanded] = useState<string | null>(null);
const { data, isLoading } = usePatientInvoices(patientUuid, page);
const payload = data?.data;
const patient = payload?.patient;
const rows = payload?.data ?? EMPTY;
const summary = payload?.summary;
const total = payload?.meta?.totalRecords ?? 0;
const columns: Column<PatientInvoiceRow>[] = [
{ key: 'number', header: 'شماره صورتحساب', render: (r) => <span style={{ fontWeight: 600 }} dir="ltr">#{formatNumber(r.number)}</span> },
{ key: 'issued_at', header: 'تاریخ', render: (r) => formatDate(r.issued_at) },
{ key: 'time', header: 'ساعت', render: (r) => <span dir="ltr">{formatTime(r.issued_at)}</span> },
{ key: 'service_title', header: 'نوع خدمت', render: (r) => <span style={{ color: 'var(--text-2)' }}>{r.service_title ?? '—'}</span> },
{
key: 'total_rials',
header: 'مبلغ کل',
render: (r) => (
<div>
<div style={{ fontWeight: 600 }}>{formatRial(r.total_rials)}</div>
{r.status === 'partial' && (
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 2 }}>
پرداخت‌شده: {formatRial(r.paid_rials)}
</div>
)}
</div>
),
},
{ key: 'status', header: 'وضعیت', render: (r) => <StatusBadge type="invoice" value={r.status} /> },
];
return (
<>
<PageHeader
title="پرداخت‌های ثبت‌شده"
description="صورتحساب‌های ثبت‌شده‌ی این بیمار"
breadcrumbs={[
{ label: 'داشبورد', to: '/admin' },
{ label: 'لیست پرداخت‌ها', to: '/admin/my-payments' },
{ label: patient?.name ?? 'بیمار' },
]}
action={
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn sm" onClick={() => navigate('/admin/my-payments')}>
<ChevronRightIcon style={{ width: 15 }} /> بازگشت
</button>
<button className="btn primary sm" onClick={() => toast.info('ثبت پرداخت جدید به‌زودی اضافه می‌شود')}>
<PlusIcon style={{ width: 15 }} /> پرداخت جدید
</button>
</div>
}
/>
<div className="card" style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px', marginBottom: 'var(--gap)' }}>
<Avatar name={patient?.name ?? null} />
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 700, fontSize: 15 }}>{patient?.name ?? '—'}</div>
<div style={{ fontSize: 12.5, color: 'var(--text-3)', marginTop: 3 }} dir="ltr">
{patient?.national_code ?? '—'}
</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 'var(--gap)', marginBottom: 'var(--gap)' }}>
<StatCard tone="violet" label="مجموع صورتحساب‌ها" value={formatRial(summary?.total_rials ?? 0)} />
<StatCard tone="green" label="پرداخت‌شده" value={formatRial(summary?.paid_rials ?? 0)} />
<StatCard tone="pink" label="تسویه‌نشده" value={formatRial(summary?.unsettled_rials ?? 0)} />
<StatCard tone="amber" label="تعداد صورتحساب" value={formatNumber(summary?.invoices_count ?? 0)} />
</div>
<div className="card" style={{ padding: 18 }}>
<DataTable
columns={columns}
data={rows}
loading={isLoading}
emptyMessage="صورتحسابی ثبت نشده است."
actions={(row) => (
<button
className="btn ghost sm"
style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color: 'var(--primary)' }}
onClick={() => setExpanded(expanded === row.uuid ? null : row.uuid)}
>
بیشتر
<ChevronDownIcon style={{ width: 15, transform: expanded === row.uuid ? 'rotate(180deg)' : undefined, transition: 'transform .15s' }} />
</button>
)}
renderExpanded={(row) => (expanded === row.uuid ? <InvoiceBreakdown row={row} /> : null)}
/>
{total > MY_PAYMENTS_LIMIT && (
<Pagination page={page} total={total} limit={MY_PAYMENTS_LIMIT} onPageChange={setPage} />
)}
</div>
</>
);
}