Files
clinicpro/assets/admin/pages/NewSessionPage.tsx
T
hamedandClaude Opus 4.8 76bfa5d36f fix: issue invoice on demand so view-invoice buttons actually work
The admin SPA never called POST /billing/invoices, so every session had
invoice_uuid = null and the view-invoice buttons on the patient services
tab (and MyPatients visit modal) were permanently disabled. Add a shared
useIssueInvoice hook (idempotent create + finalize when draft) and wire
it into:
- DetailsStep: the wizard's final step now really issues the invoice
- SessionServiceCard / PatientDetailPage: clicking view-invoice on a
  session without an invoice issues it first, then opens the summary
- MyPatientsPage visit modal: same, replacing the dead disabled button

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:39:02 +03:30

120 lines
5.3 KiB
TypeScript

import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { PatientRecord } from '../types';
import type { SessionCardData } from '../components/SessionServiceCard';
import SessionStepper from '../components/SessionStepper';
import CreateStep from '../components/session/CreateStep';
import PaymentStep from '../components/session/PaymentStep';
import DetailsStep from '../components/session/DetailsStep';
import { ArrowLeftPH, ArrowLeftD, CloseModalD } from '../components/icons/FilesServiceIcons';
/**
* ثبت مراجعه جدید — پورت کامل tauri /files/create-service (حالت ایجاد):
* ویزارد سه‌گامی «ایجاد سرویس ← پرداخت ← جزییات». گام ۱ session را می‌سازد
* (تاریخ/ساعت پذیرش، خدمات + پرسنل، کالای مصرفی، پکیج، بیمه‌ی شرطی)؛
* گام‌های ۲ و ۳ همان کامپوننت‌های مشترک SessionPaymentPage هستند.
*/
export default function NewSessionPage() {
const { recordUuid = '' } = useParams();
const nav = useNavigate();
const steps = ['ایجاد سرویس', 'پرداخت', 'جزییات'];
const [activeStep, setActiveStep] = useState(0);
const [createdUuid, setCreatedUuid] = useState('');
const recordQ = useQuery<ApiResponse<PatientRecord>>({
queryKey: ['patient-detail', recordUuid],
queryFn: () => api.get(`/api/v1/patient/${recordUuid}`),
enabled: !!recordUuid,
});
const record = recordQ.data?.data as PatientRecord | undefined;
const patientName = record?.user_name || record?.profile?.full_name || '—';
// بعد از ساخت session در گام ۱، گام‌های ۲/۳ از لیست sessions تغذیه می‌شوند.
const sessionsQ = useQuery<ApiResponse<SessionCardData[]>>({
queryKey: ['patient-sessions', recordUuid],
queryFn: () => api.get(`/api/v1/patient/${recordUuid}/sessions`),
enabled: !!recordUuid && !!createdUuid,
});
const session = (sessionsQ.data?.data ?? []).find((s) => s.uuid === createdUuid);
const walletQ = useQuery<ApiResponse<{ balance_rials: number }>>({
queryKey: ['patient-wallet', recordUuid],
queryFn: () => api.get(`/api/v1/patient/${recordUuid}/wallet`),
enabled: !!recordUuid && !!createdUuid,
});
const walletBalance = (walletQ.data?.data as any)?.balance_rials ?? 0;
const finish = () => nav(`/admin/patients/${recordUuid}?tab=services`);
return (
<div className="fade-in" style={{ width: '100%' }}>
{/* breadcrumb — tauri AddService header */}
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, padding: '0 16px' }}>
<div
onClick={() => nav(-1)}
className="bg-white dark:bg-[#222433]"
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}
>
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
<span>بازگشت</span>
</div>
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{patientName}</span>
</div>
</div>
{/* card — tauri width 748 centered */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-white dark:bg-[#222433]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<button
type="button"
aria-label="بستن"
onClick={() => nav(-1)}
style={{ minWidth: 40, height: 40, marginBottom: 8, borderRadius: '50%', border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<CloseModalD />
</button>
</div>
<SessionStepper activeStep={activeStep} steps={steps} />
{activeStep === 0 && (
<CreateStep
recordUuid={recordUuid}
profile={record?.profile}
onCreated={(uuid) => { setCreatedUuid(uuid); setActiveStep(1); }}
onCancel={() => nav(-1)}
/>
)}
{activeStep === 1 && (
session ? (
<PaymentStep
recordUuid={recordUuid}
session={session}
walletBalance={walletBalance}
onContinue={() => setActiveStep(2)}
onCancel={finish}
/>
) : (
<div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--text-3)', fontSize: 14 }}>در حال بارگذاری...</div>
)
)}
{activeStep === 2 && session && (
<DetailsStep session={session} recordUuid={recordUuid} onBack={() => setActiveStep(1)} onFinish={finish} />
)}
</div>
</div>
</div>
);
}