mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-20 11:54:25 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user