/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useCallback, useEffect, useMemo, useState } from 'react' import { Plus } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from '@/components/ui/sheet' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { ConfirmDialog } from '@/components/confirm-dialog' import { sideDrawerContentClassName, sideDrawerFormClassName, sideDrawerHeaderClassName, } from '@/components/drawer-layout' import { StatusBadge } from '@/components/status-badge' import { TableId } from '@/components/table-id' import { getAdminPlans, getUserSubscriptions, createUserSubscription, invalidateUserSubscription, deleteUserSubscription, } from '../../api' import { formatTimestamp } from '../../lib' import type { PlanRecord, UserSubscriptionRecord } from '../../types' interface Props { open: boolean onOpenChange: (open: boolean) => void user: { id: number; username?: string } | null onSuccess?: () => void } function SubscriptionStatusBadge(props: { sub: UserSubscriptionRecord['subscription'] t: (key: string) => string }) { // eslint-disable-next-line react-hooks/purity const now = Date.now() / 1000 const isExpired = (props.sub.end_time || 0) > 0 && props.sub.end_time < now const isActive = props.sub.status === 'active' && !isExpired if (isActive) return ( ) if (props.sub.status === 'cancelled') return ( ) return ( ) } export function UserSubscriptionsDialog(props: Props) { const { t } = useTranslation() const [loading, setLoading] = useState(false) const [creating, setCreating] = useState(false) const [plans, setPlans] = useState([]) const [subs, setSubs] = useState([]) const [selectedPlanId, setSelectedPlanId] = useState('') const [confirmAction, setConfirmAction] = useState<{ type: 'invalidate' | 'delete' subId: number } | null>(null) const planTitleMap = useMemo(() => { const map = new Map() plans.forEach((p) => { if (p.plan.id) map.set(p.plan.id, p.plan.title || `#${p.plan.id}`) }) return map }, [plans]) const loadData = useCallback(async () => { if (!props.user?.id) return setLoading(true) try { const [plansRes, subsRes] = await Promise.all([ getAdminPlans(), getUserSubscriptions(props.user.id), ]) if (plansRes.success) setPlans(plansRes.data || []) if (subsRes.success) setSubs(subsRes.data || []) } catch { toast.error(t('Loading failed')) } finally { setLoading(false) } }, [props.user?.id, t]) useEffect(() => { if (props.open && props.user?.id) { setSelectedPlanId('') loadData() } }, [props.open, props.user?.id, loadData]) const handleCreate = async () => { if (!props.user?.id || !selectedPlanId) { toast.error(t('Please select a subscription plan')) return } setCreating(true) try { const res = await createUserSubscription(props.user.id, { plan_id: Number(selectedPlanId), }) if (res.success) { toast.success(res.data?.message || t('Added successfully')) setSelectedPlanId('') await loadData() props.onSuccess?.() } } catch { toast.error(t('Request failed')) } finally { setCreating(false) } } const handleConfirmAction = async () => { if (!confirmAction) return try { if (confirmAction.type === 'invalidate') { const res = await invalidateUserSubscription(confirmAction.subId) if (res.success) { toast.success(res.data?.message || t('Has been invalidated')) await loadData() props.onSuccess?.() } } else { const res = await deleteUserSubscription(confirmAction.subId) if (res.success) { toast.success(t('Deleted')) await loadData() props.onSuccess?.() } } } catch { toast.error(t('Operation failed')) } finally { setConfirmAction(null) } } return ( <> {t('User Subscription Management')} {props.user?.username || '-'} (ID: {props.user?.id || '-'}) ({ value: String(p.plan.id), label: ( <> {p.plan.title}($ {Number(p.plan.price_amount || 0).toFixed(2)}) > ), })), ]} value={selectedPlanId} onValueChange={(v) => v !== null && setSelectedPlanId(v)} > {plans.map((p) => ( {p.plan.title} ($ {Number(p.plan.price_amount || 0).toFixed(2)}) ))} {t('Add subscription')} ID {t('Plan')} {t('Status')} {t('Validity')} {t('Total Quota')} {t('Actions')} {loading ? ( {t('Loading...')} ) : subs.length === 0 ? ( {t('No subscription records')} ) : ( subs.map((record) => { const sub = record.subscription const now = Date.now() / 1000 const isExpired = (sub.end_time || 0) > 0 && sub.end_time < now const isActive = sub.status === 'active' && !isExpired const total = Number(sub.amount_total || 0) const used = Number(sub.amount_used || 0) return ( {planTitleMap.get(sub.plan_id) || `#${sub.plan_id}`} {t('Source')}: {sub.source || '-'} {t('Start')}: {formatTimestamp(sub.start_time)} {t('End')}: {formatTimestamp(sub.end_time)} {total > 0 ? `${used}/${total}` : t('Unlimited')} setConfirmAction({ type: 'invalidate', subId: sub.id, }) } > {t('Invalidate')} setConfirmAction({ type: 'delete', subId: sub.id, }) } > {t('Delete')} ) }) )} {confirmAction && ( !v && setConfirmAction(null)} title={ confirmAction.type === 'invalidate' ? t('Confirm invalidate') : t('Confirm delete') } desc={ confirmAction.type === 'invalidate' ? t( 'After invalidating, this subscription will be immediately deactivated. Historical records are not affected. Continue?' ) : t( 'Deleting will permanently remove this subscription record (including benefit details). Continue?' ) } handleConfirm={handleConfirmAction} destructive={confirmAction.type === 'delete'} /> )} > ) }