initial: standalone repo from monorepo split
This commit is contained in:
128
src/app/(dashboard)/dashboard/subscriptions/page.tsx
Normal file
128
src/app/(dashboard)/dashboard/subscriptions/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user