- 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.
171 lines
10 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|