import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ChevronDownIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../../lib/api'; import type { ApiResponse } from '../../lib/api'; import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { formatRial, formatDateTime, tomanToRial, rialToToman } from '../../lib/utils'; import type { DiscountSuggestion } from '../../types'; import type { SessionCardData } from '../SessionServiceCard'; import SearchableSelect from '../ui/SearchableSelect'; import PersianDateInput from '../ui/PersianDateInput'; import PriceInput from '../ui/PriceInput'; import Modal from '../ui/Modal'; import ConfirmDialog from '../ui/ConfirmDialog'; import { Step2PaymentCard, FilesServiceBalanceWallet, TrashRed } from '../icons/FilesServiceIcons'; /** روش‌های پرداخت — همان چهار گزینه‌ی آکاردئون tauri Step2Payment. */ const METHODS: { key: string; label: string }[] = [ { key: 'wallet', label: 'پرداخت از طریق کیف پول' }, { key: 'pos', label: 'پرداخت از طریق کارت خوان' }, { key: 'cash', label: 'پرداخت نقدی' }, { key: 'card', label: 'کارت به کارت' }, ]; export const METHOD_LABELS: Record = { wallet: 'پرداخت از کیف پول', pos: 'پرداخت کارتخوان', cash: 'پرداخت نقدی', card: 'کارت به کارت', }; const todayISO = () => new Date().toISOString().slice(0, 10); /** YYYY-MM-DD → unix (ظهر همان روز تا با هر timezone یک روز بماند) */ const isoToUnix = (iso: string) => Math.floor(new Date(`${iso}T12:00:00`).getTime() / 1000); const fieldLabel: React.CSSProperties = { fontSize: 14, color: '#6B7280', marginBottom: 8, 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; session: SessionCardData; walletBalance: number; onContinue: () => void; onCancel: () => void; } /** * گام «پرداخت» — پورت tauri Step2Payment با دیتای واقعی: * تخفیف تسویه (PATCH /session/{uuid}) + پرداخت چندتکه (POST /session/{uuid}/payments). * بین NewSessionPage (ویزارد سه‌گامه) و SessionPaymentPage (دوگامه) مشترک است. */ export default function PaymentStep({ recordUuid, session, walletBalance, onContinue, onCancel }: Props) { const qc = useQueryClient(); const sessionUuid = session.uuid; const [discountType, setDiscountType] = useState(''); const [discountValue, setDiscountValue] = useState(0); const [paymentDate, setPaymentDate] = useState(todayISO()); const [expanded, setExpanded] = useState(null); const [amount, setAmount] = useState(0); const invalidate = () => { qc.invalidateQueries({ queryKey: ['patient-sessions', recordUuid] }); qc.invalidateQueries({ queryKey: ['patient-wallet', recordUuid] }); qc.invalidateQueries({ queryKey: ['discount-suggestions', sessionUuid] }); }; const discountMut = useMutation({ mutationFn: (body: object) => api.patch(`/api/v1/session/${sessionUuid}`, body), onSuccess: () => { invalidate(); toast.success('تخفیف به‌روزرسانی شد'); }, onError: (e: Error) => toast.error(e.message), }); const payMut = useMutation({ mutationFn: (body: object) => api.post(`/api/v1/session/${sessionUuid}/payments`, body), onSuccess: () => { invalidate(); setAmount(0); toast.success('پرداخت ثبت شد'); }, onError: (e: Error) => toast.error(e.message), }); // ویرایش/حذف پرداخت const [editPay, setEditPay] = useState<{ uuid: string; method: string; amountToman: number } | null>(null); const [delPayUuid, setDelPayUuid] = useState(null); const editPayMut = useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: object }) => api.patch(`/api/v1/session/${sessionUuid}/payments/${uuid}`, body), onSuccess: () => { invalidate(); setEditPay(null); toast.success('پرداخت ویرایش شد'); }, onError: (e: Error) => toast.error(e.message), }); const delPayMut = useMutation({ mutationFn: (uuid: string) => api.delete(`/api/v1/session/${sessionUuid}/payments/${uuid}`), onSuccess: () => { invalidate(); setDelPayUuid(null); toast.success('پرداخت حذف شد'); }, onError: (e: Error) => toast.error(e.message), }); const suggestionsQ = useQuery({ queryKey: ['discount-suggestions', sessionUuid], queryFn: () => api.get>(`/api/v1/session/${sessionUuid}/discount-suggestions`), }); const suggestions: DiscountSuggestion[] = (suggestionsQ.data?.data as any)?.data ?? (suggestionsQ.data?.data as any) ?? []; const applyDiscount = () => { if (!discountType || discountValue <= 0) return; // percent درصد است (بدون تبدیل)؛ fixed مبلغ تومان است → ریال. const value = discountType === 'fixed' ? tomanToRial(discountValue) : discountValue; discountMut.mutate({ discount_type: discountType, discount_value: value }); }; const applyRule = (ruleUuid: string) => { discountMut.mutate({ discount_rule_uuid: ruleUuid }); }; const removeDiscount = () => { setDiscountType(''); setDiscountValue(0); discountMut.mutate({ discount_rule_uuid: null }); }; const submitPayment = (method: string) => { if (amount <= 0) return; payMut.mutate({ method, amount_rials: tomanToRial(amount), paid_at: isoToUnix(paymentDate) }); }; const finalPrice = session.final_price_rials ?? 0; const discountRials = session.discount_rials ?? 0; const payments = session.payments ?? []; const appliedRuleLabel = session.applied_discount_rule_label ?? null; // تفکیک بیمه و مانده را سرور می‌دهد؛ محاسبه‌ی محلی باعث واگرایی با فاکتور می‌شد. 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; const payable = session.remaining_rials !== undefined ? session.remaining_rials + (session.paid_total_rials ?? 0) : Math.max(0, finalPrice - discountRials); const debt = session.remaining_rials ?? session.patient_debt_rials ?? 0; return ( <> {/* هزینه سرویس */}
هزینه سرویس: {formatRial(finalPrice)}
{/* تخفیف‌های پیشنهادی (قوانین تخفیف) */} {suggestions.length > 0 && (
تخفیف‌های قابل اعمال:
{suggestions.map((s) => (
{s.rule_name} — {formatRial(s.discount_rials)} {' '}({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})
))}
)} {/* تخفیف — port of tauri DiscountInput (نوع + مقدار + ثبت) */} تخفیف:
setDiscountType(v ? String(v) : '')} placeholder="مبلغ تخفیف" />
حذف تخفیف
{/* خلاصه‌ی مبلغ: قبل از تخفیف / تخفیف / نهایی / منبع */}
{hasInsurance && ( <>
هزینه کل خدمات: {formatRial(grossTotal)}
{baseInsurance > 0 && (
سهم بیمه پایه: {formatRial(baseInsurance)}
)} {suppInsurance > 0 && (
سهم بیمه تکمیلی: {formatRial(suppInsurance)}
)} )}
{hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'} {formatRial(finalPrice)}
مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}: {formatRial(discountRials)}
مبلغ نهایی قابل پرداخت: {formatRial(payable)}
{/* تاریخ پرداخت */} تاریخ پرداخت {/* روش‌های پرداخت */} انتخاب روش های پرداخت:
موجودی کیف پول:
{formatRial(walletBalance)}
مبلغ باقی‌مانده: {formatRial(debt)}
{/* آکاردئون چهار روش — باز شدن هر روش: مبلغ + ثبت پرداخت */}
{METHODS.map((m) => { const open = expanded === m.key; return (
{open && (
)}
); })}
{/* پرداخت شده‌ها — باکس خط‌چین tauri */}
پرداخت شده ها: {payments.length === 0 ? ( پرداختی ثبت نشده است ) : (
{payments.map((p) => (
{METHOD_LABELS[p.method] ?? p.method} مبلغ : {formatRial(p.amount_rials)} {p.method !== 'wallet' && ( )}
{(p.paid_at ?? p.created_at) && {formatDateTime(p.paid_at ?? p.created_at!)}} {p.created_by_name && ثبت: {p.created_by_name}}
))}
)}
مبلغ باقی‌مانده : {formatRial(debt)}
{/* ACTIONS */}
{editPay && ( setEditPay(null)}>
m.key !== 'wallet').map((m) => ({ value: m.key, label: m.label }))} value={editPay.method} onChange={(v) => setEditPay((s) => s && { ...s, method: v ? String(v) : s.method })} height={40} />
setEditPay((s) => s && { ...s, amountToman: v })} />
)} delPayUuid && delPayMut.mutate(delPayUuid)} onCancel={() => setDelPayUuid(null)} /> ); }