239 lines
9.6 KiB
TypeScript
239 lines
9.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { CheckIcon, SparklesIcon } from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse } from '../lib/api';
|
|
import type { SubscriptionPlan, MySubscription, SubscriptionPeriod } from '../types';
|
|
import { formatDate, formatRial, formatNumber } from '../lib/utils';
|
|
import Modal from '../components/ui/Modal';
|
|
import PageHeader from '../components/ui/PageHeader';
|
|
|
|
const PLAN_FEATURE_LABELS: Record<string, string> = {
|
|
patient_records: 'پرونده بیمار',
|
|
services: 'مدیریت سرویسها',
|
|
sms_panel: 'پنل پیامک',
|
|
};
|
|
|
|
const PLAN_DISPLAY: Record<string, { label: string; color: string }> = {
|
|
free: { label: 'رایگان', color: '#64748b' },
|
|
basic: { label: 'پایه', color: '#3b82f6' },
|
|
professional: { label: 'حرفهای', color: '#8b5cf6' },
|
|
};
|
|
|
|
const GATEWAY_LABELS: Record<string, string> = { mellat: 'ملت', sep: 'سپ' };
|
|
|
|
export default function SubscriptionPage() {
|
|
const qc = useQueryClient();
|
|
const [purchaseTarget, setPurchaseTarget] = useState<SubscriptionPeriod | null>(null);
|
|
const [selectedGateway, setSelectedGateway] = useState<'mellat' | 'sep'>('mellat');
|
|
|
|
const { data: plansData, isLoading: plansLoading } = useQuery<ApiResponse<SubscriptionPlan[]>>({
|
|
queryKey: ['subscription-plans'],
|
|
queryFn: () => api.get('/api/v1/subscription/plans'),
|
|
});
|
|
|
|
const { data: myData } = useQuery<ApiResponse<MySubscription>>({
|
|
queryKey: ['subscription-my'],
|
|
queryFn: () => api.get('/api/v1/subscription/my'),
|
|
});
|
|
|
|
const plans = plansData?.data ?? [];
|
|
const my = myData?.data;
|
|
|
|
const trialMutation = useMutation({
|
|
mutationFn: () => api.post('/api/v1/subscription/trial', {}),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['subscription-my'] });
|
|
toast.success('تریال رایگان با موفقیت فعال شد');
|
|
},
|
|
onError: (err: any) => toast.error(err.message),
|
|
});
|
|
|
|
const purchaseMutation = useMutation({
|
|
mutationFn: ({ period_uuid, gateway }: { period_uuid: string; gateway: string }) =>
|
|
api.post<{ data: { payment_url: string } }>('/api/v1/subscription-payment', { period_uuid, gateway }),
|
|
onSuccess: (res: any) => {
|
|
const url = res?.data?.payment_url;
|
|
if (url) window.location.href = url;
|
|
else toast.error('خطا در دریافت لینک پرداخت');
|
|
},
|
|
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;
|
|
|
|
return (
|
|
<>
|
|
<PageHeader title="پنل اشتراکی" description="مدیریت اشتراک و ارتقای پنل" />
|
|
|
|
{/* وضعیت اشتراک فعلی */}
|
|
{my && (
|
|
<div className="card" style={{ marginBottom: 20 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
|
<SparklesIcon style={{ width: 22, color: PLAN_DISPLAY[my.plan.name]?.color ?? '#64748b' }} />
|
|
<div>
|
|
<div style={{ fontWeight: 700, fontSize: 16 }}>
|
|
پنل {PLAN_DISPLAY[my.plan.name]?.label ?? my.plan.name}
|
|
{my.is_trial && <span className="badge amber" style={{ marginRight: 8, fontSize: 11 }}>تریال</span>}
|
|
</div>
|
|
{my.expires_at
|
|
? <div style={{ fontSize: 13, color: 'var(--text-3)' }}>
|
|
انقضا: {formatDate(my.expires_at)} — {formatNumber(my.days_remaining ?? 0)} روز باقیمانده
|
|
</div>
|
|
: <div style={{ fontSize: 13, color: 'var(--text-3)' }}>بدون تاریخ انقضا</div>
|
|
}
|
|
</div>
|
|
{!my.used_trial && (
|
|
<button
|
|
className="btn primary sm"
|
|
style={{ marginRight: 'auto' }}
|
|
onClick={() => trialMutation.mutate()}
|
|
disabled={trialMutation.isPending}
|
|
>
|
|
{trialMutation.isPending ? 'در حال فعالسازی...' : 'فعالسازی تریال رایگان'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{daysProgress !== null && (
|
|
<div style={{ background: 'var(--surface-2)', borderRadius: 6, height: 6, overflow: 'hidden' }}>
|
|
<div style={{ width: `${daysProgress}%`, height: '100%', background: PLAN_DISPLAY[my.plan.name]?.color ?? '#3b82f6', transition: 'width .3s' }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* کارتهای پلن */}
|
|
{plansLoading ? (
|
|
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>در حال بارگذاری...</div>
|
|
) : (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 16 }}>
|
|
{plans.map((plan) => (
|
|
<PlanCard
|
|
key={plan.uuid}
|
|
plan={plan}
|
|
currentPlanLevel={my?.plan.level ?? 0}
|
|
usedTrial={my?.used_trial ?? false}
|
|
onPurchase={(period) => setPurchaseTarget(period)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal پرداخت */}
|
|
<Modal
|
|
open={!!purchaseTarget}
|
|
onClose={() => setPurchaseTarget(null)}
|
|
title={`خرید ${purchaseTarget?.label ?? ''}`}
|
|
>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
{(['mellat', 'sep'] as const).map((gw) => (
|
|
<button
|
|
key={gw}
|
|
className={`btn ${selectedGateway === gw ? 'primary' : ''}`}
|
|
onClick={() => setSelectedGateway(gw)}
|
|
>
|
|
{GATEWAY_LABELS[gw]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p style={{ margin: 0, color: 'var(--text-3)', fontSize: 14 }}>
|
|
مبلغ: <b style={{ color: 'var(--text-1)' }}>{purchaseTarget ? formatRial(purchaseTarget.price_rials) : ''}</b>
|
|
</p>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button
|
|
className="btn primary"
|
|
disabled={purchaseMutation.isPending}
|
|
onClick={() =>
|
|
purchaseTarget &&
|
|
purchaseMutation.mutate({ period_uuid: purchaseTarget.uuid, gateway: selectedGateway })
|
|
}
|
|
>
|
|
{purchaseMutation.isPending ? 'در حال انتقال...' : 'پرداخت'}
|
|
</button>
|
|
<button className="btn" onClick={() => setPurchaseTarget(null)}>انصراف</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function PlanCard({
|
|
plan,
|
|
currentPlanLevel,
|
|
usedTrial,
|
|
onPurchase,
|
|
}: {
|
|
plan: SubscriptionPlan;
|
|
currentPlanLevel: number;
|
|
usedTrial: boolean;
|
|
onPurchase: (period: SubscriptionPeriod) => void;
|
|
}) {
|
|
const display = PLAN_DISPLAY[plan.name] ?? { label: plan.name, color: '#64748b' };
|
|
const isCurrent = plan.level === currentPlanLevel;
|
|
const paidPeriods = plan.periods.filter((p) => !p.is_trial);
|
|
const trialPeriod = plan.periods.find((p) => p.is_trial);
|
|
|
|
return (
|
|
<div
|
|
className="card"
|
|
style={{ border: isCurrent ? `2px solid ${display.color}` : undefined, position: 'relative' }}
|
|
>
|
|
{isCurrent && (
|
|
<span className="badge green" style={{ position: 'absolute', top: 12, left: 12, fontSize: 11 }}>
|
|
پنل فعلی
|
|
</span>
|
|
)}
|
|
<div style={{ fontWeight: 700, fontSize: 18, color: display.color, marginBottom: 8 }}>
|
|
{display.label}
|
|
</div>
|
|
<div style={{ fontSize: 13, color: 'var(--text-3)', marginBottom: 12 }}>
|
|
حداکثر {plan.max_secretaries} منشی
|
|
</div>
|
|
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 16px', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
{Object.entries(plan.features).map(([key, enabled]) => (
|
|
<li key={key} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
|
|
<CheckIcon style={{ width: 14, color: enabled ? '#22c55e' : '#94a3b8' }} />
|
|
<span style={{ color: enabled ? 'var(--text-1)' : 'var(--text-3)' }}>
|
|
{PLAN_FEATURE_LABELS[key] ?? key}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{paidPeriods.length > 0 && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{paidPeriods.map((period) => (
|
|
<div key={period.uuid} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
<span style={{ fontSize: 13 }}>{period.label}</span>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<span style={{ fontWeight: 600, fontSize: 13 }}>{formatRial(period.price_rials)}</span>
|
|
<button
|
|
className="btn primary sm"
|
|
onClick={() => onPurchase(period)}
|
|
>
|
|
{isCurrent ? 'تمدید' : 'خرید'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{trialPeriod && !usedTrial && plan.level > 0 && (
|
|
<div style={{ marginTop: 12, fontSize: 12, color: 'var(--text-3)', borderTop: '1px solid var(--border)', paddingTop: 10 }}>
|
|
تریال {trialPeriod.duration_months} ماهه رایگان — از دکمه بالای صفحه فعال کنید
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|