feat: Add subscribers, usage, and settings pages with navigation sidebar and stat card component

- Add Sidebar component with 8 navigation items: Dashboard, Subscribers, Usage & Billing, Payments, eSIM Profiles, System Health, Chaos Engineering, Settings
- Add active route highlighting using usePathname with bg-sidebar-accent styling
- Add StatCard component with title, value, icon, optional trend indicator and description props
- Add SubscribersPage with 8 sample subscribers displaying name, MSISDN,
This commit is contained in:
nutcas3
2026-04-17 16:42:17 +03:00
parent 09a84936cc
commit f913724cf7
5 changed files with 345 additions and 0 deletions
@@ -0,0 +1,75 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Settings } from "lucide-react";
export default function SettingsPage() {
return (
<div className="p-8 space-y-6">
<div className="flex items-center gap-3">
<Settings className="size-8 text-muted-foreground" />
<div>
<h1 className="text-3xl font-bold tracking-tight">Settings</h1>
<p className="text-muted-foreground mt-1">Platform configuration and preferences.</p>
</div>
</div>
<div className="grid gap-6 max-w-2xl">
<Card>
<CardHeader>
<CardTitle>API Configuration</CardTitle>
<CardDescription>Configure the API server connection.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">API Base URL</label>
<input type="text" defaultValue="http://localhost:8000" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<div>
<label className="text-sm font-medium">Charging Engine URL</label>
<input type="text" defaultValue="http://localhost:3001" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<Button size="sm">Save Changes</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>IMSI Configuration</CardTitle>
<CardDescription>Mobile Country Code and Mobile Network Code settings.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">MCC+MNC Prefix</label>
<input type="text" defaultValue="20893" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<div>
<label className="text-sm font-medium">IMSI Range</label>
<input type="text" defaultValue="1 - 999999999" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" readOnly />
</div>
</div>
<Button size="sm">Save Changes</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>ES2+ SM-DP+ Configuration</CardTitle>
<CardDescription>eSIM profile server connection settings.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">SM-DP+ URL</label>
<input type="text" defaultValue="https://smdp.example.com" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<div>
<label className="text-sm font-medium">API Key</label>
<input type="password" defaultValue="••••••••" className="mt-1 block w-full h-9 rounded-lg border border-input bg-background px-3 text-sm outline-none focus:ring-2 focus:ring-ring/50" />
</div>
<Button size="sm">Save Changes</Button>
</CardContent>
</Card>
</div>
</div>
);
}
@@ -0,0 +1,94 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { UserPlus, Search, Download } from "lucide-react";
const subscribers = [
{ id: 1, name: "Alice Martin", msisdn: "+33612345678", imsi: "208930000000001", email: "[email protected]", status: "active" as const, balance: 45.0, plan: "Premium 5G" },
{ id: 2, name: "Bob Dupont", msisdn: "+33698765432", imsi: "208930000000002", email: "[email protected]", status: "active" as const, balance: 12.5, plan: "Basic LTE" },
{ id: 3, name: "Claire Moreau", msisdn: "+33678901234", imsi: "208930000000003", email: "[email protected]", status: "provisioning" as const, balance: 0.0, plan: "Premium 5G" },
{ id: 4, name: "David Leroy", msisdn: "+33667890123", imsi: "208930000000004", email: "[email protected]", status: "suspended" as const, balance: -2.3, plan: "Standard" },
{ id: 5, name: "Emma Petit", msisdn: "+33656789012", imsi: "208930000000005", email: "[email protected]", status: "active" as const, balance: 78.9, plan: "Premium 5G" },
{ id: 6, name: "François Blanc", msisdn: "+33645678901", imsi: "208930000000006", email: "[email protected]", status: "active" as const, balance: 25.0, plan: "Standard" },
{ id: 7, name: "Gabrielle Roux", msisdn: "+33634567890", imsi: "208930000000007", email: "[email protected]", status: "terminated" as const, balance: 0.0, plan: "Basic LTE" },
{ id: 8, name: "Henri Faure", msisdn: "+33623456789", imsi: "208930000000008", email: "[email protected]", status: "active" as const, balance: 156.2, plan: "Enterprise" },
];
const statusVariant = (s: string) =>
s === "active" ? "success" : s === "suspended" ? "warning" : s === "terminated" ? "destructive" : "secondary";
export default function SubscribersPage() {
return (
<div className="p-8 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Subscribers</h1>
<p className="text-muted-foreground mt-1">Manage subscriber accounts and profiles.</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm"><Download className="size-4 mr-1.5" />Export</Button>
<Button size="sm"><UserPlus className="size-4 mr-1.5" />Add Subscriber</Button>
</div>
</div>
<Card>
<CardHeader className="flex-row items-center justify-between">
<CardTitle>All Subscribers</CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
<input
type="text"
placeholder="Search subscribers..."
className="h-9 rounded-lg border border-input bg-background pl-9 pr-3 text-sm outline-none focus:ring-2 focus:ring-ring/50 w-64"
/>
</div>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-3 font-medium">Name</th>
<th className="pb-3 font-medium">MSISDN</th>
<th className="pb-3 font-medium">IMSI</th>
<th className="pb-3 font-medium">Plan</th>
<th className="pb-3 font-medium text-right">Balance</th>
<th className="pb-3 font-medium">Status</th>
<th className="pb-3 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody>
{subscribers.map((sub) => (
<tr key={sub.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
<td className="py-3">
<div>
<p className="font-medium">{sub.name}</p>
<p className="text-xs text-muted-foreground">{sub.email}</p>
</div>
</td>
<td className="py-3 font-mono text-xs">{sub.msisdn}</td>
<td className="py-3 font-mono text-xs">{sub.imsi}</td>
<td className="py-3">{sub.plan}</td>
<td className="py-3 text-right font-mono">
<span className={sub.balance < 0 ? "text-red-600" : ""}>
€{sub.balance.toFixed(2)}
</span>
</td>
<td className="py-3">
<Badge variant={statusVariant(sub.status)}>{sub.status}</Badge>
</td>
<td className="py-3 text-right">
<Button variant="ghost" size="xs">View</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
+78
View File
@@ -0,0 +1,78 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { StatCard } from "@/components/stat-card";
import { Activity, HardDrive, Phone, MessageSquare } from "lucide-react";
const usageEvents = [
{ id: 1, imsi: "208930000000001", type: "DATA", volume: "1.2 GB", cost: "€2.40", time: "2 min ago" },
{ id: 2, imsi: "208930000000002", type: "VOICE", volume: "15 min", cost: "€0.75", time: "5 min ago" },
{ id: 3, imsi: "208930000000005", type: "SMS", volume: "3 msgs", cost: "€0.30", time: "12 min ago" },
{ id: 4, imsi: "208930000000001", type: "DATA", volume: "450 MB", cost: "€0.90", time: "18 min ago" },
{ id: 5, imsi: "208930000000008", type: "DATA", volume: "2.8 GB", cost: "€5.60", time: "25 min ago" },
{ id: 6, imsi: "208930000000006", type: "VOICE", volume: "42 min", cost: "€2.10", time: "30 min ago" },
{ id: 7, imsi: "208930000000002", type: "DATA", volume: "780 MB", cost: "€1.56", time: "45 min ago" },
{ id: 8, imsi: "208930000000005", type: "SMS", volume: "1 msg", cost: "€0.10", time: "1 hr ago" },
];
const typeIcon = (t: string) => {
if (t === "DATA") return HardDrive;
if (t === "VOICE") return Phone;
return MessageSquare;
};
export default function UsagePage() {
return (
<div className="p-8 space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Usage &amp; Billing</h1>
<p className="text-muted-foreground mt-1">Monitor real-time usage and billing metrics.</p>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard title="Data Usage Today" value="48.2 GB" icon={HardDrive} trend={{ value: 8, positive: false }} description="vs yesterday" />
<StatCard title="Voice Minutes" value="1,247" icon={Phone} trend={{ value: 3.1, positive: true }} description="vs yesterday" />
<StatCard title="SMS Sent" value="389" icon={MessageSquare} description="today" />
<StatCard title="Revenue Today" value="€1,842" icon={Activity} trend={{ value: 15, positive: true }} description="vs yesterday" />
</div>
<Card>
<CardHeader>
<CardTitle>Recent Usage Events</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-3 font-medium">Type</th>
<th className="pb-3 font-medium">IMSI</th>
<th className="pb-3 font-medium">Volume</th>
<th className="pb-3 font-medium text-right">Cost</th>
<th className="pb-3 font-medium text-right">Time</th>
</tr>
</thead>
<tbody>
{usageEvents.map((evt) => {
const Icon = typeIcon(evt.type);
return (
<tr key={evt.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
<td className="py-3">
<div className="flex items-center gap-2">
<Icon className="size-4 text-muted-foreground" />
<span>{evt.type}</span>
</div>
</td>
<td className="py-3 font-mono text-xs">{evt.imsi}</td>
<td className="py-3">{evt.volume}</td>
<td className="py-3 text-right font-mono">{evt.cost}</td>
<td className="py-3 text-right text-muted-foreground">{evt.time}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,62 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Users,
LayoutDashboard,
Activity,
CreditCard,
Shield,
Settings,
Radio,
HeartPulse,
} from "lucide-react";
import { cn } from "@/lib/utils";
const navItems = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
{ href: "/subscribers", label: "Subscribers", icon: Users },
{ href: "/usage", label: "Usage & Billing", icon: Activity },
{ href: "/payments", label: "Payments", icon: CreditCard },
{ href: "/esim", label: "eSIM Profiles", icon: Radio },
{ href: "/health", label: "System Health", icon: HeartPulse },
{ href: "/chaos", label: "Chaos Engineering", icon: Shield },
{ href: "/settings", label: "Settings", icon: Settings },
];
export function Sidebar() {
const pathname = usePathname();
return (
<aside className="hidden lg:flex flex-col w-64 border-r border-border bg-sidebar text-sidebar-foreground">
<div className="flex items-center gap-2 px-6 py-5 border-b border-sidebar-border">
<Radio className="size-6 text-sidebar-primary" />
<span className="text-lg font-semibold tracking-tight">Telecom Admin</span>
</div>
<nav className="flex-1 py-4 px-3 space-y-1">
{navItems.map((item) => {
const active =
item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
active
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground"
)}
>
<item.icon className="size-4" />
{item.label}
</Link>
);
})}
</nav>
</aside>
);
}
@@ -0,0 +1,36 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface StatCardProps {
title: string;
value: string | number;
description?: string;
icon: LucideIcon;
trend?: { value: number; positive: boolean };
className?: string;
}
export function StatCard({ title, value, description, icon: Icon, trend, className }: StatCardProps) {
return (
<Card size="sm" className={cn("", className)}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
<Icon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{(description || trend) && (
<p className="text-xs text-muted-foreground mt-1">
{trend && (
<span className={cn("font-medium", trend.positive ? "text-emerald-600" : "text-red-600")}>
{trend.positive ? "+" : ""}{trend.value}%{" "}
</span>
)}
{description}
</p>
)}
</CardContent>
</Card>
);
}