diff --git a/assets/admin/pages/SubscriptionPage.tsx b/assets/admin/pages/SubscriptionPage.tsx index f52440ae..d35ac298 100644 --- a/assets/admin/pages/SubscriptionPage.tsx +++ b/assets/admin/pages/SubscriptionPage.tsx @@ -1,31 +1,53 @@ import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { CheckIcon, SparklesIcon } from '@heroicons/react/24/outline'; +import { + CheckIcon, XMarkIcon, SparklesIcon, RocketLaunchIcon, + ShieldCheckIcon, CreditCardIcon, ClockIcon, ArrowPathIcon, +} from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import type { SubscriptionPlan, MySubscriptionData, SubscriptionPeriod } from '../types'; import { formatDate, formatRial, formatNumber } from '../lib/utils'; import Modal from '../components/ui/Modal'; -import PageHeader from '../components/ui/PageHeader'; + +// ── Constants ───────────────────────────────────────────────────────────── const PLAN_FEATURE_LABELS: Record = { patient_records: 'پرونده بیمار', - services: 'مدیریت سرویس‌ها', - sms_panel: 'پنل پیامک', + services: 'مدیریت سرویس‌ها', + sms_panel: 'پنل پیامک', }; -const PLAN_DISPLAY: Record = { - free: { label: 'رایگان', color: '#64748b' }, - basic: { label: 'پایه', color: '#3b82f6' }, - professional: { label: 'حرفه‌ای', color: '#8b5cf6' }, +const PLAN_META: Record = { + free: { + label: 'رایگان', desc: 'برای شروع کار با سیستم', + color: '#64748b', bg: '#f1f5f9', border: '#cbd5e1', + icon: ShieldCheckIcon, + }, + basic: { + label: 'پایه', desc: 'برای مطب‌های کوچک و متوسط', + color: '#2563eb', bg: '#eff6ff', border: '#93c5fd', + icon: RocketLaunchIcon, + }, + professional: { + label: 'حرفه‌ای', desc: 'برای کلینیک‌های بزرگ', + color: '#7c3aed', bg: '#f5f3ff', border: '#c4b5fd', + icon: SparklesIcon, + }, }; -const GATEWAY_LABELS: Record = { mellat: 'ملت', sep: 'سپ' }; +const GATEWAY_LABELS: Record = { mellat: 'بانک ملت', sep: 'سپ (صادرات)' }; + +// ── Main Page ───────────────────────────────────────────────────────────── export default function SubscriptionPage() { const qc = useQueryClient(); - const [purchaseTarget, setPurchaseTarget] = useState(null); + const [purchaseTarget, setPurchaseTarget] = useState<{ period: SubscriptionPeriod; planName: string } | null>(null); const [selectedGateway, setSelectedGateway] = useState<'mellat' | 'sep'>('mellat'); const { data: plansData, isLoading: plansLoading } = useQuery>({ @@ -33,15 +55,15 @@ export default function SubscriptionPage() { queryFn: () => api.get('/api/v1/subscription/plans'), }); - const { data: myData } = useQuery>({ + const { data: myData, isLoading: myLoading } = useQuery>({ queryKey: ['subscription-my'], queryFn: () => api.get('/api/v1/subscription/my'), }); - const plans = plansData?.data ?? []; - const myRaw = myData?.data; - const my = myRaw?.subscription ?? null; - const usedTrial = myRaw?.used_trial ?? false; + const plans = plansData?.data ?? []; + const myRaw = myData?.data; + const my = myRaw?.subscription ?? null; + const usedTrial = myRaw?.used_trial ?? false; const trialMutation = useMutation({ mutationFn: () => api.post('/api/v1/subscription/trial', {}), @@ -63,164 +85,436 @@ export default function SubscriptionPage() { onError: (err: any) => toast.error(err.message), }); - const daysTotal = my?.expires_at && my?.starts_at - ? Math.round((my.expires_at - my.starts_at) / 86400) - : null; - const daysProgress = daysTotal && my?.days_remaining != null - ? Math.max(0, Math.min(100, ((daysTotal - my.days_remaining) / daysTotal) * 100)) - : null; + const daysTotal = my?.expires_at && my?.starts_at ? Math.round((my.expires_at - my.starts_at) / 86400) : null; + const daysLeft = my?.days_remaining ?? 0; + const daysProgress = daysTotal ? Math.max(0, Math.min(100, ((daysTotal - daysLeft) / daysTotal) * 100)) : null; + const planMeta = my ? (PLAN_META[my.plan.name] ?? PLAN_META.free) : null; + const isExpiringSoon = daysLeft > 0 && daysLeft <= 7; return ( - <> - +
- {/* وضعیت اشتراک فعلی */} - {my && ( -
-
- -
-
- پنل {PLAN_DISPLAY[my.plan.name]?.label ?? my.plan.name} - {my.is_trial && تریال} -
- {my.expires_at - ?
- انقضا: {formatDate(my.expires_at)} — {formatNumber(my.days_remaining ?? 0)} روز باقی‌مانده -
- :
بدون تاریخ انقضا
- } + {/* ── Header ── */} +
+

پنل اشتراکی

+

+ پنل خود را انتخاب کنید و از امکانات بیشتر بهره‌مند شوید +

+
+ + {/* ── وضعیت فعلی ── */} + {!myLoading && my && ( +
+ {/* decorative circle */} +
+ +
+ {/* icon */} +
+ {planMeta && }
+ + {/* info */} +
+
+ + پنل {planMeta!.label} + + {my.is_trial && ( + + تریال + + )} + {isExpiringSoon && ( + + در حال انقضا + + )} +
+ + {my.expires_at ? ( +
+ + انقضا {formatDate(my.expires_at)} + + ({formatNumber(daysLeft)} روز مانده) + +
+ ) : ( +
بدون تاریخ انقضا
+ )} + + {/* progress bar */} + {daysProgress !== null && ( +
+
+
+
+ + {Math.round(daysProgress)}% گذشته + +
+ )} +
+ + {/* trial CTA */} {!usedTrial && ( )}
- {daysProgress !== null && ( -
-
-
- )}
)} - {/* کارت‌های پلن */} + {/* ── بنر تریال (اگر هنوز استفاده نشده و اشتراکی ندارد) ── */} + {!myLoading && !my && !usedTrial && ( +
+ +
+
+ یک ماه تریال رایگان دارید! +
+
+ پنل پایه را به مدت یک ماه رایگان امتحان کنید +
+
+ +
+ )} + + {/* ── کارت‌های پلن ── */} {plansLoading ? ( -
در حال بارگذاری...
+
+ {[1, 2, 3].map((i) => ( +
+
+
+ ))} +
) : ( -
+
{plans.map((plan) => ( setPurchaseTarget(period)} + onPurchase={(period) => setPurchaseTarget({ period, planName: plan.name })} + onTrial={() => trialMutation.mutate()} + trialPending={trialMutation.isPending} /> ))}
)} - {/* Modal پرداخت */} + {/* ── فوتر راهنما ── */} +
+ 🔒 پرداخت امن از طریق درگاه‌های معتبر + 🔄 تمدید از تاریخ انقضای قبلی محاسبه می‌شود + 📦 داده‌ها پس از انقضا حفظ می‌شوند +
+ + {/* ── Modal پرداخت ── */} setPurchaseTarget(null)} - title={`خرید ${purchaseTarget?.label ?? ''}`} + onClose={() => { setPurchaseTarget(null); setSelectedGateway('mellat'); }} + title="پرداخت اشتراک" + size="sm" > -
-
- {(['mellat', 'sep'] as const).map((gw) => ( + {purchaseTarget && ( +
+ {/* خلاصه خرید */} +
+
+
دوره انتخابی
+
+ {PLAN_META[purchaseTarget.planName]?.label ?? purchaseTarget.planName} — {purchaseTarget.period.label} +
+
+ {purchaseTarget.period.duration_months} ماه +
+
+
+
مبلغ قابل پرداخت
+
+ {formatRial(purchaseTarget.period.price_rials)} +
+
+
+ + {/* انتخاب درگاه */} +
+
+ انتخاب درگاه پرداخت +
+
+ {(['mellat', 'sep'] as const).map((gw) => ( + + ))} +
+
+ + {/* دکمه‌ها */} +
- ))} + +
-

- مبلغ: {purchaseTarget ? formatRial(purchaseTarget.price_rials) : ''} -

-
- - -
-
+ )} - +
); } +// ── PlanCard ────────────────────────────────────────────────────────────── + function PlanCard({ plan, - currentPlanLevel, + currentPlanName, usedTrial, onPurchase, + onTrial, + trialPending, }: { plan: SubscriptionPlan; - currentPlanLevel: number; + currentPlanName: string; usedTrial: boolean; onPurchase: (period: SubscriptionPeriod) => void; + onTrial: () => void; + trialPending: boolean; }) { - const display = PLAN_DISPLAY[plan.name] ?? { label: plan.name, color: '#64748b' }; - const isCurrent = plan.level === currentPlanLevel; + const meta = PLAN_META[plan.name] ?? PLAN_META.free; + const isCurrent = plan.name === currentPlanName; + const isUpgrade = (plan.level ?? 0) > ((PLAN_META[currentPlanName] ? plan.level : 0)); const paidPeriods = plan.periods.filter((p) => !p.is_trial); const trialPeriod = plan.periods.find((p) => p.is_trial); + const Icon = meta.icon; return ( -
{ (e.currentTarget as HTMLElement).style.boxShadow = 'var(--shadow)'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-3px)'; }} + onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = ''; (e.currentTarget as HTMLElement).style.transform = ''; }} > - {isCurrent && ( - - پنل فعلی - - )} -
- {display.label} -
-
- حداکثر {plan.max_secretaries} منشی + {/* توپ تزئینی */} +
+ + {/* سربرگ کارت */} +
+ {isCurrent && ( + + پنل فعلی + + )} + +
+ +
+ +
+ {meta.label} +
+
+ {meta.desc} +
+
+ {plan.max_secretaries} + منشی +
-
    - {Object.entries(plan.features).map(([key, enabled]) => ( -
  • - - - {PLAN_FEATURE_LABELS[key] ?? key} + {/* قابلیت‌ها */} +
    0 ? '1px solid var(--border)' : 'none', flex: 1 }}> +
    + امکانات +
    +
      + {Object.entries(plan.features).map(([key, enabled]) => ( +
    • + + {enabled + ? + : + } + + + {PLAN_FEATURE_LABELS[key] ?? key} + +
    • + ))} +
    + + {/* تریال */} + {trialPeriod && !usedTrial && plan.level > 0 && ( +
    + 🎁 + + تریال {trialPeriod.duration_months} ماهه رایگان -
  • - ))} -
+ +
+ )} +
+ {/* دوره‌های پرداختی */} {paidPeriods.length > 0 && ( -
+
{paidPeriods.map((period) => ( -
- {period.label} +
+
+
{period.label}
+
{period.duration_months} ماه
+
- {formatRial(period.price_rials)} + + {formatRial(period.price_rials)} + @@ -230,9 +524,15 @@ function PlanCard({
)} - {trialPeriod && !usedTrial && plan.level > 0 && ( -
- تریال {trialPeriod.duration_months} ماهه رایگان — از دکمه بالای صفحه فعال کنید + {/* پنل رایگان — بدون دوره */} + {paidPeriods.length === 0 && plan.level === 0 && ( +
+ {isCurrent ? '✓ شما اکنون در این پنل هستید' : 'رایگان — بدون هزینه'}
)}