import React, { useEffect, useMemo, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import type { DiscountRule, DiscountRuleType } from '../types'; import { formatRial, formatDate, tomanToRial, rialToToman, tehranWallClockToUnix } from '../lib/utils'; import Modal from './ui/Modal'; import ConfirmDialog from './ui/ConfirmDialog'; import SearchableSelect from './ui/SearchableSelect'; import PriceInput from './ui/PriceInput'; import PersianDateInput from './ui/PersianDateInput'; import { digitsOnly } from '../lib/utils'; const TYPE_LABELS: Record = { patient_tag: 'تگ بیمار', invoice_amount: 'مبلغ فاکتور', specific_patient: 'بیمار خاص', occasion: 'مناسبتی', service: 'سرویس', visit_count: 'تعداد مراجعات', }; interface Option { uuid: string; name?: string } const unixToIso = (u: number | null): string => { if (!u) return ''; const d = new Date(u * 1000); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; const isoToUnix = (iso: string): number | null => (iso ? tehranWallClockToUnix(iso, '00:00') : null); interface FormState { name: string; type: DiscountRuleType; discount_type: 'percent' | 'fixed'; value: number; // percent (0..100) or toman (fixed) priority: number; combinable: boolean; active: boolean; valid_from: string; // iso valid_to: string; // iso target_tag_uuid: string; target_record_uuid: string; target_service_item_uuid: string; min_amount_toman: number; min_visit_count: number; occasion_kind: '' | 'birthday'; } const emptyForm = (): FormState => ({ name: '', type: 'invoice_amount', discount_type: 'percent', value: 0, priority: 0, combinable: false, active: true, valid_from: '', valid_to: '', target_tag_uuid: '', target_record_uuid: '', target_service_item_uuid: '', min_amount_toman: 0, min_visit_count: 0, occasion_kind: '', }); const fromRule = (r: DiscountRule): FormState => ({ name: r.name, type: r.type, discount_type: r.discount_type, value: r.discount_type === 'fixed' ? rialToToman(r.value) : r.value, priority: r.priority, combinable: r.combinable, active: r.active, valid_from: unixToIso(r.valid_from), valid_to: unixToIso(r.valid_to), target_tag_uuid: r.target_tag_uuid ?? '', target_record_uuid: r.target_record_uuid ?? '', target_service_item_uuid: r.target_service_item_uuid ?? '', min_amount_toman: r.min_amount_rials ? rialToToman(r.min_amount_rials) : 0, min_visit_count: r.min_visit_count ?? 0, occasion_kind: r.occasion_kind === 'birthday' ? 'birthday' : '', }); function labelStyle(): React.CSSProperties { return { fontSize: 12.5, color: 'var(--text-3)', display: 'block', marginBottom: 6 }; } export default function DiscountTab() { const qc = useQueryClient(); const [modal, setModal] = useState<'create' | DiscountRule | null>(null); const [toDelete, setToDelete] = useState(null); const { data, isLoading } = useQuery({ queryKey: ['admin-discount-rules'], queryFn: () => api.get>('/api/v1/admin/discount-rules'), }); const rules: DiscountRule[] = (data?.data as any)?.data ?? (data?.data as any) ?? []; const removeMut = useMutation({ mutationFn: (uuid: string) => api.delete(`/api/v1/admin/discount-rules/${uuid}`), onSuccess: () => { toast.success('قانون حذف شد'); setToDelete(null); qc.invalidateQueries({ queryKey: ['admin-discount-rules'] }); }, onError: (e: Error) => toast.error(e.message), }); const discountDisplay = (r: DiscountRule) => r.discount_type === 'percent' ? `${r.value}٪` : formatRial(r.value); return (
قوانین تخفیف عمومی — بر اساس تگ، مبلغ، بیمار، مناسبت، سرویس یا تعداد مراجعه
{isLoading ? (
در حال بارگذاری...
) : rules.length === 0 ? (
هنوز قانونی تعریف نشده است.
) : (
{rules.map((r) => ( ))}
نام نوع تخفیف اولویت ترکیب‌پذیر وضعیت
{r.name} {TYPE_LABELS[r.type]} {discountDisplay(r)} {r.priority} {r.combinable ? 'بله' : 'خیر'} {r.active ? 'فعال' : 'غیرفعال'}
)} {modal && ( setModal(null)} onSaved={() => { setModal(null); qc.invalidateQueries({ queryKey: ['admin-discount-rules'] }); }} /> )} toDelete && removeMut.mutate(toDelete.uuid)} onCancel={() => setToDelete(null)} />
); } function RuleModal({ initial, onClose, onSaved }: { initial: DiscountRule | null; onClose: () => void; onSaved: () => void }) { const [f, setF] = useState(initial ? fromRule(initial) : emptyForm()); const set = (k: K, v: FormState[K]) => setF((s) => ({ ...s, [k]: v })); const tagsQ = useQuery({ queryKey: ['tenant-tags'], queryFn: () => api.get>('/api/v1/tenant-tags'), enabled: f.type === 'patient_tag', }); const sectionsQ = useQuery({ queryKey: ['service-sections'], queryFn: () => api.get>('/api/v1/service-sections'), enabled: f.type === 'service', }); const [sectionUuid, setSectionUuid] = useState(''); const itemsQ = useQuery({ queryKey: ['service-items', sectionUuid], queryFn: () => api.get>(`/api/v1/service-items/${sectionUuid}`), enabled: f.type === 'service' && !!sectionUuid, }); const tags = (tagsQ.data?.data as any)?.data ?? (tagsQ.data?.data as any) ?? []; const sections = (sectionsQ.data?.data as any)?.data ?? (sectionsQ.data?.data as any) ?? []; const items = (itemsQ.data?.data as any)?.data ?? (itemsQ.data?.data as any) ?? []; const save = useMutation({ mutationFn: () => { const body: Record = { name: f.name.trim(), type: f.type, discount_type: f.discount_type, value: f.discount_type === 'fixed' ? tomanToRial(f.value) : f.value, priority: f.priority, combinable: f.combinable, active: f.active, valid_from: isoToUnix(f.valid_from), valid_to: isoToUnix(f.valid_to), target_tag_uuid: f.type === 'patient_tag' ? (f.target_tag_uuid || null) : null, target_record_uuid: f.type === 'specific_patient' ? (f.target_record_uuid.trim() || null) : null, target_service_item_uuid: f.type === 'service' ? (f.target_service_item_uuid || null) : null, min_amount_rials: f.type === 'invoice_amount' ? tomanToRial(f.min_amount_toman) : null, min_visit_count: f.type === 'visit_count' ? f.min_visit_count : null, occasion_kind: f.type === 'occasion' ? (f.occasion_kind || null) : null, }; return initial ? api.patch(`/api/v1/admin/discount-rules/${initial.uuid}`, body) : api.post('/api/v1/admin/discount-rules', body); }, onSuccess: () => { toast.success('قانون ذخیره شد'); onSaved(); }, onError: (e: Error) => toast.error(e.message), }); const canSave = f.name.trim().length > 0 && f.value >= 0; return (
set('name', e.target.value)} placeholder="مثال: بیماران VIP" />
({ value: t, label: TYPE_LABELS[t] }))} value={f.type} onChange={(v) => set('type', (v as DiscountRuleType) || 'invoice_amount')} height={38} />
set('discount_type', (v as 'percent' | 'fixed') || 'percent')} height={38} />
{f.discount_type === 'percent' ? set('value', Number(digitsOnly(e.target.value, 3)) || 0)} /> : set('value', v)} />}
set('priority', Number(digitsOnly(e.target.value)) || 0)} />
{/* target فیلد پویا بر اساس نوع */} {f.type === 'patient_tag' && (
({ value: t.uuid, label: t.name ?? '' }))} value={f.target_tag_uuid || null} onChange={(v) => set('target_tag_uuid', v ? String(v) : '')} placeholder="انتخاب تگ" isLoading={tagsQ.isLoading} isClearable height={38} />
)} {f.type === 'invoice_amount' && (
set('min_amount_toman', v)} />
)} {f.type === 'specific_patient' && (
set('target_record_uuid', e.target.value)} placeholder="record uuid" />
)} {f.type === 'service' && (
({ value: s.uuid, label: s.name ?? '' }))} value={sectionUuid || null} onChange={(v) => { setSectionUuid(v ? String(v) : ''); set('target_service_item_uuid', ''); }} placeholder="انتخاب بخش" isLoading={sectionsQ.isLoading} isClearable height={38} />
({ value: s.uuid, label: s.name ?? '' }))} value={f.target_service_item_uuid || null} onChange={(v) => set('target_service_item_uuid', v ? String(v) : '')} placeholder="انتخاب سرویس" isDisabled={!sectionUuid} isLoading={itemsQ.isLoading} isClearable height={38} />
)} {f.type === 'visit_count' && (
set('min_visit_count', Number(digitsOnly(e.target.value)) || 0)} />
)} {f.type === 'occasion' && (
set('occasion_kind', (v as '' | 'birthday'))} height={38} />
)} {/* بازه‌ی اعتبار (اختیاری؛ برای مناسبتی/موقت) */}
set('valid_from', v)} />
set('valid_to', v)} />
); }