import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { PaginatedResponse } from '../lib/api'; import type { SubscriptionPlan, SubscriptionPeriod } from '../types'; import { formatRial, formatNumber, formatDate } from '../lib/utils'; import Modal from '../components/ui/Modal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import PageHeader from '../components/ui/PageHeader'; import Pagination from '../components/ui/Pagination'; import { numericField } from '../lib/forms'; // ── Types ───────────────────────────────────────────────────────────────── interface ReportRow { uuid: string; entityType: string; entityId: number; isTrial: boolean; startsAt: number; expiresAt: number | null; createdAt: number; plan_name: string; plan_level: number; } // ── Schemas ─────────────────────────────────────────────────────────────── const planSchema = z.object({ name: z.string().min(1, 'نام الزامی است'), level: z.coerce.number().min(0), max_secretaries: z.coerce.number().min(1), features: z.record(z.string(), z.boolean()), active: z.boolean(), }); type PlanForm = z.infer; const periodSchema = z.object({ plan_uuid: z.string().min(1, 'پلن الزامی است'), label: z.string().min(1, 'عنوان دوره الزامی است'), duration_months: z.coerce.number().min(1), price_rials: z.coerce.number().min(0), is_trial: z.boolean(), sort_order: z.coerce.number().min(0), active: z.boolean(), }); type PeriodForm = z.infer; // ── Feature labels ──────────────────────────────────────────────────────── const FEATURE_LABELS: Record = { patient_records: 'پرونده بیمار', services: 'سرویس‌ها', sms_panel: 'پنل پیامک', insurance: 'بیمه و مطالبات', }; const FEATURE_KEYS = Object.keys(FEATURE_LABELS); const emptyFeatures = (): Record => Object.fromEntries(FEATURE_KEYS.map((k) => [k, false])); const PLAN_DISPLAY: Record = { free: 'رایگان', basic: 'پایه', professional: 'حرفه‌ای', }; // ── Switch toggle ───────────────────────────────────────────────────────── const SwitchToggle = React.forwardRef>( function SwitchToggle(props, ref) { return ( e.stopPropagation()}> ); }, ); // ── Plans tab ───────────────────────────────────────────────────────────── function PlansTab() { const qc = useQueryClient(); const [planModal, setPlanModal] = useState<'create' | SubscriptionPlan | null>(null); const [periodModal, setPeriodModal] = useState<'create' | SubscriptionPeriod | null>(null); const [activePlan, setActivePlan] = useState(null); const [deletePeriod, setDeletePeriod] = useState(null); const { data: plansData, isLoading } = useQuery({ queryKey: ['admin-subscription-plans'], queryFn: () => api.get>('/api/v1/admin/subscription/plans'), }); const plans: SubscriptionPlan[] = (plansData as any)?.data ?? []; const planForm = useForm({ resolver: zodResolver(planSchema), defaultValues: { features: emptyFeatures(), active: true }, }); const periodForm = useForm({ resolver: zodResolver(periodSchema), defaultValues: { is_trial: false, sort_order: 0, active: true, price_rials: 0 }, }); const createPlanMut = useMutation({ mutationFn: (body: PlanForm) => api.post('/api/v1/admin/subscription/plan', body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPlanModal(null); planForm.reset(); toast.success('پلن ایجاد شد'); }, onError: (e: any) => toast.error(e.message), }); const updatePlanMut = useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: PlanForm }) => api.patch(`/api/v1/admin/subscription/plan/${uuid}`, body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPlanModal(null); toast.success('پلن بروزرسانی شد'); }, onError: (e: any) => toast.error(e.message), }); const createPeriodMut = useMutation({ mutationFn: (body: PeriodForm) => api.post('/api/v1/admin/subscription/period', body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPeriodModal(null); periodForm.reset(); toast.success('دوره ایجاد شد'); }, onError: (e: any) => toast.error(e.message), }); const updatePeriodMut = useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: PeriodForm }) => api.patch(`/api/v1/admin/subscription/period/${uuid}`, body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setPeriodModal(null); toast.success('دوره بروزرسانی شد'); }, onError: (e: any) => toast.error(e.message), }); const deletePeriodMut = useMutation({ mutationFn: (uuid: string) => api.delete(`/api/v1/admin/subscription/period/${uuid}`), onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin-subscription-plans'] }); setDeletePeriod(null); toast.success('دوره حذف شد'); }, onError: (e: any) => toast.error(e.message), }); const openEditPlan = (plan: SubscriptionPlan) => { planForm.reset({ name: plan.name, level: plan.level, max_secretaries: plan.max_secretaries, features: { ...emptyFeatures(), ...plan.features }, active: (plan as any).active ?? true, }); setPlanModal(plan); }; const openEditPeriod = (period: SubscriptionPeriod) => { periodForm.reset({ plan_uuid: (period as any).plan_uuid ?? '', label: period.label, duration_months: period.duration_months, price_rials: period.price_rials, is_trial: period.is_trial, sort_order: (period as any).sort_order ?? 0, active: (period as any).active ?? true, }); setPeriodModal(period); }; const openCreatePeriod = (plan: SubscriptionPlan) => { setActivePlan(plan); periodForm.reset({ plan_uuid: plan.uuid, is_trial: false, sort_order: 0, active: true, price_rials: 0, label: '', duration_months: 1 }); setPeriodModal('create'); }; return ( <>
{isLoading ? (
در حال بارگذاری...
) : (
{plans.map((plan) => { const periods: SubscriptionPeriod[] = Array.isArray(plan.periods) ? plan.periods : Object.values(plan.periods ?? {}); return (
{PLAN_DISPLAY[plan.name] ?? plan.name} {plan.active ? 'فعال' : 'غیرفعال'} سطح {plan.level} حداکثر {plan.max_secretaries} منشی {Object.entries(plan.features).map(([k, v]) => ( {FEATURE_LABELS[k] ?? k} ))}
{periods.length === 0 ? (
هنوز دوره‌ای تعریف نشده
) : (
{periods.map((period, i) => ( ))}
دوره مدت قیمت نوع عملیات
{period.label} {period.duration_months} ماه {period.is_trial ? 'رایگان' : formatRial(period.price_rials)} {period.is_trial ? تریال : پولی}
)}
); })}
)} {/* Modal پنل */} setPlanModal(null)} title={planModal === 'create' ? 'پلن جدید' : 'ویرایش پلن'} >
{ if (planModal === 'create') createPlanMut.mutate(d); else if (planModal !== null && typeof planModal === 'object') updatePlanMut.mutate({ uuid: planModal.uuid, body: d }); })}>
{planForm.formState.errors.name && {planForm.formState.errors.name.message}}
قابلیت‌های پلن
{FEATURE_KEYS.map((k, i) => ( ))}
{/* Modal دوره */} setPeriodModal(null)} title={periodModal === 'create' ? `دوره جدید — ${PLAN_DISPLAY[activePlan?.name ?? ''] ?? activePlan?.name ?? ''}` : 'ویرایش دوره'} >
{ if (periodModal === 'create') createPeriodMut.mutate(d); else if (periodModal !== null && typeof periodModal === 'object') updatePeriodMut.mutate({ uuid: periodModal.uuid, body: d }); })}>
{periodForm.formState.errors.label && {periodForm.formState.errors.label.message}}
{/* Confirm حذف دوره */} deletePeriod && deletePeriodMut.mutate(deletePeriod.uuid)} onCancel={() => setDeletePeriod(null)} /> ); } // ── Report tab ──────────────────────────────────────────────────────────── function ReportTab() { const [page, setPage] = useState(1); const limit = 20; const { data, isLoading } = useQuery({ queryKey: ['admin-subscription-report', page], queryFn: () => api.get>(`/api/v1/admin/subscription/report?page=${page}&limit=${limit}`), }); const rows: ReportRow[] = data?.data ?? []; const total = data?.meta?.totalRecords ?? 0; return (
{isLoading ? (
در حال بارگذاری...
) : rows.length === 0 ? (
هیچ اشتراکی ثبت نشده
) : ( <>
{rows.map((row, i) => ( ))}
نوع پلن نوع اشتراک شروع انقضا تاریخ ثبت
{row.entityType === 'clinic' ? 'کلینیک' : 'دکتر'} #{row.entityId} {PLAN_DISPLAY[row.plan_name] ?? row.plan_name} سطح {row.plan_level} {row.isTrial ? تریال : پولی} {formatDate(row.startsAt)} {row.expiresAt ? formatDate(row.expiresAt) : بی‌نهایت} {formatDate(row.createdAt)}
)}
); } // ── Main ────────────────────────────────────────────────────────────────── export default function AdminSubscriptionPage() { const [tab, setTab] = useState<'plans' | 'report'>('plans'); return ( <>
{tab === 'plans' && } {tab === 'report' && } ); }