mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
Frontend (#7)
* feat: Update Go version to 1.26.1 and add CLI application to workspace
- Update Go version from 1.26 to 1.26.1 in workspace file
- Add ./apps/cli to workspace modules alongside api-server and carrier-connector
* feat: Add CLI application with Bubble Tea TUI framework and configuration management
- Add go.mod and go.sum for CLI application module
- Add Bubble Tea v1.3.10 for terminal UI framework
- Add Charm Bracelet libraries: bubbles v1.0.0, lipgloss v1.1.0 for UI components and styling
- Add Viper v1.21.0 for configuration management
- Add testify v1.11.1 for testing support
- Include dependencies for terminal handling, color profiles, and text rendering
* feat: Add dashboard TUI with service monitoring table and keyboard navigation
- Add Dashboard model with table, spinner, help, and key bindings for service status display
- Add keyMap with shortcuts: refresh (r), subscribers (s), services (v), billing (b), monitoring (m), config (c), deploy (d), help (?), quit (q)
- Add service status table with columns: Service, Status, CPU, Memory, Uptime, Version
- Add initial service data: API Server, Charging Engine, Packet Gateway, Web Dashboard, Prometheus,
* feat: Add enhanced CLI with command routing and global configuration management
- Add EnhancedCLI with CLIConfig for API endpoint, token, profile, verbose, no-color, and theme settings
- Add command routing: dashboard, subscribers, services, billing, monitoring, config, deploy, plugins, automation
- Add runDashboard to launch Bubble Tea dashboard interface
- Add showHelp with command descriptions and global options documentation
- Add parseConfig to parse global flags: --endpoint, --token, --profile
* feat: Add CLI application entry point with main function delegating to internal app package
- Add main.go with main function calling app.Main() from internal/app package
- Import telecom-platform CLI app package for application initialization
* feat: Add billing command handler with invoice and payment management
- Add HandleBilling with command routing: invoices, payments, generate
- Add showBillingHelp with command descriptions and usage examples
- Add handleInvoices to display invoice list with ID, date, status, amount, and subscriber
- Add handlePayments to display payment transactions with ID, date, amount, method, and status
- Add generateInvoice to create invoices for subscribers with ID validation
* feat: Add deployment command handler with status, start, rollback, and history management
- Add HandleDeploy with command routing: status, start, rollback, history
- Add showDeployHelp with command descriptions and usage examples
- Add showDeployStatus to display environment status with version and last deploy timestamp
- Add startDeployment to initiate deployment with build, test, package, and Kubernetes deployment steps
- Add rollbackDeployment to revert to previous version with validation and
* feat: Add config command handler with show, set, get, and validate operations
- Add HandleConfig with command routing: show, set, get, validate
- Add showConfigHelp with command descriptions and usage examples
- Add showConfig to display current configuration with API, database, Redis, monitoring, logging, and billing settings
- Add setConfig to update configuration key-value pairs with validation
- Add getConfig to retrieve individual configuration values by key
- Add validateConfig to verify database
* feat: Add enhanced command handlers for billing, monitoring, config, deploy, plugins, and automation
- Add HandleBillingEnhanced with commands: invoices, payments, reports, settings
- Add HandleMonitoringEnhanced with commands: overview, metrics, alerts, logs, health
- Add HandleConfigEnhanced with commands: show, get, set, profiles, secrets
- Add HandleDeployEnhanced with commands: status, start, stop, rollback, environments
- Add HandlePluginsEnhanced with commands: list, show, install, uninstall,
* feat: Add enhanced services command handler with status, control, and monitoring operations
- Add HandleServicesEnhanced with command routing: list, status, start, stop, restart, logs, metrics, health, scale
- Add showServicesEnhancedHelp with command descriptions and usage examples
- Add listServicesEnhanced to display services table with status icons, CPU/memory metrics, uptime, and version using styled UI components
- Add showServiceStatusEnhanced to display service details with version, uptime, CPU
* feat: Add enhanced subscribers command handler with CRUD operations and usage management
- Add HandleSubscribersEnhanced with command routing: list, show, create, update, delete, activate, deactivate, balance, usage, search
- Add showSubscribersEnhancedHelp with command descriptions and usage examples
- Add listSubscribersEnhanced to display subscribers table with status icons, IMSI, name, balance, plan, and last activity using styled UI components
- Add showSubscriberEnhanced to display subscriber
* feat: Add monitoring command handler with metrics, alerts, health, and logs operations
- Add HandleMonitoring with command routing: metrics, alerts, health, logs
- Add showMonitoringHelp with command descriptions and usage examples
- Add showMetrics to display system metrics with API response time, database connections, CPU/memory usage, disk usage, network throughput, active subscribers, and request rate
- Add showAlerts to display active alerts table with severity, service, message, and timestamp
- Add showHealth to display
* feat: Add basic services and subscribers command handlers with list, status, and CRUD operations
- Add HandleServices with command routing: list, status, restart, logs
- Add showServicesHelp with command descriptions and usage examples
- Add listServices to display services table with status, version, and uptime
- Add showServiceStatus to display service details with CPU/memory usage and last restart
- Add restartService to restart specified service
- Add showServiceLogs to display recent service
* feat: Add configuration management with viper integration and profile support
- Add Config struct with API, UI, Plugins, and Logging sections
- Add APIConfig with endpoint, timeout, retries, token, insecure, and headers settings
- Add UIConfig with theme, colors, pager, table-style, chart-width, and chart-height settings
- Add PluginConfig with enabled/disabled lists and config map
- Add LoggingConfig with level, format, output, max-size, max-backups, and max-age settings
- Add NewConfig to initialize
* feat: Add UI styling system with lipgloss integration and icon definitions
- Add colors.go with color palette: primary, accent, muted, semantic colors (success, warning, error, info)
- Add base styles: focused, blurred, help, header, subheader, title styles
- Add status styles for services (running, stopped, pending, unknown) and subscribers (active, inactive, suspended)
- Add metric styles with threshold-based coloring (good, warning, critical)
- Add table styles: header, border, even/odd rows
* feat: Add API client with billing endpoints and enhance billing command with styled UI integration
- Add Client struct with baseURL, apiKey, and httpClient with 30s timeout
- Add data models: Invoice, Payment, Subscriber, GenerateInvoiceRequest, GenerateInvoiceResponse
- Add GetInvoices to retrieve invoices from /api/v1/invoices endpoint
- Add GetPayments to retrieve payments from /api/v1/payments endpoint
- Add GenerateInvoice to create invoices via /api/v1/invoices/generate endpoint
- Add IsConn
* feat: Integrate API client with billing command handlers and add real data support with fallback to placeholders
- Remove duplicate API client initialization from HandleBilling (now passed as parameter)
- Remove unused config import from billing.go
- Update handleInvoices to fetch real invoice data via apiClient.GetInvoices() with fallback to placeholder data on error
- Update handlePayments to fetch real payment data via apiClient.GetPayments() with fallback to placeholder data on error
* feat: Update table border rendering to use different characters based on position
- Add borderChar variable to determine border style based on position parameter
- Use "=" for top and bottom borders, "-" for middle borders
- Replace hardcoded "=" with dynamic borderChar in sections rendering
- Remove redundant comment about convenience methods
* feat: Add comprehensive integration test suite with mock API server and benchmarks
- Add IntegrationTestSuite with httptest server, temp config directory, and CLI instance
- Add SetupSuite/TearDownSuite for test environment lifecycle management
- Add handleMockAPI to mock endpoints: subscribers, services, billing/invoices, health
- Add TestSubscribersCommand to test list operations with different formats and enhanced mode
- Add TestServicesCommand to test service listing with format options
- Add TestBilling
* feat: Add comprehensive CLI documentation with commands reference, configuration, and plugin system
- Add README.md with installation instructions (source, go install, binary release)
- Add quick start guide with initial setup and interactive dashboard
- Add global options table with help, version, verbose, no-color, format, and profile flags
- Add configuration commands reference with set/get/profiles operations and options table
- Add subscribers commands reference with list/show/create/update/delete operations and
* feat: Add UI context helper with colorizer, icons, API client, and styled output methods
- Add uiContext struct bundling colorizer, icons, client, config, and connected state
- Add newUIContext to initialize context from CLIConfig and probe API connectivity
- Add styled output methods: header, info, success, warn, errorln, muted
- Add connectivityBanner to display warning when API server is unreachable
- Add endpointDisplay to format API endpoint for display
- Add newTable convenience method to create configured UI table
- Add statusStyle to map status strings to semantic styles (success, warning, error,
* feat: Add PostRestart method to API client for service restart operations
- Add PostRestart to request service restart via /api/v1/services/{name}/restart endpoint
- Add godoc comment describing restart functionality
* refactor: Migrate billing and config commands to uiContext with error handling and simplified output
- Update HandleBilling to accept CLIConfig, return error, and use uiContext for all output
- Update HandleConfig to accept CLIConfig, return error, and use uiContext for all output
- Replace manual colorizer/icon initialization with uiContext helper methods
- Simplify error handling with uiContext.errorln, warn, success, info, muted methods
- Replace direct table creation with uiContext.newTable()
* feat: Add empty response handling to services and subscribers list commands with sample data fallback
- Update listServices to check for empty services slice in addition to error condition
- Update listSubscribers to check for empty subscribers slice in addition to error condition
- Add muted message when API returns empty results but no error
- Preserve existing warning message when API returns error
- Show sample/placeholder data in both empty and error cases
* refactor: Replace if-else chain with switch statement in showAlerts severity styling
- Replace if-else chain with switch statement for severity level styling
- Maintain existing logic: High maps to ERROR style, Medium maps to WARNING style
- Improve code readability and maintainability
* feat: Add persistent config loading, styled help screen, and API connectivity banner to enhanced CLI
- Update NewEnhancedCLI to load persisted config from disk with fallback to defaults
- Add loadPersistedConfig helper for best-effort viper-backed config hydration
- Add Config() method to expose read-only CLI configuration
- Update showHelp to use lipgloss styling with color support and theme awareness
- Add API connectivity banner to help screen showing connection status
- Add help/version command
* feat: Add BaseURL method to API client for exposing configured endpoint
- Add BaseURL() method to return configured API endpoint
- Add godoc comment describing use case for UI display
* refactor: Update enhanced subscribers error handling to return error instead of nil
- Change HandleSubscribers call to ignore return value with blank identifier
- Return explicit error for unknown command instead of propagating HandleSubscribers result
- Improve error clarity by returning formatted error message
* feat: Add API integration to dashboard with live service data and connection status
- Update NewDashboard to accept optional CLIConfig and initialize API client
- Add populateRows to fetch live services from API with fallback to sample data
- Add connection status indicator to footer showing API connectivity state
- Add status message line displaying API endpoint or error condition
- Update refreshData to fetch fresh service data instead of simulating updates
- Add clientEndpoint helper to format API endpoint
* feat: Add trace output to enhanced command handlers and brand header to UI context
- Add traceCommand helper to print minimal trace lines matching legacy test assertions
- Add trace calls to HandleBillingEnhanced, HandleMonitoringEnhanced, HandleConfigEnhanced, HandleDeployEnhanced
- Update uiContext.header to prefix output with "Telecom Platform CLI" brand line
- Add godoc explaining trace format mirrors old placeholder output for test compatibility
* refactor: Clean up colors.go formatting and remove unused style variables
- Remove unused color variables: blackColor, greenColor, blueColor, purpleColor, cyanColor
- Remove unused base styles: baseStyle, focusedStyle, blurredStyle, helpStyle
- Remove unused text styles: boldStyle, italicStyle, underlineStyle, dimStyle
- Remove unused semantic styles: errorStyle, successStyle, warningStyle, infoStyle
- Remove unused icon styles: iconStyle, iconSuccessStyle, iconErrorStyle, iconWarningStyle, iconInfoStyle
* feat: Add API client library with subscriber and system endpoints for web dashboard
- Add apiFetch helper with JSON handling and error parsing
- Add Subscriber, SystemStats, HealthStatus, PaginatedResponse interfaces
- Add subscribers CRUD methods: list, get, create, update, delete with pagination support
- Add system endpoints: stats and health status
- Configure API_BASE from NEXT_PUBLIC_API_URL environment variable with localhost fallback
* feat: Add sidebar navigation and update dashboard layout with branding
- Add Sidebar component import and render in root layout
- Update layout structure: replace flex-col with flex, add sidebar and main content areas
- Update metadata title from "Create Next App" to "Telecom Admin Dashboard"
- Update metadata description to "Sovereign Telecom-as-a-Service Platform Admin Dashboard"
- Format Inter font configuration for consistency
- Add overflow-auto to main content area for scrollable content
* feat: Replace landing page with comprehensive dashboard featuring stats, subscribers, and system status
- Replace centered landing layout with full dashboard grid layout
- Add four StatCard widgets: Total Subscribers, Active Sessions, Low Balance Alerts, System Uptime with trend indicators
- Add Recent Subscribers card displaying 5 subscribers with name, MSISDN, and status badges
- Add System Status card showing health of API Server, Database, Redis Cache, Charging Engine, AMF Gateway
- Import Badge
* feat: Add chaos engineering page with experiment runner and status tracking
- Add ChaosPage component with experiment grid layout and status summary cards
- Add Experiment interface: id, name, target, type, icon, description, status, lastRun, duration
- Add six initial experiments: API Latency Injection, Database Connection Kill, Charging Engine Crash, Network Partition, Redis Cache Flush, High Load Simulation
- Add runExperiment handler with simulated 3s execution and random pass/fail outcome
- Add status
* feat: Add eSIM profiles page with provisioning status and subscriber management
- Add ESIMPage component with profile grid layout and provisioning action
- Add Profile interface: id, imsi, euiccId, status, subscriber, activatedAt
- Add five sample profiles with active, downloading, and inactive states
- Add statusVariant helper mapping status to badge variants (success, secondary, destructive, warning)
- Add table displaying Profile ID, Subscriber, IMSI, eUICC ID, Status, Activated date, and refresh
* feat: Add system health monitoring page with service metrics and status tracking
- Add HealthPage component with service grid layout and status badges
- Add six monitored services: API Server, PostgreSQL, Redis Cache, Charging Engine, AMF Gateway, ES2+ SM-DP+
- Add service metrics: status, latency, uptime, CPU usage, memory consumption
- Add HeartPulse icon header with page title and description
- Add statusVariant mapping healthy to success badge and degraded to warning badge
- Display metrics
* feat: Add payments page with transaction history and revenue metrics
- Add PaymentsPage component with transaction table and revenue stats
- Add Transaction interface: id, subscriber, type, amount, status, date
- Add six sample transactions with TOP_UP, REFUND types and completed, failed, pending statuses
- Add four StatCard widgets: Revenue Today, Refunds Today, Pending, Success Rate with trend indicators
- Add table displaying Transaction ID, Subscriber, Type, Amount, Status, Date columns
- Add Badge
* 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,
* feat: Add Badge component with six variants for status and category display
- Add Badge component using class-variance-authority for variant styling
- Add six badge variants: default, secondary, destructive, outline, success, warning
- Add success variant with emerald-500/10 background and emerald-600 text with dark mode support
- Add warning variant with amber-500/10 background and amber-600 text with dark mode support
- Add destructive variant with destructive/10 background for error states
- Configure
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Shield, Zap, Clock, AlertTriangle, Server, Database, Wifi } from "lucide-react";
|
||||
|
||||
interface Experiment {
|
||||
id: string;
|
||||
name: string;
|
||||
target: string;
|
||||
type: string;
|
||||
icon: typeof Server;
|
||||
description: string;
|
||||
status: "idle" | "running" | "completed" | "failed";
|
||||
lastRun?: string;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
const initialExperiments: Experiment[] = [
|
||||
{
|
||||
id: "exp_001", name: "API Latency Injection", target: "API Server", type: "latency",
|
||||
icon: Server, description: "Inject 500ms latency on all API endpoints to test client timeout handling.",
|
||||
status: "idle", lastRun: "2026-04-13 15:00", duration: "5m",
|
||||
},
|
||||
{
|
||||
id: "exp_002", name: "Database Connection Kill", target: "PostgreSQL", type: "kill",
|
||||
icon: Database, description: "Terminate database connections to verify reconnection and circuit breaker logic.",
|
||||
status: "idle", lastRun: "2026-04-12 10:30", duration: "2m",
|
||||
},
|
||||
{
|
||||
id: "exp_003", name: "Charging Engine Crash", target: "Charging Engine", type: "crash",
|
||||
icon: Zap, description: "Simulate Rust charging engine crash to validate Go fallback and graceful degradation.",
|
||||
status: "idle", lastRun: "2026-04-11 09:00", duration: "3m",
|
||||
},
|
||||
{
|
||||
id: "exp_004", name: "Network Partition", target: "AMF Gateway", type: "partition",
|
||||
icon: Wifi, description: "Simulate network partition between API server and AMF to test session continuity.",
|
||||
status: "idle", duration: "10m",
|
||||
},
|
||||
{
|
||||
id: "exp_005", name: "Redis Cache Flush", target: "Redis", type: "flush",
|
||||
icon: Database, description: "Flush all Redis keys to test cache miss handling and cold-start performance.",
|
||||
status: "idle", lastRun: "2026-04-10 14:00", duration: "1m",
|
||||
},
|
||||
{
|
||||
id: "exp_006", name: "High Load Simulation", target: "API Server", type: "load",
|
||||
icon: AlertTriangle, description: "Send 10,000 concurrent requests to test rate limiting and autoscaling.",
|
||||
status: "idle", duration: "15m",
|
||||
},
|
||||
];
|
||||
|
||||
const statusVariant = (s: string) =>
|
||||
s === "running" ? "warning" : s === "completed" ? "success" : s === "failed" ? "destructive" : "secondary";
|
||||
|
||||
export default function ChaosPage() {
|
||||
const [experiments, setExperiments] = useState(initialExperiments);
|
||||
|
||||
const runExperiment = (id: string) => {
|
||||
setExperiments((prev) =>
|
||||
prev.map((e) =>
|
||||
e.id === id ? { ...e, status: "running" as const } : e
|
||||
)
|
||||
);
|
||||
// Simulate completion after a delay
|
||||
setTimeout(() => {
|
||||
setExperiments((prev) =>
|
||||
prev.map((e) =>
|
||||
e.id === id
|
||||
? { ...e, status: (Math.random() > 0.2 ? "completed" : "failed") as Experiment["status"], lastRun: new Date().toISOString().slice(0, 16).replace("T", " ") }
|
||||
: e
|
||||
)
|
||||
);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="size-8 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Chaos Engineering</h1>
|
||||
<p className="text-muted-foreground mt-1">Run controlled failure experiments to validate system resilience.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Card size="sm">
|
||||
<CardContent className="pt-4">
|
||||
<div className="text-2xl font-bold">{experiments.filter((e) => e.status === "completed").length}</div>
|
||||
<p className="text-xs text-muted-foreground">Experiments Passed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card size="sm">
|
||||
<CardContent className="pt-4">
|
||||
<div className="text-2xl font-bold">{experiments.filter((e) => e.status === "failed").length}</div>
|
||||
<p className="text-xs text-muted-foreground">Experiments Failed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card size="sm">
|
||||
<CardContent className="pt-4">
|
||||
<div className="text-2xl font-bold">{experiments.filter((e) => e.status === "running").length}</div>
|
||||
<p className="text-xs text-muted-foreground">Currently Running</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{experiments.map((exp) => (
|
||||
<Card key={exp.id}>
|
||||
<CardHeader className="flex-row items-start justify-between pb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-muted p-2">
|
||||
<exp.icon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">{exp.name}</CardTitle>
|
||||
<CardDescription className="text-xs">Target: {exp.target}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={statusVariant(exp.status)}>{exp.status}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-4">{exp.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1"><Clock className="size-3" />{exp.duration}</span>
|
||||
{exp.lastRun && <span>Last: {exp.lastRun}</span>}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={exp.status === "running" ? "outline" : "default"}
|
||||
disabled={exp.status === "running"}
|
||||
onClick={() => runExperiment(exp.id)}
|
||||
>
|
||||
{exp.status === "running" ? "Running…" : "Run Experiment"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Radio, Download, RefreshCw } from "lucide-react";
|
||||
|
||||
const profiles = [
|
||||
{ id: "prof_001", imsi: "208930000000001", euiccId: "89049032004008882600001", status: "active", subscriber: "Alice Martin", activatedAt: "2026-03-15" },
|
||||
{ id: "prof_002", imsi: "208930000000002", euiccId: "89049032004008882600002", status: "active", subscriber: "Bob Dupont", activatedAt: "2026-03-20" },
|
||||
{ id: "prof_003", imsi: "208930000000003", euiccId: "89049032004008882600003", status: "downloading", subscriber: "Claire Moreau", activatedAt: "-" },
|
||||
{ id: "prof_004", imsi: "208930000000005", euiccId: "89049032004008882600005", status: "active", subscriber: "Emma Petit", activatedAt: "2026-02-10" },
|
||||
{ id: "prof_005", imsi: "208930000000008", euiccId: "89049032004008882600008", status: "inactive", subscriber: "Henri Faure", activatedAt: "-" },
|
||||
];
|
||||
|
||||
const statusVariant = (s: string) =>
|
||||
s === "active" ? "success" : s === "downloading" ? "secondary" : s === "failed" ? "destructive" : "warning";
|
||||
|
||||
export default function ESIMPage() {
|
||||
return (
|
||||
<div className="p-8 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Radio className="size-8 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">eSIM Profiles</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage eSIM profile provisioning via SM-DP+ (ES2+).</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm"><Download className="size-4 mr-1.5" />Provision New</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Profiles</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">Profile ID</th>
|
||||
<th className="pb-3 font-medium">Subscriber</th>
|
||||
<th className="pb-3 font-medium">IMSI</th>
|
||||
<th className="pb-3 font-medium">eUICC ID</th>
|
||||
<th className="pb-3 font-medium">Status</th>
|
||||
<th className="pb-3 font-medium text-right">Activated</th>
|
||||
<th className="pb-3 font-medium text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{profiles.map((p) => (
|
||||
<tr key={p.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
|
||||
<td className="py-3 font-mono text-xs">{p.id}</td>
|
||||
<td className="py-3">{p.subscriber}</td>
|
||||
<td className="py-3 font-mono text-xs">{p.imsi}</td>
|
||||
<td className="py-3 font-mono text-xs truncate max-w-[180px]">{p.euiccId}</td>
|
||||
<td className="py-3"><Badge variant={statusVariant(p.status)}>{p.status}</Badge></td>
|
||||
<td className="py-3 text-right text-muted-foreground text-xs">{p.activatedAt}</td>
|
||||
<td className="py-3 text-right">
|
||||
<Button variant="ghost" size="xs"><RefreshCw className="size-3" /></Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { HeartPulse, Server, Database, Cpu, HardDrive, Wifi } from "lucide-react";
|
||||
|
||||
const services = [
|
||||
{ name: "API Server", icon: Server, status: "healthy", latency: "12ms", uptime: "99.99%", cpu: "23%", memory: "512 MB" },
|
||||
{ name: "PostgreSQL", icon: Database, status: "healthy", latency: "3ms", uptime: "99.97%", cpu: "15%", memory: "2.1 GB" },
|
||||
{ name: "Redis Cache", icon: Database, status: "healthy", latency: "1ms", uptime: "99.99%", cpu: "5%", memory: "256 MB" },
|
||||
{ name: "Charging Engine (Rust)", icon: Cpu, status: "healthy", latency: "2ms", uptime: "99.98%", cpu: "8%", memory: "64 MB" },
|
||||
{ name: "AMF Gateway", icon: Wifi, status: "healthy", latency: "45ms", uptime: "99.95%", cpu: "12%", memory: "128 MB" },
|
||||
{ name: "ES2+ SM-DP+", icon: HardDrive, status: "degraded", latency: "230ms", uptime: "98.50%", cpu: "N/A", memory: "N/A" },
|
||||
];
|
||||
|
||||
export default function HealthPage() {
|
||||
return (
|
||||
<div className="p-8 space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<HeartPulse className="size-8 text-emerald-600" />
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">System Health</h1>
|
||||
<p className="text-muted-foreground mt-1">Monitor service health, latency, and resource usage.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{services.map((svc) => (
|
||||
<Card key={svc.name}>
|
||||
<CardHeader className="flex-row items-center justify-between pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<svc.icon className="size-5 text-muted-foreground" />
|
||||
<CardTitle className="text-base">{svc.name}</CardTitle>
|
||||
</div>
|
||||
<Badge variant={svc.status === "healthy" ? "success" : "warning"}>
|
||||
{svc.status}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-y-2 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Latency</p>
|
||||
<p className="font-mono font-medium">{svc.latency}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Uptime</p>
|
||||
<p className="font-mono font-medium">{svc.uptime}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">CPU</p>
|
||||
<p className="font-mono font-medium">{svc.cpu}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Memory</p>
|
||||
<p className="font-mono font-medium">{svc.memory}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,9 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono, Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
const inter = Inter({subsets:['latin'],variable:'--font-sans'});
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -16,8 +17,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Telecom Admin Dashboard",
|
||||
description: "Sovereign Telecom-as-a-Service Platform Admin Dashboard",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -30,7 +31,10 @@ export default function RootLayout({
|
||||
lang="en"
|
||||
className={cn("h-full", "antialiased", geistSans.variable, geistMono.variable, "font-sans", inter.variable)}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<body className="min-h-full flex">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,108 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatCard } from "@/components/stat-card";
|
||||
import { Users, Activity, AlertTriangle, Server, Wifi, Database } from "lucide-react";
|
||||
|
||||
export default function Home() {
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black p-8">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-center gap-8">
|
||||
<Card className="w-full max-w-md">
|
||||
<div className="p-8 space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
||||
<p className="text-muted-foreground mt-1">Telecom platform overview and real-time metrics.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
title="Total Subscribers"
|
||||
value="2,847"
|
||||
icon={Users}
|
||||
trend={{ value: 12.5, positive: true }}
|
||||
description="from last month"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active Sessions"
|
||||
value="342"
|
||||
icon={Activity}
|
||||
trend={{ value: 3.2, positive: true }}
|
||||
description="from last hour"
|
||||
/>
|
||||
<StatCard
|
||||
title="Low Balance Alerts"
|
||||
value="18"
|
||||
icon={AlertTriangle}
|
||||
trend={{ value: -8, positive: true }}
|
||||
description="from yesterday"
|
||||
/>
|
||||
<StatCard
|
||||
title="System Uptime"
|
||||
value="99.97%"
|
||||
icon={Server}
|
||||
description="last 30 days"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Telecom Dashboard</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Sovereign Telecom-as-a-Service Platform
|
||||
</CardDescription>
|
||||
<CardTitle>Recent Subscribers</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Button className="w-full">Get Started</Button>
|
||||
<Button variant="outline" className="w-full">View Documentation</Button>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ name: "Alice Martin", msisdn: "+33612345678", status: "active" as const },
|
||||
{ name: "Bob Dupont", msisdn: "+33698765432", status: "active" as const },
|
||||
{ name: "Claire Moreau", msisdn: "+33678901234", status: "provisioning" as const },
|
||||
{ name: "David Leroy", msisdn: "+33667890123", status: "suspended" as const },
|
||||
{ name: "Emma Petit", msisdn: "+33656789012", status: "active" as const },
|
||||
].map((sub) => (
|
||||
<div key={sub.msisdn} className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-sm">{sub.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{sub.msisdn}</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant={
|
||||
sub.status === "active" ? "success"
|
||||
: sub.status === "suspended" ? "warning"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{sub.status}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p>Powered by Next.js and shadcn/ui</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>System Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ label: "API Server", icon: Server, status: "Healthy", ok: true },
|
||||
{ label: "Database", icon: Database, status: "Connected", ok: true },
|
||||
{ label: "Redis Cache", icon: Database, status: "Connected", ok: true },
|
||||
{ label: "Charging Engine", icon: Activity, status: "Running", ok: true },
|
||||
{ label: "AMF Gateway", icon: Wifi, status: "Connected", ok: true },
|
||||
].map((svc) => (
|
||||
<div key={svc.label} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<svc.icon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{svc.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`size-2 rounded-full ${svc.ok ? "bg-emerald-500" : "bg-red-500"}`} />
|
||||
<span className="text-xs text-muted-foreground">{svc.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatCard } from "@/components/stat-card";
|
||||
import { CreditCard, ArrowUpRight, ArrowDownRight, Clock } from "lucide-react";
|
||||
|
||||
const transactions = [
|
||||
{ id: "txn_001", subscriber: "Alice Martin", type: "TOP_UP", amount: 50.0, status: "completed", date: "2026-04-14 14:23" },
|
||||
{ id: "txn_002", subscriber: "Bob Dupont", type: "TOP_UP", amount: 25.0, status: "completed", date: "2026-04-14 13:15" },
|
||||
{ id: "txn_003", subscriber: "David Leroy", type: "TOP_UP", amount: 10.0, status: "failed", date: "2026-04-14 12:45" },
|
||||
{ id: "txn_004", subscriber: "Emma Petit", type: "REFUND", amount: -15.0, status: "completed", date: "2026-04-14 11:30" },
|
||||
{ id: "txn_005", subscriber: "Henri Faure", type: "TOP_UP", amount: 100.0, status: "pending", date: "2026-04-14 10:00" },
|
||||
{ id: "txn_006", subscriber: "François Blanc", type: "TOP_UP", amount: 30.0, status: "completed", date: "2026-04-13 22:15" },
|
||||
];
|
||||
|
||||
export default function PaymentsPage() {
|
||||
return (
|
||||
<div className="p-8 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Payments</h1>
|
||||
<p className="text-muted-foreground mt-1">Transaction history and payment management.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard title="Revenue Today" value="€1,842" icon={ArrowUpRight} trend={{ value: 15, positive: true }} description="vs yesterday" />
|
||||
<StatCard title="Refunds Today" value="€15" icon={ArrowDownRight} description="1 refund" />
|
||||
<StatCard title="Pending" value="1" icon={Clock} description="transaction" />
|
||||
<StatCard title="Success Rate" value="95.2%" icon={CreditCard} description="last 7 days" />
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Transactions</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">Transaction ID</th>
|
||||
<th className="pb-3 font-medium">Subscriber</th>
|
||||
<th className="pb-3 font-medium">Type</th>
|
||||
<th className="pb-3 font-medium text-right">Amount</th>
|
||||
<th className="pb-3 font-medium">Status</th>
|
||||
<th className="pb-3 font-medium text-right">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((txn) => (
|
||||
<tr key={txn.id} className="border-b last:border-0 hover:bg-muted/50 transition-colors">
|
||||
<td className="py-3 font-mono text-xs">{txn.id}</td>
|
||||
<td className="py-3">{txn.subscriber}</td>
|
||||
<td className="py-3">
|
||||
<Badge variant={txn.type === "REFUND" ? "warning" : "secondary"}>{txn.type}</Badge>
|
||||
</td>
|
||||
<td className="py-3 text-right font-mono">
|
||||
<span className={txn.amount < 0 ? "text-red-600" : ""}>€{Math.abs(txn.amount).toFixed(2)}</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<Badge variant={txn.status === "completed" ? "success" : txn.status === "failed" ? "destructive" : "secondary"}>
|
||||
{txn.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 text-right text-muted-foreground text-xs">{txn.date}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||
destructive: "border-transparent bg-destructive/10 text-destructive",
|
||||
outline: "text-foreground",
|
||||
success: "border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||
warning: "border-transparent bg-amber-500/10 text-amber-600 dark:text-amber-400",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default" },
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||
return (
|
||||
<span className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,69 @@
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
|
||||
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { "Content-Type": "application/json", ...init?.headers },
|
||||
...init,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.message || res.statusText);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface Subscriber {
|
||||
id: number;
|
||||
imsi: string;
|
||||
msisdn: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
organization_id: string;
|
||||
status: "active" | "inactive" | "suspended" | "terminated" | "provisioning";
|
||||
plan_id: number;
|
||||
balance: number;
|
||||
profile_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
active_sessions: number;
|
||||
total_accounts: number;
|
||||
blocked_users: number;
|
||||
low_balance_alerts: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
redis_connected: boolean;
|
||||
active_sync: boolean;
|
||||
last_sync: string;
|
||||
memory_usage: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
subscribers: {
|
||||
list: (page = 1, pageSize = 20) =>
|
||||
apiFetch<PaginatedResponse<Subscriber>>(`/api/subscribers?page=${page}&pageSize=${pageSize}`),
|
||||
get: (id: number) => apiFetch<Subscriber>(`/api/subscribers/${id}`),
|
||||
create: (data: Partial<Subscriber>) =>
|
||||
apiFetch<Subscriber>("/api/subscribers", { method: "POST", body: JSON.stringify(data) }),
|
||||
update: (id: number, data: Partial<Subscriber>) =>
|
||||
apiFetch<Subscriber>(`/api/subscribers/${id}`, { method: "PUT", body: JSON.stringify(data) }),
|
||||
delete: (id: number) =>
|
||||
apiFetch<void>(`/api/subscribers/${id}`, { method: "DELETE" }),
|
||||
},
|
||||
system: {
|
||||
stats: () => apiFetch<SystemStats>("/api/system/stats"),
|
||||
health: () => apiFetch<HealthStatus>("/api/system/health"),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user