Files
fiberops-web/src/app/(dashboard)/dashboard/subscriptions/page.tsx
2026-04-13 09:36:37 +08:00

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>
);
}