SessionStepper copied MUI's LTR connector offsets (left: -50%), which in the RTL admin drew each connector away from its previous step — a stray line ran off the card edge past the last step and the first two steps had no line between them. Swap the offsets so connectors extend toward the previous step on the right, matching tauri's MUI-flipped rendering. The acceptance-time field showed two clock icons (custom ClockP + the browser's native picker indicator); hide the native one like the tauri source, which has a single ClockField adornment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
427 lines
27 KiB
TypeScript
427 lines
27 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { useQuery, useMutation } 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 } 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';
|
|
|
|
interface Contract { uuid: string; insurance_id: number; insurance_name: string | null; insurance_kind: string | null; coverage_percent: number; franchise_rials: number; annual_ceiling_rials: number | null }
|
|
interface CoverageRow { service_item_uuid: string | null; covered: boolean; coverage_percent: number | null; franchise_rials: number | null; ceiling_rials: number | null }
|
|
interface Rule { covered: boolean; percent: number; franchise: number; ceiling: 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 }
|
|
|
|
// آینهی BillingCalculator سمت سرور: سهم بیمار یک خدمت با پوشش پایه/مکمل.
|
|
function patientShareOf(total: number, base: Rule | null, supp: Rule | null): number {
|
|
let baseShare = 0;
|
|
let remaining = total;
|
|
if (base && base.covered) {
|
|
baseShare = Math.round(total * (base.percent / 100));
|
|
if (base.ceiling !== null) baseShare = Math.min(baseShare, base.ceiling);
|
|
remaining = total - baseShare;
|
|
}
|
|
let suppShare = 0;
|
|
if (supp && supp.covered) {
|
|
suppShare = Math.round(remaining * (supp.percent / 100));
|
|
if (supp.ceiling !== null) suppShare = Math.min(suppShare, supp.ceiling);
|
|
remaining = remaining - suppShare;
|
|
}
|
|
const franchise = (base?.franchise ?? 0) + (supp?.franchise ?? 0);
|
|
return Math.min(remaining + franchise, total);
|
|
}
|
|
|
|
const todayISO = () => new Date().toISOString().slice(0, 10);
|
|
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: '#6B7280', display: 'block' };
|
|
const primaryBtn: React.CSSProperties = { background: '#5559CE', color: '#fff', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
|
|
const ghostBtn: React.CSSProperties = { background: 'transparent', color: '#5559CE', border: '1px solid #5559CE', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
|
|
|
|
interface Props {
|
|
recordUuid: string;
|
|
profile?: PatientProfile | null;
|
|
onCreated: (sessionUuid: string) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
/**
|
|
* گام «ایجاد سرویس» — پورت tauri Step1Details با دیتای واقعی:
|
|
* تاریخ/ساعت پذیرش، بخش/سرویس/پرسنل، کالای مصرفی با شمارنده، پکیج، و بلوک بیمهی
|
|
* موجودِ NewSessionPage (نمایش شرطی: سرویسِ تحت پوشش بیمه یا بیمه در پروفایل بیمار).
|
|
*/
|
|
export default function CreateStep({ recordUuid, profile, onCreated, onCancel }: Props) {
|
|
const userName = useAuthStore((s) => s.userName);
|
|
|
|
// ── 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 }[]>([]);
|
|
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 [baseId, setBaseId] = useState('');
|
|
const [suppId, setSuppId] = useState('');
|
|
const [basePercent, setBasePercent] = useState('0');
|
|
const [suppPercent, setSuppPercent] = useState('0');
|
|
const [notes, setNotes] = useState('');
|
|
|
|
// ── دادهها ──────────────────────────────────────────────────────────────
|
|
const { data: sectionsData } = useQuery<ApiResponse<ServiceSection[]>>({
|
|
queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'),
|
|
});
|
|
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 } }>({
|
|
queryKey: ['insurance-pricing'], queryFn: () => api.get('/api/v1/insurance-pricing'),
|
|
});
|
|
const freeVisit = (pricingData as any)?.data?.free_visit_price_rials ?? 0;
|
|
|
|
useEffect(() => {
|
|
if (freeVisit > 0 && (!visitPrice || visitPrice === '0')) setVisitPrice(String(freeVisit));
|
|
}, [freeVisit]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
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 = (itemsData?.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 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): 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 };
|
|
return {
|
|
covered: true,
|
|
percent: ov?.coverage_percent ?? contract.coverage_percent,
|
|
franchise: ov?.franchise_rials ?? contract.franchise_rials,
|
|
ceiling: ov?.ceiling_rials ?? contract.annual_ceiling_rials,
|
|
};
|
|
};
|
|
|
|
const coverageOf = (id: string): number => contracts.find(c => String(c.insurance_id) === id)?.coverage_percent ?? 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 showInsurance = 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 }]);
|
|
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));
|
|
};
|
|
|
|
// ── قیمتها (آینهی سرور) ────────────────────────────────────────────────
|
|
const visit = 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;
|
|
if (!x.insured) return sum + total;
|
|
return sum + patientShareOf(total, ruleFor(baseContract, baseCoverage, x.uuid), ruleFor(suppContract, suppCoverage, x.uuid));
|
|
}, 0),
|
|
[selectedServices, baseContract, suppContract, baseCoverage, suppCoverage], // 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) => api.post(`/api/v1/patient/${recordUuid}/session`, body),
|
|
onSuccess: (res: any) => {
|
|
toast.success('مراجعه ثبت شد');
|
|
onCreated(res?.data?.uuid as string);
|
|
},
|
|
onError: (e: Error) => toast.error(e.message),
|
|
});
|
|
|
|
const submit = () => {
|
|
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) } : {}),
|
|
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 className="dark:border-[#404040]" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 74, height: 27, border: '1px solid #d1d1d1', borderRadius: 8, padding: '0 4px', gap: 4 }}>
|
|
<button type="button" aria-label="افزایش" onClick={() => set(qty + 1)} style={{ border: 'none', background: 'transparent', color: '#FF7A45', cursor: 'pointer', display: 'flex' }}>
|
|
<PlusIcon style={{ width: 16 }} />
|
|
</button>
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, color: '#525252', minWidth: 20, textAlign: 'center' }}>{qty}</span>
|
|
<button type="button" aria-label="کاهش" onClick={() => set(qty - 1)} style={{ border: 'none', background: 'transparent', color: '#FF7A45', cursor: 'pointer', display: 'flex' }}>
|
|
{qty > 1 ? <MinusIcon style={{ width: 16 }} /> : <TrashRed size={16} color="#f17732" />}
|
|
</button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div style={{ marginTop: 16 }}>
|
|
{/* پذیرش کننده — tauri UserTick + کاربر لاگینشده */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 28 }}>
|
|
<UserTick color="#6B7280" size={18} />
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#616161' }}>پذیرش کننده: {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-white dark:bg-[#222433] dark:border-[#404040]" style={{ display: 'flex', alignItems: 'center', gap: 8, height: 48, border: '1px solid #e1e1e1', borderRadius: 4, padding: '0 8px' }}>
|
|
<ClockP color="#6B7280" 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="انتخاب کنید..." isDisabled={!sectionUuid} />
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 20, flexShrink: 0 }}>
|
|
<FilesServiceAddCard color="#6B7280" />
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, color: '#3b3b3b' }}>
|
|
قیمت: {currentItem ? formatRial(currentItem.price_rials) : '—'}
|
|
</span>
|
|
</div>
|
|
<button type="button" aria-label="افزودن سرویس" onClick={addService} disabled={!itemUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: '#5559CE', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 20 }}>
|
|
<PlusIcon style={{ width: 18, color: '#fff' }} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* خدمات انتخاب شده */}
|
|
{selectedServices.length > 0 && (
|
|
<div style={{ border: '1px dashed #C7C9F4', borderRadius: 8, padding: 16, marginBottom: 16 }}>
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', display: 'block', marginBottom: 16 }}>خدمات انتخاب شده</span>
|
|
{selectedServices.map(item => (
|
|
<div key={item.uuid} style={{ display: 'flex', alignItems: 'center', gap: 24, padding: '8px 0' }}>
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#525252', minWidth: 120 }}>{item.name}</span>
|
|
{counter(item.qty, (q) => setServiceQty(item.uuid, q))}
|
|
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, fontWeight: 500, color: '#3b3b3b' }}>قیمت: {formatRial(item.price * item.qty)}</span>
|
|
</div>
|
|
))}
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', 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 className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#525252', 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="#6B7280" />
|
|
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>
|
|
قیمت: {currentConsumable ? formatRial(currentConsumable.price) : '—'}
|
|
</span>
|
|
</div>
|
|
<button type="button" aria-label="افزودن کالا" onClick={addConsumable} disabled={!consumableUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: '#5559CE', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<PlusIcon style={{ width: 18, color: '#fff' }} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* کالاهای انتخاب شده — باکس خطچین tauri */}
|
|
<div style={{ border: '1px dashed #C7C9F4', borderRadius: 8, padding: 16, marginBottom: 16 }}>
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', display: 'block', marginBottom: 16 }}>کالاهای انتخاب شده</span>
|
|
{selectedConsumables.length === 0 ? (
|
|
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>کالایی انتخاب نشده است</span>
|
|
) : selectedConsumables.map(item => (
|
|
<div key={item.uuid} style={{ display: 'flex', alignItems: 'center', gap: 24, padding: '8px 0' }}>
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#525252', minWidth: 120 }}>{item.name}</span>
|
|
{counter(item.qty, (q) => setConsumableQty(item.uuid, q))}
|
|
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, fontWeight: 500, color: '#3b3b3b' }}>قیمت: {formatRial(item.price * item.qty)}</span>
|
|
</div>
|
|
))}
|
|
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', 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}>قیمت ویزیت (تومان)</span>
|
|
<input className="input" type="number" min={0} dir="ltr" aria-label="قیمت ویزیت" value={visitPrice} onChange={(e) => setVisitPrice(e.target.value)} />
|
|
</div>
|
|
|
|
{/* بیمه — منطق موجود NewSessionPage؛ فقط وقتی سرویس تحت پوشش یا بیمار بیمه دارد */}
|
|
{showInsurance && (
|
|
<div style={{ marginBottom: 16 }}>
|
|
<span style={fieldLabel}>بیمه</span>
|
|
<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="number" min={0} max={100} dir="ltr" aria-label="تخفیف بیمه پایه" value={basePercent} onChange={(e) => setBasePercent(e.target.value)} />
|
|
</div>
|
|
<div>
|
|
<span style={fieldLabel}>تخفیف تکمیلی (%)</span>
|
|
<input className="input" type="number" min={0} max={100} dir="ltr" aria-label="تخفیف تکمیلی" value={suppPercent} onChange={(e) => setSuppPercent(e.target.value)} />
|
|
</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 className="dark:border-[#404040]" 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 ? 'در حال ذخیره...' : 'ایجاد سرویس'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|