import React, { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import SettingsLayout from '../components/layout/SettingsLayout'; import { z } from 'zod'; import { toast } from 'sonner'; import { ArrowUpCircleIcon, ArrowDownCircleIcon, DevicePhoneMobileIcon, ClockIcon, CheckCircleIcon, XCircleIcon, BellAlertIcon, ChatBubbleLeftEllipsisIcon, BanknotesIcon, } from '@heroicons/react/24/outline'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import type { SmsWalletBalance, SmsWalletLog, SmsSettings } from '../types'; import { usePaymentConfig } from '../hooks/usePaymentConfig'; import { formatRial, formatNumber, formatDateTime, tomanToRial } from '../lib/utils'; import Modal from '../components/ui/Modal'; import Pagination from '../components/ui/Pagination'; import SearchableSelect from '../components/ui/SearchableSelect'; import PageHeader from '../components/ui/PageHeader'; import FeatureGate from '../components/ui/FeatureGate'; import { numericField } from '../lib/forms'; const chargeSchema = z.object({ amount_rials: z.coerce.number().min(1000, 'حداقل مبلغ ۱٬۰۰۰ تومان است'), }); type ChargeForm = z.infer; const EMPTY_LOGS: SmsWalletLog[] = []; const REMINDER_HOUR_OPTIONS = [1, 2, 3, 4, 6, 12, 24, 48]; const POST_VISIT_VARS: { key: string; label: string }[] = [ { key: 'patient_name', label: 'نام بیمار' }, { key: 'doctor', label: 'نام پزشک' }, { key: 'clinic', label: 'نام کلینیک' }, { key: 'date', label: 'تاریخ ویزیت' }, ]; function SmsWalletPageInner() { const qc = useQueryClient(); const [chargeOpen, setChargeOpen] = useState(false); const [gateway, setGateway] = useState<'mellat' | 'sep'>('mellat'); const [logPage, setLogPage] = useState(1); const { data: balanceData, isLoading: balanceLoading } = useQuery>({ queryKey: ['sms-wallet-balance'], queryFn: () => api.get('/api/v1/sms/wallet/balance'), }); const { data: logsData, isLoading: logsLoading } = useQuery>({ queryKey: ['sms-wallet-logs', logPage], queryFn: () => api.get(`/api/v1/sms/wallet/logs?page=${logPage}&limit=20`), }); const { data: settingsData } = useQuery>({ queryKey: ['sms-settings'], queryFn: () => api.get('/api/v1/sms/settings'), }); const { isTestMode } = usePaymentConfig(); const balance = balanceData?.data; const logs = logsData?.data ?? EMPTY_LOGS; const total = logsData?.meta?.totalRecords ?? 0; const settings = settingsData?.data; const chargeForm = useForm({ resolver: zodResolver(chargeSchema) }); const watchAmount = chargeForm.watch('amount_rials'); const chargeMutation = useMutation({ mutationFn: ({ amount_rials }: ChargeForm) => api.post<{ data: { payment_url: string } }>('/api/v1/sms/wallet/charge', { gateway, amount_rials: tomanToRial(amount_rials), frontend_address: `${window.location.origin}${window.location.pathname}`, }), onSuccess: (res: any) => { const url = res?.data?.pay_url ?? res?.data?.redirect_url ?? res?.data?.payment_url; if (url) window.location.href = url; else toast.error('خطا در دریافت لینک پرداخت'); }, onError: (e: any) => toast.error(e.message), }); const [localSettings, setLocalSettings] = useState(null); const currentSettings = localSettings ?? settings; // با هر بار تازه‌شدن داده‌ی سرور، ویرایش محلی صفر شود تا وضعیت واقعی (مثل وضعیت تأیید متن) نمایش داده شود. useEffect(() => { setLocalSettings(null); }, [settingsData]); const saveMutation = useMutation({ mutationFn: (body: SmsSettings) => api.patch('/api/v1/sms/settings', body), onSuccess: () => { setLocalSettings(null); qc.invalidateQueries({ queryKey: ['sms-settings'] }); toast.success('تنظیمات ذخیره شد'); }, onError: (e: any) => toast.error(e.message), }); const smsCount = balance?.estimated_sms_count ?? 0; const isLowBalance = smsCount < 10; return ( <> {/* ردیف بالا: موجودی + آمار */}
{/* کارت موجودی — gradient */}
موجودی کیف پول
{isLowBalance ? 'موجودی کم' : 'فعال'}
{balanceLoading ?
: ( <>
{formatRial(balance?.balance_rials ?? 0)}
معادل {formatNumber(smsCount)} پیامک {balance?.sms_price_rials ? ` (هر پیامک ${formatRial(balance.sms_price_rials)})` : ''}
)}
{/* آمار پیامک */}
{formatNumber(smsCount)}
پیامک باقی‌مانده
{currentSettings?.reminder_enabled ? `${currentSettings.reminder_hours_before} ساعت` : '—'}
یادآوری قبل از نوبت
{formatRial(balance?.sms_price_rials ?? 0)}
قیمت هر پیامک
{/* وضعیت سرویس */}
وضعیت سرویس‌ها
{[ { label: 'یادآوری نوبت', on: currentSettings?.reminder_enabled ?? false }, { label: 'پیامک بعد از ویزیت', on: currentSettings?.post_visit_enabled ?? false }, ].map(({ label, on }) => (
{label} {on ? 'فعال' : 'غیرفعال'}
))}
{/* تنظیمات ارسال — full width */}
تنظیمات ارسال پیامک
{currentSettings ? (
{/* ردیف: یادآوری قبل از نوبت */}
یادآوری قبل از نوبت
پیامک یادآور به بیمار چند ساعت قبل از وقت نوبت ارسال می‌شود
{currentSettings.reminder_enabled && (
({ value: h, label: `${h} ساعت قبل` }))} value={currentSettings.reminder_hours_before} onChange={(v) => setLocalSettings({ ...currentSettings, reminder_hours_before: Number(v) || 0 })} placeholder="ساعت" />
)}
{/* ردیف: پیامک بعد از ویزیت */}
{/* header ردیف */}
پیامک بعد از ویزیت
متن تشکر پس از پایان مراجعه برای بیمار ارسال می‌شود
{/* محتوای باز‌شونده */} {currentSettings.post_visit_enabled && (
متن پیامک