Files
clinicpro/assets/admin/components/session/DetailsStep.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

171 lines
10 KiB
TypeScript

import { toast } from 'sonner';
import { formatRial, formatDateTime } from '../../lib/utils';
import type { SessionCardData } from '../SessionServiceCard';
import { METHOD_LABELS } from './PaymentStep';
import { useIssueInvoice } from '../../hooks/useIssueInvoice';
const primaryBtn: React.CSSProperties = { background: 'var(--primary)', color: 'var(--on-primary)', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
const ghostBtn: React.CSSProperties = { background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
interface Props {
session: SessionCardData;
/** برای invalidate شدن لیست sessions بعد از صدور فاکتور. */
recordUuid?: string;
onBack: () => void;
onFinish: () => void;
}
/**
* گام «جزییات» — پورت tauri Step3Final با دیتای واقعی:
* خلاصه‌ی سرویس/هزینه/تخفیف + لیست پرداخت‌شده‌ها + مانده. مشترک بین
* NewSessionPage (ویزارد سه‌گامه) و SessionPaymentPage (دوگامه).
* «صدور فاکتور» فاکتور را واقعاً صادر می‌کند (create + finalize) و بعد onFinish.
*/
export default function DetailsStep({ session, recordUuid, onBack, onFinish }: Props) {
const issueMut = useIssueInvoice(recordUuid);
const issue = () => {
if (session.invoice_uuid) { onFinish(); return; }
issueMut.mutate(session.uuid, {
onSuccess: () => onFinish(),
onError: () => toast.error('صدور فاکتور ناموفق بود'),
});
};
const finalPrice = session.final_price_rials ?? 0;
const discountRials = session.discount_rials ?? 0;
const debt = session.patient_debt_rials ?? 0;
const payments = session.payments ?? [];
const visitPrice = session.visit_price_rials ?? 0;
const serviceNames = (session.services ?? []).map((s) => s.service_name || s.name).filter(Boolean) as string[];
if (visitPrice > 0) serviceNames.unshift('ویزیت');
// زمان مراجعه: زمان واقعی نوبت؛ در نبود آن، زمان ثبت پرونده.
const visitAt = session.session_at ?? session.created_at ?? null;
// آیتم‌های هزینه‌ی تفکیک‌شده: ویزیت + هر سرویس + کالای مصرفی.
// کالای مصرفی هم باید بیاید چون سرور آن را در سهم بیمار می‌آورد
// (PatientService::…$consumablesTotal)؛ نبودش ریز هزینه‌ها را با جمع ناسازگار می‌کرد.
const costItems: Array<{ label: string; rials: number }> = [];
if (visitPrice > 0) costItems.push({ label: 'ویزیت', rials: visitPrice });
(session.services ?? []).forEach((s) => {
costItems.push({
label: (s.service_name || s.name) ?? 'سرویس',
rials: s.line_total_rials ?? (s.price_rials ?? 0) * (s.quantity ?? 1),
});
});
(session.consumables ?? []).forEach((c) => {
costItems.push({
label: c.item_name ?? 'کالای مصرفی',
rials: c.line_total_rials ?? 0,
});
});
// خطوط بالا ناخالص‌اند (پیش از بیمه) ولی finalPrice سهمِ بیمار پس از بیمه است.
// پس وقتی بیمه هست، جمعِ خطوط را ناخالص نشان بده و سهم بیمه را جدا کم کن.
const grossTotal = session.gross_total_rials ?? finalPrice;
const baseInsurance = session.base_insurance_rials ?? 0;
const suppInsurance = session.supplementary_insurance_rials ?? 0;
const hasInsurance = baseInsurance > 0 || suppInsurance > 0;
return (
<>
{/* گام جزییات — پورت tauri Step3Final با دیتای واقعی */}
<div dir="rtl" className="dark:bg-[var(--surface)]" style={{ padding: 16, border: '1px dashed var(--primary-soft2)' }}>
{visitAt && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 16 }}>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>تاریخ و ساعت مراجعه:</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{formatDateTime(visitAt)}</span>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 32, marginBottom: 16 }}>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
{serviceNames.length ? serviceNames.join(' - ') : 'ویزیت'}
</span>
<span style={{ width: 1, height: 38, background: 'var(--border-2)' }} />
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{session.doctor_name || '—'}</span>
</div>
{costItems.length > 0 && (
<div style={{ marginBottom: 16, borderTop: '1px solid var(--border)', borderBottom: '1px solid var(--border)', padding: '4px 0' }}>
{costItems.map((it, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 0' }}>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>{it.label}</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{formatRial(it.rials)}</span>
</div>
))}
{hasInsurance && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0 4px', borderTop: '1px dashed var(--border-2)' }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>جمع کل خدمات</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{formatRial(grossTotal)}</span>
</div>
{baseInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 0' }}>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>سهم بیمه پایه</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--success)' }}>{formatRial(baseInsurance)}</span>
</div>
)}
{suppInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 0' }}>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>سهم بیمه تکمیلی</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--success)' }}>{formatRial(suppInsurance)}</span>
</div>
)}
</>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0 4px', borderTop: '1px dashed var(--border-2)' }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{hasInsurance ? 'سهم بیمار' : 'جمع کل'}</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</div>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 72, marginBottom: 16 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>هزینه سرویس:</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</span>
<span style={{ width: 1, height: 38, background: 'var(--border-2)' }} />
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>تخفیف:</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{formatRial(discountRials)}</span>
</span>
</div>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--primary)', display: 'block', marginBottom: 12 }}>پرداخت شده ها:</span>
{payments.length === 0 ? (
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>پرداختی ثبت نشده است</span>
) : payments.map((p) => (
<div key={p.uuid} style={{ padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 56 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--primary)', flexShrink: 0 }} />
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>مبلغ: {formatRial(p.amount_rials)}</span>
</div>
<div style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: 'var(--text-3)', flexWrap: 'wrap' }}>
{(p.paid_at ?? p.created_at) && <span>{formatDateTime(p.paid_at ?? p.created_at!)}</span>}
{p.created_by_name && <span>ثبت: {p.created_by_name}</span>}
</div>
</div>
))}
<div style={{ display: 'flex', width: '100%', justifyContent: 'flex-end', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 16 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>مبلغ باقی مانده:</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--danger)' }}>{formatRial(debt)}</span>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 16, width: '100%', maxWidth: 320, margin: '16px auto 0' }}>
<button type="button" style={{ ...ghostBtn, flex: 1 }} onClick={onBack}>انصراف</button>
<button type="button" disabled={issueMut.isPending} style={{ ...primaryBtn, flex: 1 }} onClick={issue}>
{issueMut.isPending ? 'در حال صدور…' : 'صدور فاکتور'}
</button>
</div>
</>
);
}