Files
clinicpro/assets/admin/components/session/CreateStep.tsx
T

561 lines
35 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { PlusIcon, MinusIcon } from '@heroicons/react/24/outline';
import { api } from '../../lib/api';
import type { ApiResponse } from '../../lib/api';
import type { PatientProfile, ServiceSection, ServiceItem } from '../../types';
import { formatRial, rialToToman, tomanToRial } from '../../lib/utils';
import SearchableSelect from '../ui/SearchableSelect';
import PersianDateInput from '../ui/PersianDateInput';
import { UserTick, FilesServiceAddCard, ClockP, TrashRed } from '../icons/FilesServiceIcons';
import { useAuthStore } from '../../stores/authStore';
import { digitsOnly, todayIso } from '../../lib/utils';
import {
DEFAULT_SERVICE_CATEGORY, contractPercentFor, patientShareOf,
type CoverageRule as Rule, type TenantContract,
} from '../../lib/insuranceShares';
interface Contract extends TenantContract { uuid: string }
interface CoverageRow { service_item_uuid: string | null; covered: boolean; coverage_percent: number | null; franchise_percent: number | null; ceiling_rials: number | null }
interface InventoryItemRow { uuid: string; name: string; unit: string; price: number; stock: number; status: string }
interface PackageRow { uuid: string; title: string; total: number; available: boolean }
interface StaffRow { uuid: string; full_name: string; active?: boolean }
/** ویزیت خدمتِ سرپایی است. */
const VISIT_SERVICE_CATEGORY = DEFAULT_SERVICE_CATEGORY;
// آینه‌ی BillingCalculator در lib/insuranceShares است؛ اینجا فقط re-export می‌شود تا
// مصرف‌کنندگان قبلی (و تست‌ها) نشکنند.
export { patientShareOf };
const todayISO = todayIso;
const nowHHMM = () => {
const d = new Date();
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
};
/** تاریخ میلادی + ساعت → unix زمان پذیرش */
const toSessionAt = (iso: string, time: string) => Math.floor(new Date(`${iso}T${time || '12:00'}:00`).getTime() / 1000);
// برچسب فیلد — tauri Step1Details Typography (fontWeight 500, 0.875rem, #6B7280)
const fieldLabel: React.CSSProperties = { fontWeight: 500, lineHeight: '130%', fontSize: '0.875rem', marginBottom: 8, color: 'var(--text-2)', display: 'block' };
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 {
recordUuid: string;
profile?: PatientProfile | null;
onCreated: (sessionUuid: string) => void;
onCancel: () => void;
/** وقتی داده شود فرم در حالت ویرایش است و با PATCH به همان مراجعه ارسال می‌کند. */
editSession?: import('../SessionServiceCard').SessionCardData;
}
/**
* گام «ایجاد سرویس» — پورت tauri Step1Details با دیتای واقعی:
* تاریخ/ساعت پذیرش، بخش/سرویس/پرسنل، کالای مصرفی با شمارنده، پکیج، و بلوک بیمه‌ی
* موجودِ NewSessionPage (نمایش شرطی: سرویسِ تحت پوشش بیمه یا بیمه در پروفایل بیمار).
*/
export default function CreateStep({ recordUuid, profile, onCreated, onCancel, editSession }: Props) {
const qc = useQueryClient();
const userName = useAuthStore((s) => s.userName);
const isEdit = !!editSession;
// ── state گام ایجاد ──────────────────────────────────────────────────────
const [dateISO, setDateISO] = useState(todayISO());
const [time, setTime] = useState(nowHHMM());
const [sectionUuid, setSectionUuid] = useState('');
const [itemUuid, setItemUuid] = useState('');
const [staffUuid, setStaffUuid] = useState('');
const [selectedServices, setSelectedServices] = useState<{ uuid: string; name: string; price: number; qty: number; insured: boolean; category: string }[]>([]);
const [consumableUuid, setConsumableUuid] = useState('');
const [selectedConsumables, setSelectedConsumables] = useState<{ uuid: string; name: string; price: number; qty: number }[]>([]);
const [packageUuid, setPackageUuid] = useState('');
const [visitPrice, setVisitPrice] = useState('0');
const [visitPriceError, setVisitPriceError] = useState('');
const [baseId, setBaseId] = useState('');
const [suppId, setSuppId] = useState('');
const [basePercent, setBasePercent] = useState('0');
const [suppPercent, setSuppPercent] = useState('0');
/** نوع خدمتِ بیمه‌ایِ این مراجعه؛ خالی یعنی «پیش‌فرضِ tenant». */
const [serviceCategory, setServiceCategory] = useState('');
const [notes, setNotes] = useState('');
// ── داده‌ها ──────────────────────────────────────────────────────────────
const { data: sectionsData } = useQuery<ApiResponse<ServiceSection[]>>({
queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'),
});
// همه‌ی سرویس‌ها (سراسری) تا بتوان بدون انتخاب بخش هم جستجو و انتخاب کرد.
const { data: allItemsData } = useQuery<ApiResponse<ServiceItem[]>>({
queryKey: ['service-items-all'],
queryFn: () => api.get('/api/v1/service-items'),
});
const { data: itemsData } = useQuery<ApiResponse<ServiceItem[]>>({
queryKey: ['service-items-for-session', sectionUuid],
queryFn: () => api.get(`/api/v1/service-items/${sectionUuid}`),
enabled: !!sectionUuid,
});
const { data: staffData } = useQuery<ApiResponse<StaffRow[]>>({
queryKey: ['staff'], queryFn: () => api.get('/api/v1/staff'),
});
const { data: inventoryData } = useQuery<ApiResponse<{ items: InventoryItemRow[] }>>({
queryKey: ['inventory-items'], queryFn: () => api.get('/api/v1/inventory-items'),
});
const { data: packagesData } = useQuery<ApiResponse<PackageRow[]>>({
queryKey: ['inventory-packages'], queryFn: () => api.get('/api/v1/inventory-packages'),
});
const { data: contractsData } = useQuery<{ data: { data: Contract[] } }>({
queryKey: ['tenant-insurances'], queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
});
const { data: pricingData } = useQuery<{ data: { free_visit_price_rials: number; require_visit_price: boolean } }>({
queryKey: ['insurance-pricing'], queryFn: () => api.get('/api/v1/insurance-pricing'),
});
const freeVisit = (pricingData as any)?.data?.free_visit_price_rials ?? 0;
const requireVisit = (pricingData as any)?.data?.require_visit_price ?? false;
// نوع خدماتِ بیمه‌ایِ فعالِ این tenant — همان تنظیم سراسری «مدیریت بیمه».
const enabledCategories: { key: string; label: string }[] =
((pricingData as any)?.data?.service_categories ?? []).filter((c: any) => c.enabled);
const needsCategoryChoice = enabledCategories.length > 1;
const defaultCategory: string =
(pricingData as any)?.data?.default_service_category ?? enabledCategories[0]?.key ?? VISIT_SERVICE_CATEGORY;
useEffect(() => {
if (!isEdit && freeVisit > 0 && (!visitPrice || visitPrice === '0')) setVisitPrice(String(rialToToman(freeVisit)));
}, [freeVisit]); // eslint-disable-line react-hooks/exhaustive-deps
// پیش‌پرکردن فرم در حالت ویرایش (یک‌بار).
const [prefilled, setPrefilled] = useState(false);
useEffect(() => {
if (!editSession || prefilled) return;
setVisitPrice(String(rialToToman(editSession.visit_price_rials ?? 0)));
if (editSession.session_at) {
const d = new Date(editSession.session_at * 1000);
setDateISO(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
setTime(`${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`);
}
setNotes((editSession as any).notes ?? '');
if (editSession.insurance_base_id) { setBaseId(String(editSession.insurance_base_id)); setBasePercent(String(editSession.base_insurance_discount_percent ?? 0)); }
if (editSession.insurance_supplementary_id) { setSuppId(String(editSession.insurance_supplementary_id)); setSuppPercent(String(editSession.supplementary_discount_percent ?? 0)); }
setServiceCategory(editSession.insurance_service_category ?? '');
setSelectedServices((editSession.services ?? []).map((s) => ({
uuid: s.service_item_uuid ?? '', name: s.service_name || s.name || '', price: s.price_rials ?? 0, qty: s.quantity ?? 1, insured: false,
category: VISIT_SERVICE_CATEGORY,
})).filter((s) => s.uuid));
setSelectedConsumables((editSession.consumables ?? []).map((c) => ({
uuid: c.inventory_item_uuid ?? '', name: c.item_name ?? '', price: c.price_rials ?? 0, qty: c.quantity ?? 1,
})).filter((c) => c.uuid));
setPrefilled(true);
}, [editSession, prefilled]);
const contracts = (contractsData as any)?.data?.data as Contract[] | undefined ?? [];
const baseOpts = contracts.filter(c => c.insurance_kind === 'basic').map(c => ({ value: String(c.insurance_id), label: c.insurance_name ?? `#${c.insurance_id}` }));
const suppOpts = contracts.filter(c => c.insurance_kind === 'supplementary').map(c => ({ value: String(c.insurance_id), label: c.insurance_name ?? `#${c.insurance_id}` }));
const sectionOptions = (sectionsData?.data ?? []).map(s => ({ value: s.uuid, label: s.name }));
// با انتخاب بخش، فقط سرویس‌های همان بخش؛ بدون بخش، همه‌ی سرویس‌ها (قابل جستجو).
const serviceItems = (sectionUuid ? (itemsData?.data ?? []) : (allItemsData?.data ?? [])).filter(i => i.active);
const itemOptions = serviceItems.map(i => ({ value: i.uuid, label: i.name }));
const currentItem = serviceItems.find(i => i.uuid === itemUuid);
const staffOptions = ((staffData?.data as StaffRow[] | undefined) ?? []).filter(s => s.active !== false).map(s => ({ value: s.uuid, label: s.full_name }));
const inventoryItems = ((inventoryData?.data as any)?.items as InventoryItemRow[] | undefined) ?? [];
const consumableOptions = inventoryItems.map(i => ({ value: i.uuid, label: i.name }));
const currentConsumable = inventoryItems.find(i => i.uuid === consumableUuid);
const packageOptions = ((packagesData?.data as PackageRow[] | undefined) ?? []).map(p => ({ value: p.uuid, label: `${p.title}${formatRial(p.total)}` }));
const baseContract = contracts.find(c => String(c.insurance_id) === baseId) ?? null;
const suppContract = contracts.find(c => String(c.insurance_id) === suppId) ?? null;
/**
* مراجعهٔ جدید: بیمهٔ پروفایل بیمار پیش‌انتخاب می‌شود — فقط اگر برای همان بیمه
* قرارداد فعال وجود داشته باشد. یک‌بار، و بعدش انتخاب کاربر دست‌نخورده می‌ماند.
*/
const [insurancePrefilled, setInsurancePrefilled] = useState(false);
useEffect(() => {
if (isEdit || insurancePrefilled || contracts.length === 0) return;
const profileBase = profile?.basic_insurance_id ? String(profile.basic_insurance_id) : '';
const profileSupp = profile?.supplementary_insurance_id ? String(profile.supplementary_insurance_id) : '';
if (profileBase && baseOpts.some(o => o.value === profileBase)) applyBase(profileBase);
if (profileSupp && suppOpts.some(o => o.value === profileSupp)) applySupp(profileSupp);
setInsurancePrefilled(true);
}, [contracts.length, profile, isEdit, insurancePrefilled]); // eslint-disable-line react-hooks/exhaustive-deps
const baseCoverageQ = useQuery<{ data: { data: CoverageRow[] } }>({
queryKey: ['service-coverage', baseContract?.uuid],
queryFn: () => api.get(`/api/v1/billing/tenant-insurances/${baseContract!.uuid}/service-coverage`),
enabled: !!baseContract,
});
const suppCoverageQ = useQuery<{ data: { data: CoverageRow[] } }>({
queryKey: ['service-coverage', suppContract?.uuid],
queryFn: () => api.get(`/api/v1/billing/tenant-insurances/${suppContract!.uuid}/service-coverage`),
enabled: !!suppContract,
});
const baseCoverage = (baseCoverageQ.data as any)?.data?.data as CoverageRow[] | undefined ?? [];
const suppCoverage = (suppCoverageQ.data as any)?.data?.data as CoverageRow[] | undefined ?? [];
/**
* قاعده‌ی پوشش یک خدمت تحت یک قرارداد: override خدمت اگر باشد، وگرنه درصد
* همان نوع خدمت (سرپایی/بستری). فرانشیز فقط در قرارداد تکمیلی خوانده می‌شود.
*/
const ruleFor = (contract: Contract | null, coverage: CoverageRow[], serviceUuid: string, category: string): Rule | null => {
if (!contract) return null;
const ov = coverage.find(r => r.service_item_uuid === serviceUuid);
if (ov && !ov.covered) return { covered: false, percent: 0, franchise: 0, ceiling: null };
const isSupplementary = contract.insurance_kind === 'supplementary';
return {
covered: true,
percent: ov?.coverage_percent ?? contractPercentFor(contract, category),
franchise: isSupplementary ? (ov?.franchise_percent ?? contract.franchise_percent) : 0,
ceiling: ov?.ceiling_rials ?? contract.annual_ceiling_rials,
};
};
/** نوع خدمتِ مؤثرِ ویزیت: انتخاب کاربر، وگرنه تنها نوع فعالِ tenant. */
const visitCategory = serviceCategory || defaultCategory;
const coverageOf = (id: string, category = visitCategory): number => {
const contract = contracts.find(c => String(c.insurance_id) === id);
return contract ? contractPercentFor(contract, category) : 0;
};
const applyBase = (id: string) => { setBaseId(id); setBasePercent(id ? String(coverageOf(id)) : '0'); };
const applySupp = (id: string) => { setSuppId(id); setSuppPercent(id ? String(coverageOf(id)) : '0'); };
/** تغییر نوع خدمت، درصدهای ویزیت را با همان نوع همگام می‌کند. */
const applyServiceCategory = (category: string) => {
setServiceCategory(category);
if (baseId) setBasePercent(String(coverageOf(baseId, category || defaultCategory)));
if (suppId) setSuppPercent(String(coverageOf(suppId, category || defaultCategory)));
};
/**
* بلوک بیمه هر وقت این پزشک/کلینیک قرارداد بیمهٔ فعال دارد نمایش داده می‌شود تا
* بیمه قابل انتخاب و تغییر باشد؛ پیش‌تر تنها با سرویسِ تحت‌پوشش یا بیمهٔ پروفایل
* ظاهر می‌شد و کاربر راهی برای انتخاب بیمه نداشت. مراجعه‌ای که بیمه دارد هم
* (حالت ویرایش) همیشه بلوک را نشان می‌دهد.
*/
const showInsurance = contracts.length > 0
|| !!baseId
|| !!suppId
|| selectedServices.some(s => s.insured)
|| !!profile?.basic_insurance_id
|| !!profile?.supplementary_insurance_id;
// ── خدمات ────────────────────────────────────────────────────────────────
const addService = () => {
if (!currentItem || selectedServices.some(s => s.uuid === currentItem.uuid)) return;
setSelectedServices(p => [...p, {
uuid: currentItem.uuid, name: currentItem.name, price: currentItem.price_rials, qty: 1,
insured: !!currentItem.insurance_covered,
category: currentItem.service_category ?? VISIT_SERVICE_CATEGORY,
}]);
setItemUuid('');
};
const setServiceQty = (uuid: string, qty: number) => {
if (qty <= 0) { setSelectedServices(p => p.filter(s => s.uuid !== uuid)); return; }
setSelectedServices(p => p.map(s => s.uuid === uuid ? { ...s, qty } : s));
};
// ── کالای مصرفی (tauri addConsumable/increase/decrease) ─────────────────
const addConsumable = () => {
if (!currentConsumable) return;
setSelectedConsumables(p => {
const found = p.find(c => c.uuid === currentConsumable.uuid);
if (found) return p.map(c => c.uuid === found.uuid ? { ...c, qty: c.qty + 1 } : c);
return [...p, { uuid: currentConsumable.uuid, name: currentConsumable.name, price: currentConsumable.price, qty: 1 }];
});
setConsumableUuid('');
};
const setConsumableQty = (uuid: string, qty: number) => {
if (qty <= 0) { setSelectedConsumables(p => p.filter(c => c.uuid !== uuid)); return; }
setSelectedConsumables(p => p.map(c => c.uuid === uuid ? { ...c, qty } : c));
};
// ── قیمت‌ها (آینه‌ی سرور) ────────────────────────────────────────────────
// فیلد «قیمت ویزیت» تومان است؛ محاسبات و API ریالی‌اند.
const visit = tomanToRial(Number(visitPrice) || 0);
const base = Number(basePercent) || 0;
const supp = Number(suppPercent) || 0;
const servicesTotal = useMemo(() => selectedServices.reduce((s, x) => s + x.price * x.qty, 0), [selectedServices]);
const servicesPatient = useMemo(
() => selectedServices.reduce((sum, x) => {
const total = x.price * x.qty;
// نوع خدمت و پرچم پوشش از کاتالوگ خوانده می‌شوند تا ردیف‌های پیش‌پرشدهٔ ویرایش
// هم مثل سرور حساب شوند (هنگام prefill این دو را نداریم).
const catalogItem = serviceItems.find(i => i.uuid === x.uuid);
const insured = catalogItem?.insurance_covered ?? x.insured;
if (!insured) return sum + total;
const category = catalogItem?.service_category ?? x.category;
return sum + patientShareOf(
total,
ruleFor(baseContract, baseCoverage, x.uuid, category),
ruleFor(suppContract, suppCoverage, x.uuid, category),
);
}, 0),
[selectedServices, baseContract, suppContract, baseCoverage, suppCoverage, serviceItems], // eslint-disable-line react-hooks/exhaustive-deps
);
const consumablesTotal = useMemo(() => selectedConsumables.reduce((s, c) => s + c.price * c.qty, 0), [selectedConsumables]);
const afterBase = Math.round(visit * (1 - base / 100));
const afterSupp = Math.round(afterBase * (1 - supp / 100));
const finalPrice = afterSupp + servicesPatient + consumablesTotal;
// ── ثبت ──────────────────────────────────────────────────────────────────
const createMut = useMutation({
mutationFn: (body: object) => isEdit
? api.patch(`/api/v1/session/${editSession!.uuid}`, body)
: api.post(`/api/v1/patient/${recordUuid}/session`, body),
onSuccess: (res: any) => {
// بدون این، صفحهٔ پرونده و فهرست پرداخت‌ها نسخهٔ کش‌شده را نشان می‌دادند و
// وضعیت مراجعه تا رفرشِ دستی عوض نمی‌شد. صورتحساب هم سمت سرور با همین ویرایش
// هم‌تراز می‌شود، پس آمار پرداخت‌ها هم کهنه می‌ماند.
qc.invalidateQueries({ queryKey: ['patient-sessions', recordUuid] });
qc.invalidateQueries({ queryKey: ['patient-detail', recordUuid] });
qc.invalidateQueries({ queryKey: ['patient', recordUuid] });
qc.invalidateQueries({ queryKey: ['patient-invoices', recordUuid] });
qc.invalidateQueries({ queryKey: ['payments'] });
qc.invalidateQueries({ queryKey: ['payments-summary'] });
toast.success(isEdit ? 'مراجعه ویرایش شد' : 'مراجعه ثبت شد');
onCreated((isEdit ? editSession!.uuid : res?.data?.uuid) as string);
},
onError: (e: Error) => toast.error(e.message),
});
const submit = () => {
if (requireVisit && visit <= 0) {
setVisitPriceError('هزینه ویزیت الزامی است');
toast.error('هزینه ویزیت الزامی است');
return;
}
createMut.mutate({
visit_price_rials: visit,
base_insurance_discount_percent: showInsurance ? base : 0,
supplementary_discount_percent: showInsurance ? supp : 0,
...(showInsurance && baseId ? { insurance_base_id: Number(baseId) } : {}),
...(showInsurance && suppId ? { insurance_supplementary_id: Number(suppId) } : {}),
// نوع خدمتِ ویزیت؛ سرور درصد پوشش را بر پایهٔ همین resolve می‌کند.
insurance_service_category: showInsurance && (baseId || suppId) ? visitCategory : null,
...(isEdit ? {} : { payment_method: 'pending' }),
...(notes ? { notes } : {}),
session_at: toSessionAt(dateISO, time),
...(packageUuid ? { inventory_package_uuid: packageUuid } : {}),
services: selectedServices.map(s => ({ service_item_uuid: s.uuid, quantity: s.qty, ...(staffUuid ? { staff_uuid: staffUuid } : {}) })),
consumables: selectedConsumables.map(c => ({ inventory_item_uuid: c.uuid, quantity: c.qty })),
});
};
/** ردیف شمارنده‌ی tauri (باکس ۷۴px، +/− نارنجی #FF7A45، سطل روی تعداد ۱) */
const counter = (qty: number, set: (q: number) => void) => (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 74, height: 27, border: '1px solid var(--border-2)', borderRadius: 8, padding: '0 4px', gap: 4 }}>
<button type="button" aria-label="افزایش" onClick={() => set(qty + 1)} style={{ border: 'none', background: 'transparent', color: 'var(--accent)', cursor: 'pointer', display: 'flex' }}>
<PlusIcon style={{ width: 16 }} />
</button>
<span style={{ fontSize: 13, color: 'var(--text)', minWidth: 20, textAlign: 'center' }}>{qty}</span>
<button type="button" aria-label="کاهش" onClick={() => set(qty - 1)} style={{ border: 'none', background: 'transparent', color: 'var(--accent)', cursor: 'pointer', display: 'flex' }}>
{qty > 1 ? <MinusIcon style={{ width: 16 }} /> : <TrashRed size={16} color="var(--accent)" />}
</button>
</div>
);
return (
<div style={{ marginTop: 16 }}>
{/* پذیرش کننده — tauri UserTick + کاربر لاگین‌شده */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 28 }}>
<UserTick color="var(--text-2)" size={18} />
<span style={{ fontSize: 14, color: 'var(--text-2)' }}>پذیرش کننده: {userName || '—'}</span>
</div>
{/* تاریخ و ساعت پذیرش */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 24 }}>
<div>
<span style={fieldLabel}>تاریخ پذیرش</span>
<PersianDateInput value={dateISO} onChange={setDateISO} />
</div>
<div>
<span style={fieldLabel}>ساعت پذیرش</span>
<div className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 8, height: 48, border: '1px solid var(--border-2)', borderRadius: 4, padding: '0 8px' }}>
<ClockP color="var(--text-2)" size={18} />
<input
type="time"
className="time-input-plain"
aria-label="ساعت پذیرش"
value={time}
onChange={(e) => setTime(e.target.value)}
style={{ border: 'none', outline: 'none', background: 'transparent', fontSize: '0.875rem', color: 'inherit', flex: 1 }}
/>
</div>
</div>
</div>
{/* انتخاب بخش */}
<div style={{ marginBottom: 16 }}>
<span style={fieldLabel}>انتخاب بخش</span>
<SearchableSelect inputId="section-select" options={sectionOptions} value={sectionUuid} onChange={v => { setSectionUuid(v ? String(v) : ''); setItemUuid(''); }} placeholder="انتخاب کنید..." />
</div>
{/* انتخاب سرویس + قیمت + افزودن */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<div style={{ width: '50%' }}>
<span style={fieldLabel}>انتخاب سرویس</span>
<SearchableSelect inputId="service-select" options={itemOptions} value={itemUuid} onChange={v => setItemUuid(v ? String(v) : '')} placeholder="جستجو و انتخاب سرویس..." />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 20, flexShrink: 0 }}>
<FilesServiceAddCard color="var(--text-2)" />
<span style={{ fontSize: 13, color: 'var(--text)' }}>
قیمت: {currentItem ? formatRial(currentItem.price_rials) : '—'}
</span>
</div>
<button type="button" aria-label="افزودن سرویس" onClick={addService} disabled={!itemUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: 'var(--primary)', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 20 }}>
<PlusIcon style={{ width: 18, color: 'var(--on-primary)' }} />
</button>
</div>
{/* خدمات انتخاب شده */}
{selectedServices.length > 0 && (
<div style={{ border: '1px dashed var(--primary-soft2)', borderRadius: 8, padding: 16, marginBottom: 16 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginBottom: 16 }}>خدمات انتخاب شده</span>
{selectedServices.map(item => (
<div key={item.uuid} style={{ display: 'flex', alignItems: 'center', gap: 24, padding: '8px 0' }}>
<span style={{ fontSize: 14, color: 'var(--text)', minWidth: 120 }}>{item.name}</span>
{counter(item.qty, (q) => setServiceQty(item.uuid, q))}
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text)' }}>قیمت: {formatRial(item.price * item.qty)}</span>
</div>
))}
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginTop: 16 }}>مجموع قیمت خدمات: {formatRial(servicesTotal)}</span>
</div>
)}
{/* پرسنل */}
<div style={{ marginBottom: 16 }}>
<span style={fieldLabel}>پرسنل</span>
<SearchableSelect inputId="staff-select" options={staffOptions} value={staffUuid} onChange={v => setStaffUuid(v ? String(v) : '')} placeholder="انتخاب کنید..." isClearable />
</div>
{/* انتخاب کالای مصرفی */}
<div style={{ marginBottom: 16 }}>
<span style={{ fontSize: 14, color: 'var(--text)', display: 'block', marginBottom: 8 }}>انتخاب کالای مصرفی</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 313, maxWidth: '60%' }}>
<SearchableSelect inputId="consumable-select" options={consumableOptions} value={consumableUuid} onChange={v => setConsumableUuid(v ? String(v) : '')} placeholder="انتخاب کنید..." />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
<FilesServiceAddCard color="var(--text-2)" />
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>
قیمت: {currentConsumable ? formatRial(currentConsumable.price) : '—'}
</span>
</div>
<button type="button" aria-label="افزودن کالا" onClick={addConsumable} disabled={!consumableUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: 'var(--primary)', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<PlusIcon style={{ width: 18, color: 'var(--on-primary)' }} />
</button>
</div>
</div>
{/* کالاهای انتخاب شده — باکس خط‌چین tauri */}
<div style={{ border: '1px dashed var(--primary-soft2)', borderRadius: 8, padding: 16, marginBottom: 16 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginBottom: 16 }}>کالاهای انتخاب شده</span>
{selectedConsumables.length === 0 ? (
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>کالایی انتخاب نشده است</span>
) : selectedConsumables.map(item => (
<div key={item.uuid} style={{ display: 'flex', alignItems: 'center', gap: 24, padding: '8px 0' }}>
<span style={{ fontSize: 14, color: 'var(--text)', minWidth: 120 }}>{item.name}</span>
{counter(item.qty, (q) => setConsumableQty(item.uuid, q))}
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text)' }}>قیمت: {formatRial(item.price * item.qty)}</span>
</div>
))}
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginTop: 16 }}>مجموع قیمت کالاها: {formatRial(consumablesTotal)}</span>
</div>
{/* انتخاب پکیج */}
<div style={{ marginBottom: 16 }}>
<span style={fieldLabel}>انتخاب پکیج</span>
<SearchableSelect inputId="package-select" options={packageOptions} value={packageUuid} onChange={v => setPackageUuid(v ? String(v) : '')} placeholder="انتخاب کنید..." isClearable />
</div>
{/* قیمت ویزیت */}
<div style={{ marginBottom: 16 }}>
<span style={fieldLabel}>
قیمت ویزیت (تومان){requireVisit && <span style={{ color: 'var(--danger)' }}> *</span>}
</span>
<input
className="input" type="text" inputMode="numeric" dir="ltr" aria-label="قیمت ویزیت"
aria-invalid={!!visitPriceError}
value={visitPrice}
onChange={(e) => { setVisitPrice(digitsOnly(e.target.value)); setVisitPriceError(''); }}
/>
{visitPriceError && (
<span style={{ fontSize: 12, color: 'var(--danger)', display: 'block', marginTop: 4 }}>{visitPriceError}</span>
)}
</div>
{/* بیمه — منطق موجود NewSessionPage؛ فقط وقتی سرویس تحت پوشش یا بیمار بیمه دارد */}
{showInsurance && (
<div style={{ marginBottom: 16 }}>
<span style={fieldLabel}>بیمه</span>
{/* نوع خدمت فقط وقتی چند نوع فعال است پرسیده می‌شود؛ وگرنه همان نوعِ فعال. */}
{needsCategoryChoice && (
<div style={{ marginBottom: 12 }}>
<span style={fieldLabel}>نوع خدمت</span>
<SearchableSelect
inputId="service-category-select"
options={enabledCategories.map(c => ({ value: c.key, label: c.label }))}
value={serviceCategory || null}
onChange={v => applyServiceCategory(v ? String(v) : '')}
placeholder="انتخاب نوع خدمت"
isClearable
/>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
<div>
<span style={fieldLabel}>بیمه پایه</span>
<SearchableSelect inputId="base-insurance-select" options={baseOpts} value={baseId} onChange={v => applyBase(v ? String(v) : '')} placeholder="بدون بیمه پایه" isClearable />
</div>
<div>
<span style={fieldLabel}>بیمه تکمیلی</span>
<SearchableSelect inputId="supp-insurance-select" options={suppOpts} value={suppId} onChange={v => applySupp(v ? String(v) : '')} placeholder="بدون بیمه تکمیلی" isClearable />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<span style={fieldLabel}>تخفیف بیمه پایه (%)</span>
<input className="input" type="text" inputMode="numeric" dir="ltr" aria-label="تخفیف بیمه پایه" value={basePercent} onChange={(e) => setBasePercent(digitsOnly(e.target.value, 3))} />
</div>
<div>
<span style={fieldLabel}>تخفیف تکمیلی (%)</span>
<input className="input" type="text" inputMode="numeric" dir="ltr" aria-label="تخفیف تکمیلی" value={suppPercent} onChange={(e) => setSuppPercent(digitsOnly(e.target.value, 3))} />
</div>
</div>
</div>
)}
{/* یادداشت */}
<div style={{ marginBottom: 16 }}>
<span style={fieldLabel}>یادداشت</span>
<textarea className="input" rows={3} dir="rtl" placeholder="یادداشت پزشک..." value={notes} onChange={(e) => setNotes(e.target.value)} />
</div>
{/* خلاصه مبلغ */}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: 'var(--text-3)' }}><span>ویزیت</span><span>{formatRial(afterSupp)}</span></div>
{servicesTotal > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: 'var(--text-3)' }}><span>سهم بیمار خدمات</span><span>{formatRial(servicesPatient)}</span></div>}
{consumablesTotal > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: 'var(--text-3)' }}><span>کالاهای مصرفی</span><span>{formatRial(consumablesTotal)}</span></div>}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 15, fontWeight: 700 }}>
<span>مبلغ نهایی (سهم بیمار)</span>
<span style={{ color: 'var(--primary)' }}>{formatRial(finalPrice)}</span>
</div>
</div>
{/* ACTIONS — tauri: انصراف / ایجاد سرویس */}
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginTop: 16, width: '100%', maxWidth: 340, margin: '16px auto 0' }}>
<button type="button" style={{ ...ghostBtn, width: 164 }} onClick={onCancel}>انصراف</button>
<button type="button" style={{ ...primaryBtn, width: 164 }} onClick={submit} disabled={createMut.isPending}>
{createMut.isPending ? 'در حال ذخیره...' : (isEdit ? 'ذخیره تغییرات' : 'ایجاد سرویس')}
</button>
</div>
</div>
);
}