Port the tauri /files/create-service page (payment mode) to the admin SPA
and back it with real multi-part session settlement:
Backend:
- New SessionPayment entity (session_payments table): partial payments
per session with method (wallet/pos/cash/card), amount, paid_at, actor
- PatientSession: settlement discount (percent/fixed), discount_rials,
paid_at, payments relation; remaining debt derived from
final - discount - paid total
- POST /api/v1/session/{uuid}/payments: register a partial payment;
wallet method debits the patient wallet; zero remaining marks paid
- PATCH /api/v1/session/{uuid}: accepts discount_type/discount_value
(null removes) and paid_at, backward compatible
- New error codes: ERR_SESSION_PAYMENT_INVALID/_EXCEEDS,
ERR_SESSION_DISCOUNT_INVALID
- Migration + 14 functional tests (partial/full/wallet/exceed/discount)
Frontend (admin):
- SessionPaymentPage: two-step stepper (پرداخت ← جزییات) ported from
tauri AddService payment mode — service cost, settlement discount
input, Jalali payment date, wallet balance, 4-method payment accordion,
paid-list box, details summary
- SessionStepper + stepper/payment icons ported verbatim from tauri SVGs
- «تکمیل پرداخت» on SessionServiceCard now navigates to the payment page
(replaces the small settle modal on PatientDetailPage)
- Routes for patients/ and my-patients/ variants; vitest coverage
- docs/api/patient.md updated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.9 KiB
TypeScript
64 lines
2.9 KiB
TypeScript
import {
|
|
FilesServiceAddServiceStep,
|
|
FilesAddServicePaymentStep,
|
|
FilesServiceDetailsStep,
|
|
} from './icons/FilesServiceIcons';
|
|
|
|
/**
|
|
* استپر صفحهی سرویس/تسویه — پورت tauri files/services/CustomizedStepper
|
|
* (کانکتور بنفش، آیکون دایرهای هر گام، تیک سبز برای گامهای کاملشده).
|
|
*/
|
|
|
|
// هر برچسب گام آیکون ثابت خودش را دارد تا در حالت payment-only هم درست بماند.
|
|
const ICON_BY_LABEL: Record<string, number> = {
|
|
'ایجاد سرویس': 1,
|
|
'ویرایش': 1,
|
|
'پرداخت': 2,
|
|
'جزییات': 3,
|
|
};
|
|
|
|
function StepIcon({ index, active, completed }: { index: number; active: boolean; completed: boolean }) {
|
|
if (completed && !active) {
|
|
// دایره سفید با تیک سبز (tauri ColorlibStepIconRoot completed state)
|
|
return (
|
|
<div style={{ width: 52, height: 52, borderRadius: '50%', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="#22C55E" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
const shadow = active ? { boxShadow: '0 4px 10px 0 rgba(85, 89, 206, 0.25)', borderRadius: '50%' } : undefined;
|
|
switch (index) {
|
|
case 1: return <div style={shadow}><FilesServiceAddServiceStep active={active} /></div>;
|
|
case 2: return <div style={shadow}><FilesAddServicePaymentStep active={active} /></div>;
|
|
default: return <div style={shadow}><FilesServiceDetailsStep active={active} /></div>;
|
|
}
|
|
}
|
|
|
|
export default function SessionStepper({ activeStep = 0, steps = [] }: { activeStep?: number; steps?: string[] }) {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', width: '100%', marginBottom: 24 }}>
|
|
{steps.map((label, i) => (
|
|
<div key={label} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative' }}>
|
|
{/* connector to previous step (tauri ColorlibConnector: top 22, h 3, marginX 40) */}
|
|
{i > 0 && (
|
|
<div
|
|
data-testid={`step-connector-${i}`}
|
|
style={{
|
|
position: 'absolute', top: 22, right: 'calc(50% + 40px)', left: 'calc(-50% + 40px)',
|
|
height: 3, borderRadius: 1,
|
|
background: i <= activeStep ? 'linear-gradient(95deg, #5559ce 0%, #5559ce 100%)' : '#eaeaf0',
|
|
}}
|
|
/>
|
|
)}
|
|
<StepIcon index={ICON_BY_LABEL[label] ?? i + 1} active={i === activeStep} completed={i < activeStep} />
|
|
<span className="dark:text-[#D7D8ED]" style={{ marginTop: 8, fontSize: '0.875rem', fontWeight: 600, color: '#3b3b3b' }}>
|
|
{label}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|