129 lines
6.5 KiB
TypeScript
129 lines
6.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { api } from '@/lib/api';
|
|
import { PageHeader } from '@/components/ui/page-header';
|
|
import { DataTable } from '@/components/ui/data-table';
|
|
import { Badge, statusBadgeVariant } from '@/components/ui/badge';
|
|
import { ActionIcon } from '@/components/ui/action-icon';
|
|
import { ActionMenu, ActionMenuItem } from '@/components/ui/action-menu';
|
|
import { Modal } from '@/components/ui/modal';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useToast } from '@/components/ui/toast';
|
|
|
|
interface Subscription {
|
|
id: string;
|
|
type: string;
|
|
status: string;
|
|
client: { id: string; firstName: string; lastName: string; accountNumber: string };
|
|
plan: { id: string; name: string; price: string; speedDown: number; speedUp: number };
|
|
createdAt: string;
|
|
}
|
|
|
|
export default function SubscriptionsPage() {
|
|
const { toast } = useToast();
|
|
const [subs, setSubs] = useState<Subscription[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [search, setSearch] = useState('');
|
|
const [detailTarget, setDetailTarget] = useState<Subscription | null>(null);
|
|
|
|
const loadSubs = useCallback(async () => {
|
|
try {
|
|
const res = await api.get<{ data: Subscription[] }>('/subscriptions');
|
|
setSubs(res.data.data);
|
|
} catch {
|
|
toast('Failed to load subscriptions', 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [toast]);
|
|
|
|
useEffect(() => { loadSubs(); }, [loadSubs]);
|
|
|
|
async function handleAction(id: string, action: string) {
|
|
try {
|
|
await api.patch(`/subscriptions/${id}/${action}`);
|
|
toast(`Subscription ${action}d`, 'success');
|
|
loadSubs();
|
|
} catch {
|
|
toast(`Failed to ${action} subscription`, 'error');
|
|
}
|
|
}
|
|
|
|
const filtered = search
|
|
? subs.filter((s) => `${s.client.firstName} ${s.client.lastName} ${s.client.accountNumber} ${s.plan.name}`.toLowerCase().includes(search.toLowerCase()))
|
|
: subs;
|
|
|
|
function getActionItems(s: Subscription): ActionMenuItem[] {
|
|
const items: ActionMenuItem[] = [];
|
|
if (s.status === 'active') {
|
|
items.push({ icon: 'pause', label: 'Suspend', onClick: () => handleAction(s.id, 'suspend') });
|
|
}
|
|
if (s.status === 'suspended') {
|
|
items.push({ icon: 'play', label: 'Reactivate', onClick: () => handleAction(s.id, 'reactivate') });
|
|
}
|
|
if (['pending', 'active', 'suspended'].includes(s.status)) {
|
|
items.push({ icon: 'x-circle', label: 'Cancel', onClick: () => handleAction(s.id, 'cancel'), variant: 'danger' });
|
|
}
|
|
return items;
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader title="Subscriptions" description="Manage client subscription plans and status" />
|
|
|
|
<div className="mt-5">
|
|
<DataTable data={filtered} loading={loading} keyExtractor={(s) => s.id}
|
|
emptyTitle="No subscriptions" emptyDescription="Subscriptions are created when clients sign up for plans."
|
|
searchPlaceholder="Search by client, account #, or plan..."
|
|
searchValue={search} onSearchChange={setSearch}
|
|
onRowClick={(s) => setDetailTarget(s)}
|
|
columns={[
|
|
{ key: 'client', label: 'Client', sortable: true, render: (s) => (
|
|
<div>
|
|
<span className="text-surface-800 dark:text-surface-200">{s.client.firstName} {s.client.lastName}</span>
|
|
<span className="ml-2 text-surface-400 font-mono text-xs">{s.client.accountNumber}</span>
|
|
</div>
|
|
)},
|
|
{ key: 'plan', label: 'Plan', sortable: true, render: (s) => <span className="text-surface-800 dark:text-surface-200">{s.plan.name}</span> },
|
|
{ key: 'type', label: 'Type', render: (s) => (
|
|
<Badge label={s.type} variant={s.type === 'postpaid' ? 'info' : 'default'} />
|
|
)},
|
|
{ key: 'status', label: 'Status', sortable: true, render: (s) => <Badge label={s.status} variant={statusBadgeVariant(s.status)} /> },
|
|
{ key: 'createdAt', label: 'Created', sortable: true, render: (s) => <span className="text-surface-500 dark:text-surface-400">{new Date(s.createdAt).toLocaleDateString()}</span> },
|
|
{ key: 'actions', label: '', align: 'right', render: (s) => {
|
|
const items = getActionItems(s);
|
|
return items.length > 0 ? (
|
|
<div onClick={(e) => e.stopPropagation()}>
|
|
<ActionMenu items={items} />
|
|
</div>
|
|
) : null;
|
|
}},
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
{/* Subscription Detail Modal */}
|
|
<Modal open={!!detailTarget} onClose={() => setDetailTarget(null)}
|
|
title="Subscription Details" description={detailTarget ? `${detailTarget.client.firstName} ${detailTarget.client.lastName}` : ''}>
|
|
{detailTarget && (
|
|
<div className="space-y-3 text-sm">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div><span className="text-surface-500 dark:text-surface-400">Account:</span> <span className="font-mono text-surface-700 dark:text-surface-300">{detailTarget.client.accountNumber}</span></div>
|
|
<div><span className="text-surface-500 dark:text-surface-400">Plan:</span> <span className="text-surface-800 dark:text-surface-200">{detailTarget.plan.name}</span></div>
|
|
<div><span className="text-surface-500 dark:text-surface-400">Speed:</span> <span className="text-surface-700 dark:text-surface-300">{detailTarget.plan.speedDown}/{detailTarget.plan.speedUp} Mbps</span></div>
|
|
<div><span className="text-surface-500 dark:text-surface-400">Price:</span> <span className="font-medium text-surface-900 dark:text-surface-100">PHP {Number(detailTarget.plan.price).toLocaleString()}</span></div>
|
|
<div><span className="text-surface-500 dark:text-surface-400">Type:</span> <span className="text-surface-700 dark:text-surface-300">{detailTarget.type}</span></div>
|
|
<div><span className="text-surface-500 dark:text-surface-400">Status:</span> <Badge label={detailTarget.status} variant={statusBadgeVariant(detailTarget.status)} /></div>
|
|
<div className="col-span-2"><span className="text-surface-500 dark:text-surface-400">Created:</span> <span className="text-surface-700 dark:text-surface-300">{new Date(detailTarget.createdAt).toLocaleDateString()}</span></div>
|
|
</div>
|
|
<div className="flex justify-end pt-2 border-t border-surface-200">
|
|
<Button size="sm" variant="ghost" onClick={() => setDetailTarget(null)}>Close</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|