feat: Next.js 14 web admin scaffold — auth, sidebar, dashboard, placeholders

This commit is contained in:
Forge
2026-03-25 08:23:25 +08:00
parent dc01eac449
commit 310a9453de
37 changed files with 2755 additions and 292 deletions

View File

@@ -0,0 +1,71 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Search, UserPlus } from 'lucide-react';
export default function ClientsPage() {
const [search, setSearch] = useState('');
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Clients</h1>
<p className="text-slate-500 text-sm mt-1">Manage your ISP subscribers</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<UserPlus size={16} className="mr-2" />
Add Client
</Button>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="relative flex-1 max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input
placeholder="Search clients..."
className="pl-9"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Phone</TableHead>
<TableHead>Plan</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No clients yet. Add your first client to get started.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,100 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Users, Wifi, DollarSign, AlertTriangle } from 'lucide-react';
interface DashboardSummary {
totalClients: number;
activeSubscribers: number;
todayCollections: number;
overdueInvoices: number;
}
const kpiCards = [
{
key: 'totalClients' as const,
label: 'Total Clients',
icon: Users,
color: '#0891B2',
format: (v: number) => v.toLocaleString(),
},
{
key: 'activeSubscribers' as const,
label: 'Active Subscribers',
icon: Wifi,
color: '#059669',
format: (v: number) => v.toLocaleString(),
},
{
key: 'todayCollections' as const,
label: "Today's Collections",
icon: DollarSign,
color: '#7C3AED',
format: (v: number) => '₱' + v.toLocaleString('en-PH', { minimumFractionDigits: 2 }),
},
{
key: 'overdueInvoices' as const,
label: 'Overdue Invoices',
icon: AlertTriangle,
color: '#DC2626',
format: (v: number) => v.toLocaleString(),
},
];
export default function DashboardPage() {
const { data, isLoading, error } = useQuery<DashboardSummary>({
queryKey: ['dashboard', 'summary'],
queryFn: async () => {
const res = await api.get('/api/v1/dashboard/summary');
return res.data;
},
});
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Dashboard</h1>
<p className="text-slate-500 text-sm mt-1">Overview of your ISP operations</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
Failed to load dashboard data. The API endpoint may not be available yet.
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{kpiCards.map((card) => {
const Icon = card.icon;
return (
<Card key={card.key} className="border shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-slate-500">
{card.label}
</CardTitle>
<div
className="w-9 h-9 rounded-lg flex items-center justify-center"
style={{ backgroundColor: card.color + '1A' }}
>
<Icon size={18} style={{ color: card.color }} />
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-24" />
) : (
<p className="text-2xl font-bold text-slate-800">
{data ? card.format(data[card.key]) : '—'}
</p>
)}
</CardContent>
</Card>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Search } from 'lucide-react';
export default function InvoicesPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Invoices</h1>
<p className="text-slate-500 text-sm mt-1">Track and manage billing invoices</p>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="relative max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input placeholder="Search invoices..." className="pl-9" />
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice #</TableHead>
<TableHead>Client</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No invoices found.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

32
app/(app)/layout.tsx Normal file
View File

@@ -0,0 +1,32 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import Sidebar from '@/components/layout/sidebar';
import Topbar from '@/components/layout/topbar';
export default function AppLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const accessToken = useAuthStore((s) => s.accessToken);
useEffect(() => {
if (!accessToken) {
router.replace('/login');
}
}, [accessToken, router]);
if (!accessToken) {
return null;
}
return (
<div className="flex min-h-screen" style={{ backgroundColor: '#F8FAFC' }}>
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<Topbar />
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Search } from 'lucide-react';
export default function PaymentsPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Payments</h1>
<p className="text-slate-500 text-sm mt-1">Record and review payment transactions</p>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="relative max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input placeholder="Search payments..." className="pl-9" />
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Ref #</TableHead>
<TableHead>Client</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Channel</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No payments recorded yet.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
export default function RemittancesPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Remittances</h1>
<p className="text-slate-500 text-sm mt-1">Collector cash remittance management</p>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4" />
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Reference</TableHead>
<TableHead>Collector</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No remittances found.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,33 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart3 } from 'lucide-react';
export default function ReportsPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Reports</h1>
<p className="text-slate-500 text-sm mt-1">Financial and operational reports</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{['Collections Report', 'Subscriber Growth', 'Invoice Aging', 'Technician Performance'].map(
(report) => (
<Card key={report} className="border shadow-sm hover:shadow-md transition-shadow cursor-pointer">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2 text-slate-700">
<BarChart3 size={18} style={{ color: '#0891B2' }} />
{report}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-400 text-sm">Coming soon report generation will be available here.</p>
</CardContent>
</Card>
)
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Plus, MapPin } from 'lucide-react';
export default function AreasPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Areas & Zones</h1>
<p className="text-slate-500 text-sm mt-1">Define service coverage areas</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<Plus size={16} className="mr-2" />
Add Area
</Button>
</div>
<Card className="border shadow-sm">
<CardContent className="flex flex-col items-center justify-center py-16 text-slate-400">
<MapPin size={32} className="mb-3 text-slate-300" />
<p className="text-sm font-medium">No areas defined yet</p>
<p className="text-xs mt-1">Create areas to group clients by coverage zone</p>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,31 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
export default function PlansPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Service Plans</h1>
<p className="text-slate-500 text-sm mt-1">Manage internet subscription plans</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<Plus size={16} className="mr-2" />
Add Plan
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="border shadow-sm border-dashed border-slate-300">
<CardContent className="flex flex-col items-center justify-center py-12 text-slate-400">
<Plus size={24} className="mb-2" />
<p className="text-sm">Create your first plan</p>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
'use client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
export default function TenantSettingsPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Tenant Settings</h1>
<p className="text-slate-500 text-sm mt-1">Configure your ISP business details</p>
</div>
<Card className="border shadow-sm max-w-2xl">
<CardHeader>
<CardTitle className="text-base">Business Information</CardTitle>
<CardDescription>Update your ISP company details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1.5">
<Label>Business Name</Label>
<Input placeholder="e.g. My Internet Services" />
</div>
<div className="space-y-1.5">
<Label>Address</Label>
<Input placeholder="Business address" />
</div>
<div className="space-y-1.5">
<Label>Phone</Label>
<Input placeholder="+63 9XX XXX XXXX" />
</div>
<div className="space-y-1.5">
<Label>Email</Label>
<Input type="email" placeholder="info@yourisp.com" />
</div>
<Button style={{ backgroundColor: '#0891B2' }}>Save Changes</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,53 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { UserPlus } from 'lucide-react';
export default function UsersPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Users</h1>
<p className="text-slate-500 text-sm mt-1">Manage staff accounts and permissions</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<UserPlus size={16} className="mr-2" />
Invite User
</Button>
</div>
<Card className="border shadow-sm">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={5} className="text-center py-12 text-slate-400">
No users found. Invite your team members.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,62 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Search, Plus } from 'lucide-react';
export default function TicketsPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Tickets</h1>
<p className="text-slate-500 text-sm mt-1">Support and installation tickets</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<Plus size={16} className="mr-2" />
New Ticket
</Button>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="relative max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input placeholder="Search tickets..." className="pl-9" />
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Ticket #</TableHead>
<TableHead>Client</TableHead>
<TableHead>Type</TableHead>
<TableHead>Priority</TableHead>
<TableHead>Status</TableHead>
<TableHead>Assigned To</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={7} className="text-center py-12 text-slate-400">
No tickets found.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}