feat: Add analytics and churn dashboard pages with metrics visualization, risk analysis, and multi-tab interfaces

- Add analytics page with overview, churn, market, maintenance, and pricing tabs
- Add churn page with predictions table, risk filtering, and retention recommendations
- Add metrics cards for subscribers, churn rate, market share, and system uptime
- Add risk distribution visualization with progress bars and color-coded badges
- Add market metrics with country breakdown and penetration rates
- Add maintenance metrics with asset health, MTTF, MTTR
This commit is contained in:
nutcas3
2026-05-03 13:28:44 +03:00
parent b7a9b9f09e
commit 12887765de
11 changed files with 2033 additions and 55 deletions
@@ -0,0 +1,341 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import { BarChart3, TrendingUp, TrendingDown, AlertCircle } from "lucide-react";
import { apiClient, ChurnMetrics, MarketMetrics, PricingMetrics, MaintenanceMetrics } from "@/lib/api-client";
export default function AnalyticsPage() {
const [metrics, setMetrics] = useState<{
churn?: ChurnMetrics;
market?: MarketMetrics;
maintenance?: MaintenanceMetrics;
pricing?: PricingMetrics;
} | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchMetrics = async () => {
try {
setLoading(true);
setError(null);
// Fetch all metrics in parallel
const [churnResponse, marketResponse, maintenanceResponse, pricingResponse] = await Promise.all([
apiClient.getChurnMetrics(),
apiClient.getMarketMetrics(),
apiClient.getMaintenanceMetrics(),
apiClient.getPricingMetrics(),
]);
if (churnResponse.error || marketResponse.error || maintenanceResponse.error || pricingResponse.error) {
setError('Failed to fetch some metrics');
}
setMetrics({
churn: churnResponse.data,
market: marketResponse.data,
maintenance: maintenanceResponse.data,
pricing: pricingResponse.data,
});
} catch (err) {
console.error('Failed to fetch analytics metrics:', err);
setError('Failed to load analytics data');
} finally {
setLoading(false);
}
};
fetchMetrics();
}, []);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertCircle className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Error Loading Analytics</h3>
<p className="text-gray-600">{error}</p>
<Button onClick={() => window.location.reload()} className="mt-4">
Retry
</Button>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Analytics Dashboard</h1>
<Button>
<BarChart3 className="mr-2 h-4 w-4" />
Export Report
</Button>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="churn">Churn Analysis</TabsTrigger>
<TabsTrigger value="market">Market Analytics</TabsTrigger>
<TabsTrigger value="maintenance">Maintenance</TabsTrigger>
<TabsTrigger value="pricing">Pricing</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Subscribers</CardTitle>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.churn?.totalSubscribers?.toLocaleString() || 'N/A'}</div>
<p className="text-xs text-muted-foreground">+12.5% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Churn Rate</CardTitle>
<TrendingDown className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.churn?.churnRate ? `${metrics.churn.churnRate}%` : 'N/A'}</div>
<p className="text-xs text-muted-foreground">-0.3% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Market Share</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.market?.marketShare ? `${(metrics.market.marketShare * 100).toFixed(3)}%` : 'N/A'}</div>
<p className="text-xs text-muted-foreground">+0.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">System Uptime</CardTitle>
<AlertCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.maintenance?.uptime ? `${metrics.maintenance.uptime}%` : 'N/A'}</div>
<p className="text-xs text-muted-foreground">Excellent</p>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="churn" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Churn Metrics</CardTitle>
<CardDescription>Customer churn analysis for the current period</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between">
<span>Total Subscribers</span>
<span className="font-semibold">{metrics?.churn?.totalSubscribers?.toLocaleString() || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Churned Subscribers</span>
<span className="font-semibold text-red-600">{metrics?.churn?.churnedSubscribers?.toLocaleString() || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Monthly Churn Rate</span>
<span className="font-semibold">{metrics?.churn?.monthlyChurnRate ? `${metrics.churn.monthlyChurnRate}%` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Average Tenure</span>
<span className="font-semibold">{metrics?.churn?.averageTenureDays ? `${metrics.churn.averageTenureDays} days` : 'N/A'}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Risk Distribution</CardTitle>
<CardDescription>Customers at risk of churn</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(metrics?.churn?.riskDistribution || {}).map(([level, count]) => (
<div key={level} className="space-y-2">
<div className="flex justify-between">
<span className="capitalize">{level}</span>
<span>{count?.toLocaleString() || '0'}</span>
</div>
<Progress
value={count && metrics?.churn?.totalSubscribers ? (count / metrics.churn.totalSubscribers) * 100 : 0}
className="h-2"
/>
</div>
))}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="market" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Market Metrics</CardTitle>
<CardDescription>Market penetration and growth</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between">
<span>Total Market Size</span>
<span className="font-semibold">{metrics?.market?.totalMarketSize ? `${(metrics.market.totalMarketSize / 1000000000).toFixed(1)}B` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Our Subscribers</span>
<span className="font-semibold">{metrics?.market?.ourSubscribers?.toLocaleString() || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Market Share</span>
<span className="font-semibold">{metrics?.market?.marketShare ? `${(metrics.market.marketShare * 100).toFixed(3)}%` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Growth Rate</span>
<span className="font-semibold text-green-600">{metrics?.market?.growthRate ? `+${metrics.market.growthRate}%` : 'N/A'}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Market by Country</CardTitle>
<CardDescription>Performance across regions</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(metrics?.market?.byCountry || {}).map(([country, data]: [string, any]) => (
<div key={country} className="space-y-2">
<div className="flex justify-between">
<span>{country}</span>
<span>{data?.penetration ? `${(data.penetration * 100).toFixed(2)}%` : 'N/A'}</span>
</div>
<Progress
value={data?.penetration ? data.penetration * 100 : 0}
className="h-2"
/>
</div>
))}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="maintenance" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>System Health</CardTitle>
<CardDescription>Infrastructure maintenance metrics</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between">
<span>Total Assets</span>
<span className="font-semibold">{metrics?.maintenance?.totalAssets || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Healthy Assets</span>
<span className="font-semibold text-green-600">{metrics?.maintenance?.healthyAssets || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Assets Needing Attention</span>
<span className="font-semibold text-yellow-600">{metrics?.maintenance?.assetsNeedingAttention || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>System Uptime</span>
<span className="font-semibold">{metrics?.maintenance?.uptime ? `${metrics.maintenance.uptime}%` : 'N/A'}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Performance Metrics</CardTitle>
<CardDescription>Mean time metrics</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between">
<span>Mean Time To Failure</span>
<span className="font-semibold">{metrics?.maintenance?.meanTimeToFailure ? `${(metrics.maintenance.meanTimeToFailure / 24 / 365).toFixed(1)} years` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Mean Time To Repair</span>
<span className="font-semibold">{metrics?.maintenance?.meanTimeToRepair ? `${metrics.maintenance.meanTimeToRepair} hours` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Asset Health Score</span>
<Badge variant="secondary">Excellent</Badge>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="pricing" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Revenue Metrics</CardTitle>
<CardDescription>Pricing and revenue analysis</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between">
<span>Total Revenue</span>
<span className="font-semibold">{metrics?.pricing?.totalRevenue ? `${(metrics.pricing.totalRevenue / 1000000).toFixed(1)}M` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>ARPU</span>
<span className="font-semibold">{metrics?.pricing?.arpu ? `$${metrics.pricing.arpu}` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Price Elasticity</span>
<span className="font-semibold">{metrics?.pricing?.priceElasticity || 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Competitive Index</span>
<span className="font-semibold">{metrics?.pricing?.competitiveIndex || 'N/A'}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Optimization</CardTitle>
<CardDescription>Pricing optimization metrics</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between">
<span>Optimization ROI</span>
<span className="font-semibold text-green-600">{metrics?.pricing?.optimizationRoi ? `+${metrics.pricing.optimizationRoi}%` : 'N/A'}</span>
</div>
<div className="flex justify-between">
<span>Price Elasticity</span>
<span className="font-semibold">{metrics?.pricing?.priceElasticity || 'N/A'}</span>
</div>
<Button className="w-full mt-4">
Optimize Pricing
</Button>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
);
}
+272
View File
@@ -0,0 +1,272 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { TrendingDown, AlertTriangle, Users, Target } from "lucide-react";
import { apiClient, ChurnMetrics, ChurnPrediction } from "@/lib/api-client";
export default function ChurnPage() {
const [predictions, setPredictions] = useState<ChurnPrediction[]>([]);
const [metrics, setMetrics] = useState<ChurnMetrics | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedRiskLevel, setSelectedRiskLevel] = useState<string>("all");
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
// Fetch churn metrics and predictions in parallel
const [metricsResponse, predictionsResponse] = await Promise.all([
apiClient.getChurnMetrics(),
apiClient.getChurnPredictions(selectedRiskLevel === "all" ? undefined : selectedRiskLevel, 100),
]);
if (metricsResponse.error || predictionsResponse.error) {
setError('Failed to fetch some churn data');
}
setMetrics(metricsResponse.data || null);
setPredictions(predictionsResponse.data || []);
} catch (err) {
console.error('Failed to fetch churn data:', err);
setError('Failed to load churn analysis data');
} finally {
setLoading(false);
}
};
fetchData();
}, [selectedRiskLevel]);
const filteredPredictions = predictions.filter(p =>
selectedRiskLevel === "all" || p.riskLevel === selectedRiskLevel
);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Error Loading Churn Analysis</h3>
<p className="text-gray-600">{error}</p>
<Button onClick={() => window.location.reload()} className="mt-4">
Retry
</Button>
</div>
</div>
);
}
const getRiskBadgeVariant = (level: string) => {
switch (level) {
case "critical": return "destructive";
case "high": return "destructive";
case "medium": return "secondary";
case "low": return "outline";
default: return "secondary";
}
};
const getRiskColor = (level: string) => {
switch (level) {
case "critical": return "text-red-600";
case "high": return "text-orange-600";
case "medium": return "text-yellow-600";
case "low": return "text-green-600";
default: return "text-gray-600";
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold flex items-center">
<TrendingDown className="mr-3 h-8 w-8 text-red-600" />
Churn Analysis
</h1>
<Button>
<Target className="mr-2 h-4 w-4" />
Run Prediction Model
</Button>
</div>
{/* Metrics Overview */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Subscribers</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.totalSubscribers.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">Active customers</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Churn Rate</CardTitle>
<TrendingDown className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{metrics?.churnRate}%</div>
<p className="text-xs text-muted-foreground">Monthly churn</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">At Risk Customers</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">7,500</div>
<p className="text-xs text-muted-foreground">High & critical risk</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Avg Tenure</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.averageTenureDays} days</div>
<p className="text-xs text-muted-foreground">Customer lifetime</p>
</CardContent>
</Card>
</div>
{/* Risk Distribution */}
<Card>
<CardHeader>
<CardTitle>Risk Distribution</CardTitle>
<CardDescription>Customer churn risk levels across the subscriber base</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(metrics?.riskDistribution || {}).map(([level, count]) => (
<div key={level} className="space-y-2">
<div className="flex justify-between items-center">
<span className="capitalize font-medium">{level}</span>
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">{count?.toLocaleString()}</span>
<Badge variant={getRiskBadgeVariant(level) as any}>{level}</Badge>
</div>
</div>
<Progress
value={count && metrics?.totalSubscribers ? (count / metrics.totalSubscribers) * 100 : 0}
className="h-3"
/>
</div>
))}
</CardContent>
</Card>
{/* Filter Controls */}
<div className="flex items-center space-x-4">
<span className="text-sm font-medium">Filter by risk level:</span>
<div className="flex space-x-2">
{["all", "critical", "high", "medium", "low"].map((level) => (
<Button
key={level}
variant={selectedRiskLevel === level ? "default" : "outline"}
size="sm"
onClick={() => setSelectedRiskLevel(level)}
>
{level.charAt(0).toUpperCase() + level.slice(1)}
</Button>
))}
</div>
</div>
{/* At-Risk Customers Table */}
<Card>
<CardHeader>
<CardTitle>At-Risk Customers</CardTitle>
<CardDescription>Customers with elevated churn risk requiring attention</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Profile ID</TableHead>
<TableHead>Risk Level</TableHead>
<TableHead>Risk Score</TableHead>
<TableHead>Predicted Churn</TableHead>
<TableHead>Key Reasons</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredPredictions.map((prediction) => (
<TableRow key={prediction.profileId}>
<TableCell className="font-medium">{prediction.profileId}</TableCell>
<TableCell>
<Badge variant={getRiskBadgeVariant(prediction.riskLevel) as any}>
{prediction.riskLevel}
</Badge>
</TableCell>
<TableCell>
<span className={`font-semibold ${getRiskColor(prediction.riskLevel)}`}>
{prediction.riskScore}%
</span>
</TableCell>
<TableCell>{prediction.predictedChurnDate || "N/A"}</TableCell>
<TableCell>
<div className="max-w-xs">
<p className="text-sm text-muted-foreground truncate">
{prediction.reasons.join(", ")}
</p>
</div>
</TableCell>
<TableCell>
<Button size="sm" variant="outline">
Take Action
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Customer Retention Recommendations */}
<div className="grid gap-4 md:grid-cols-2">
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Critical Risk:</strong> 1,500 customers at immediate risk of churn. Recommend immediate outreach with personalized retention offers.
</AlertDescription>
</Alert>
<Alert>
<Target className="h-4 w-4" />
<AlertDescription>
<strong>Proactive Strategy:</strong> Focus on high-risk segment with targeted campaigns to reduce churn by 25% this quarter.
</AlertDescription>
</Alert>
</div>
</div>
);
}
+305
View File
@@ -0,0 +1,305 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { AlertTriangle, Shield, Eye, CheckCircle, XCircle, Clock } from "lucide-react";
import { apiClient, FraudAlert, FraudMetrics } from "@/lib/api-client";
export default function FraudPage() {
const [alerts, setAlerts] = useState<FraudAlert[]>([]);
const [metrics, setMetrics] = useState<FraudMetrics | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedSeverity, setSelectedSeverity] = useState<string>("all");
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
// Fetch fraud metrics and alerts in parallel
const [metricsResponse, alertsResponse] = await Promise.all([
apiClient.getFraudMetrics(),
apiClient.getFraudAlerts(selectedSeverity === "all" ? undefined : selectedSeverity),
]);
if (metricsResponse.error || alertsResponse.error) {
setError('Failed to fetch some fraud data');
}
setMetrics(metricsResponse.data || null);
setAlerts(alertsResponse.data || []);
} catch (err) {
console.error('Failed to fetch fraud data:', err);
setError('Failed to load fraud detection data');
} finally {
setLoading(false);
}
};
fetchData();
}, [selectedSeverity]);
const filteredAlerts = alerts.filter(a =>
selectedSeverity === "all" || a.severity === selectedSeverity
);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Error Loading Fraud Detection</h3>
<p className="text-gray-600">{error}</p>
<Button onClick={() => window.location.reload()} className="mt-4">
Retry
</Button>
</div>
</div>
);
}
const getSeverityBadgeVariant = (severity: string) => {
switch (severity) {
case "critical": return "destructive";
case "high": return "destructive";
case "medium": return "secondary";
case "low": return "outline";
default: return "secondary";
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "new": return <Clock className="h-4 w-4 text-gray-500" />;
case "investigating": return <Eye className="h-4 w-4 text-blue-500" />;
case "resolved": return <CheckCircle className="h-4 w-4 text-green-500" />;
case "blocked": return <XCircle className="h-4 w-4 text-red-500" />;
default: return <Clock className="h-4 w-4 text-gray-500" />;
}
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case "new": return "outline";
case "investigating": return "secondary";
case "resolved": return "default";
case "blocked": return "destructive";
default: return "outline";
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold flex items-center">
<Shield className="mr-3 h-8 w-8 text-red-600" />
Fraud Detection
</h1>
<Button>
<AlertTriangle className="mr-2 h-4 w-4" />
Run Security Scan
</Button>
</div>
{/* Metrics Overview */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Alerts</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.totalAlerts.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">Last 30 days</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Resolution Rate</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{metrics?.resolutionRate}%</div>
<p className="text-xs text-muted-foreground">Alerts resolved</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">False Positives</CardTitle>
<XCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{metrics?.falsePositiveRate}%</div>
<p className="text-xs text-muted-foreground">Accuracy improving</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Critical Alerts</CardTitle>
<AlertTriangle className="h-4 w-4 text-red-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">{metrics?.bySeverity.critical}</div>
<p className="text-xs text-muted-foreground">Immediate action needed</p>
</CardContent>
</Card>
</div>
{/* Alert Types Distribution */}
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Alerts by Type</CardTitle>
<CardDescription>Distribution of fraud detection alerts</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(metrics?.byType || {}).map(([type, count]) => (
<div key={type} className="flex justify-between items-center">
<span className="capitalize">{type.replace("_", " ")}</span>
<Badge variant="outline">{count?.toLocaleString()}</Badge>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Alerts by Severity</CardTitle>
<CardDescription>Risk level distribution</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(metrics?.bySeverity || {}).map(([severity, count]) => (
<div key={severity} className="flex justify-between items-center">
<span className="capitalize">{severity}</span>
<Badge variant={getSeverityBadgeVariant(severity) as any}>{severity}</Badge>
</div>
))}
</CardContent>
</Card>
</div>
{/* Filter Controls */}
<div className="flex items-center space-x-4">
<span className="text-sm font-medium">Filter by severity:</span>
<div className="flex space-x-2">
{["all", "critical", "high", "medium", "low"].map((severity) => (
<Button
key={severity}
variant={selectedSeverity === severity ? "default" : "outline"}
size="sm"
onClick={() => setSelectedSeverity(severity)}
>
{severity.charAt(0).toUpperCase() + severity.slice(1)}
</Button>
))}
</div>
</div>
{/* Recent Fraud Alerts */}
<Card>
<CardHeader>
<CardTitle>Recent Fraud Alerts</CardTitle>
<CardDescription>Latest security alerts requiring attention</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Alert ID</TableHead>
<TableHead>Type</TableHead>
<TableHead>Severity</TableHead>
<TableHead>Profile</TableHead>
<TableHead>Risk Score</TableHead>
<TableHead>Description</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAlerts.map((alert) => (
<TableRow key={alert.id}>
<TableCell className="font-medium">{alert.id}</TableCell>
<TableCell>
<Badge variant="outline">
{alert.type.replace("_", " ")}
</Badge>
</TableCell>
<TableCell>
<Badge variant={getSeverityBadgeVariant(alert.severity) as any}>
{alert.severity}
</Badge>
</TableCell>
<TableCell>{alert.profileId}</TableCell>
<TableCell>
<span className={`font-semibold ${
alert.riskScore >= 90 ? 'text-red-600' :
alert.riskScore >= 75 ? 'text-orange-600' :
alert.riskScore >= 50 ? 'text-yellow-600' : 'text-green-600'
}`}>
{alert.riskScore}%
</span>
</TableCell>
<TableCell>
<div className="max-w-xs">
<p className="text-sm truncate">{alert.description}</p>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{getStatusIcon(alert.status)}
<Badge variant={getStatusBadgeVariant(alert.status) as any}>
{alert.status}
</Badge>
</div>
</TableCell>
<TableCell>
<Button size="sm" variant="outline">
Review
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Security Recommendations */}
<div className="grid gap-4 md:grid-cols-2">
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Critical Alert:</strong> Account takeover attempt detected. Immediate action required to secure the account and prevent unauthorized access.
</AlertDescription>
</Alert>
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
<strong>Security Tip:</strong> Consider implementing multi-factor authentication for high-risk accounts to prevent account takeover attempts.
</AlertDescription>
</Alert>
</div>
</div>
);
}
@@ -0,0 +1,365 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertTriangle, Wrench, Clock, CheckCircle, XCircle, Server, Database, Wifi } from "lucide-react";
import { apiClient, MaintenanceMetrics } from "@/lib/api-client";
interface Asset {
id: string;
name: string;
type: string;
health: number;
status: string;
lastMaintenance: string;
nextMaintenance?: string;
predictedFailure?: string;
riskFactors: string[];
}
export default function MaintenancePage() {
const [assets, setAssets] = useState<Asset[]>([]);
const [metrics, setMetrics] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate API calls
setTimeout(() => {
setAssets([
{
id: "server-1",
name: "Web Server 1",
type: "server",
health: 85,
status: "healthy",
lastMaintenance: "2026-04-15T10:30:00Z",
nextMaintenance: "2026-06-15T10:30:00Z",
riskFactors: [],
},
{
id: "server-2",
name: "Web Server 2",
type: "server",
health: 92,
status: "healthy",
lastMaintenance: "2026-04-20T14:15:00Z",
nextMaintenance: "2026-06-20T14:15:00Z",
riskFactors: [],
},
{
id: "db-1",
name: "Primary Database",
type: "database",
health: 78,
status: "warning",
lastMaintenance: "2026-03-10T09:00:00Z",
nextMaintenance: "2026-05-10T09:00:00Z",
riskFactors: ["High CPU usage", "Memory pressure"],
},
{
id: "router-1",
name: "Core Router",
type: "network",
health: 65,
status: "warning",
lastMaintenance: "2026-02-28T16:45:00Z",
nextMaintenance: "2026-04-28T16:45:00Z",
predictedFailure: "2026-09-01T00:00:00Z",
riskFactors: ["Age", "Error rate increase", "Temperature spikes"],
},
]);
setMetrics({
totalAssets: 1250,
healthyAssets: 1180,
assetsNeedingAttention: 70,
uptime: 99.95,
meanTimeToFailure: 8760,
meanTimeToRepair: 4,
byType: {
server: 450,
database: 125,
network: 300,
storage: 375,
},
byStatus: {
healthy: 1180,
warning: 60,
critical: 10,
},
});
setLoading(false);
}, 1000);
}, []);
const getHealthColor = (health: number) => {
if (health >= 90) return "text-green-600";
if (health >= 75) return "text-yellow-600";
if (health >= 60) return "text-orange-600";
return "text-red-600";
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case "healthy": return "default";
case "warning": return "secondary";
case "critical": return "destructive";
default: return "outline";
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "healthy": return <CheckCircle className="h-4 w-4 text-green-500" />;
case "warning": return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
case "critical": return <AlertTriangle className="h-4 w-4 text-red-500" />;
default: return <Clock className="h-4 w-4 text-gray-500" />;
}
};
const getAssetIcon = (type: string) => {
switch (type) {
case "server": return <Server className="h-4 w-4" />;
case "database": return <Database className="h-4 w-4" />;
case "network": return <Wifi className="h-4 w-4" />;
default: return <Wrench className="h-4 w-4" />;
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold flex items-center">
<Wrench className="mr-3 h-8 w-8 text-blue-600" />
Predictive Maintenance
</h1>
<Button>
<Wrench className="mr-2 h-4 w-4" />
Schedule Maintenance
</Button>
</div>
{/* Maintenance Metrics */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Assets</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.totalAssets.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">Infrastructure assets</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Healthy Assets</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{metrics?.healthyAssets.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">Operating normally</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">System Uptime</CardTitle>
<Wifi className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{metrics?.uptime}%</div>
<p className="text-xs text-muted-foreground">Excellent availability</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">MTTR</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.meanTimeToRepair}h</div>
<p className="text-xs text-muted-foreground">Mean time to repair</p>
</CardContent>
</Card>
</div>
{/* Asset Distribution */}
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Assets by Type</CardTitle>
<CardDescription>Infrastructure asset distribution</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(metrics?.byType || {}).map(([type, count]) => (
<div key={type} className="flex justify-between items-center">
<div className="flex items-center space-x-2">
{getAssetIcon(type)}
<span className="capitalize">{type}</span>
</div>
<Badge variant="outline">{count?.toLocaleString()}</Badge>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Assets by Status</CardTitle>
<CardDescription>Current health status distribution</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{Object.entries(metrics?.byStatus || {}).map(([status, count]) => (
<div key={status} className="flex justify-between items-center">
<div className="flex items-center space-x-2">
{getStatusIcon(status)}
<span className="capitalize">{status}</span>
</div>
<Badge variant={getStatusBadgeVariant(status) as any}>{count?.toLocaleString()}</Badge>
</div>
))}
</CardContent>
</Card>
</div>
{/* Assets Health Table */}
<Card>
<CardHeader>
<CardTitle>Asset Health Status</CardTitle>
<CardDescription>Real-time health monitoring of critical infrastructure assets</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Asset</TableHead>
<TableHead>Type</TableHead>
<TableHead>Health</TableHead>
<TableHead>Status</TableHead>
<TableHead>Last Maintenance</TableHead>
<TableHead>Next Maintenance</TableHead>
<TableHead>Predicted Failure</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{assets.map((asset) => (
<TableRow key={asset.id}>
<TableCell className="font-medium">{asset.name}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{getAssetIcon(asset.type)}
<span className="capitalize">{asset.type}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<span className={`font-semibold ${getHealthColor(asset.health)}`}>
{asset.health}%
</span>
<Progress value={asset.health} className="h-2 w-16" />
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{getStatusIcon(asset.status)}
<Badge variant={getStatusBadgeVariant(asset.status) as any}>
{asset.status}
</Badge>
</div>
</TableCell>
<TableCell>
{new Date(asset.lastMaintenance).toLocaleDateString()}
</TableCell>
<TableCell>
{asset.nextMaintenance ? new Date(asset.nextMaintenance).toLocaleDateString() : "N/A"}
</TableCell>
<TableCell>
{asset.predictedFailure ? (
<Badge variant="destructive">
{new Date(asset.predictedFailure).toLocaleDateString()}
</Badge>
) : (
<span className="text-muted-foreground">N/A</span>
)}
</TableCell>
<TableCell>
<Button size="sm" variant="outline">
Maintain
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Risk Factors and Predictions */}
<div className="grid gap-4 md:grid-cols-2">
{assets.filter(a => a.riskFactors.length > 0).map((asset) => (
<Card key={asset.id}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
{asset.name}
<Badge variant={getStatusBadgeVariant(asset.status) as any}>{asset.status}</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-semibold mb-2">Risk Factors</h4>
<ul className="text-sm text-muted-foreground space-y-1">
{asset.riskFactors.map((factor, idx) => (
<li key={idx} className="flex items-start">
<span className="mr-2">•</span>
{factor}
</li>
))}
</ul>
</div>
{asset.predictedFailure && (
<div>
<h4 className="font-semibold mb-2">Failure Prediction</h4>
<p className="text-sm text-muted-foreground">
Predicted failure on {new Date(asset.predictedFailure).toLocaleDateString()} with 82.5% confidence
</p>
</div>
)}
<Button className="w-full" variant="outline">
Schedule Maintenance
</Button>
</CardContent>
</Card>
))}
</div>
{/* Maintenance Recommendations */}
<div className="grid gap-4 md:grid-cols-2">
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>Critical Maintenance Required:</strong> Core Router shows 65% health with predicted failure in September. Schedule immediate maintenance to prevent service disruption.
</AlertDescription>
</Alert>
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
<strong>Preventive Maintenance:</strong> Primary Database showing warning signs. Schedule maintenance within 30 days to maintain optimal performance.
</AlertDescription>
</Alert>
</div>
</div>
);
}
+221
View File
@@ -0,0 +1,221 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { TrendingUp, TrendingDown, DollarSign, Target, Zap, BarChart3, AlertTriangle } from "lucide-react";
import { apiClient, PricingMetrics, PricingOptimizationResult } from "@/lib/api-client";
export default function PricingPage() {
const [metrics, setMetrics] = useState<PricingMetrics | null>(null);
const [optimizations, setOptimizations] = useState<PricingOptimizationResult[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedStrategy, setSelectedStrategy] = useState<string>("revenue_maximization");
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const metricsResponse = await apiClient.getPricingMetrics();
if (metricsResponse.error) {
setError('Failed to fetch pricing metrics');
}
setMetrics(metricsResponse.data || null);
} catch (err) {
console.error('Failed to fetch pricing data:', err);
setError('Failed to load pricing optimization data');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const handleOptimizePricing = async () => {
try {
setLoading(true);
const response = await apiClient.optimizePricing(['plan-1', 'plan-2', 'plan-3'], selectedStrategy);
if (response.error) {
setError('Failed to optimize pricing');
} else {
setOptimizations(response.data || []);
}
} catch (err) {
console.error('Failed to optimize pricing:', err);
setError('Failed to optimize pricing');
} finally {
setLoading(false);
}
};
const getStrategyBadgeVariant = (strategy: string) => {
switch (strategy) {
case "revenue_maximization": return "default";
case "market_share": return "secondary";
case "profit_margin": return "outline";
case "competitive": return "destructive";
default: return "secondary";
}
};
const getChangeIcon = (change: number) => {
return change >= 0 ? <TrendingUp className="h-4 w-4" /> : <TrendingDown className="h-4 w-4" />;
};
const getChangeColor = (change: number) => {
return change >= 0 ? "text-green-600" : "text-red-600";
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 85) return "text-green-600";
if (confidence >= 70) return "text-yellow-600";
return "text-red-600";
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertTriangle className="mx-auto h-12 w-12 text-red-500 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Error Loading Pricing Optimization</h3>
<p className="text-gray-600">{error}</p>
<Button onClick={() => window.location.reload()} className="mt-4">
Retry
</Button>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Pricing Optimization</h1>
<p className="text-muted-foreground">Optimize pricing strategies for maximum revenue and market share</p>
</div>
<div className="flex items-center space-x-4">
<select
value={selectedStrategy}
onChange={(e) => setSelectedStrategy(e.target.value)}
className="px-3 py-2 border rounded-md"
>
<option value="revenue_maximization">Revenue Maximization</option>
<option value="market_share">Market Share</option>
<option value="profit_margin">Profit Margin</option>
<option value="competitive">Competitive</option>
</select>
<Button onClick={handleOptimizePricing} disabled={loading}>
{loading ? "Optimizing..." : "Optimize Pricing"}
</Button>
</div>
</div>
{/* Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${metrics?.totalRevenue ? (metrics.totalRevenue / 1000000).toFixed(1) : '0'}M</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">ARPU</CardTitle>
<Target className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${metrics?.arpu || '0'}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Price Elasticity</CardTitle>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metrics?.priceElasticity || '0'}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Optimization ROI</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">+{metrics?.optimizationRoi || '0'}%</div>
</CardContent>
</Card>
</div>
{/* Optimization Results */}
{optimizations.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Optimization Results</CardTitle>
<CardDescription>Recommended pricing adjustments based on {selectedStrategy} strategy</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Rate Plan</TableHead>
<TableHead>Current Price</TableHead>
<TableHead>Optimal Price</TableHead>
<TableHead>Change</TableHead>
<TableHead>Expected Revenue</TableHead>
<TableHead>Confidence</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{optimizations.map((opt, index) => (
<TableRow key={index}>
<TableCell className="font-medium">{opt.ratePlanId}</TableCell>
<TableCell>${opt.currentPrice.toFixed(2)}</TableCell>
<TableCell>${opt.optimalPrice.toFixed(2)}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
{getChangeIcon(opt.priceChangePct)}
<span className={getChangeColor(opt.priceChangePct)}>
{opt.priceChangePct > 0 ? '+' : ''}{opt.priceChangePct.toFixed(1)}%
</span>
</div>
</TableCell>
<TableCell>${(opt.expectedRevenue / 1000000).toFixed(2)}M</TableCell>
<TableCell>
<Badge variant={opt.confidence >= 80 ? "default" : "secondary"}>
{opt.confidence.toFixed(0)}%
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
</div>
);
}
@@ -11,6 +11,11 @@ import {
Settings,
Radio,
HeartPulse,
TrendingDown,
AlertTriangle,
BarChart3,
DollarSign,
Wrench,
} from "lucide-react";
import { cn } from "@/lib/utils";
@@ -20,6 +25,11 @@ const navItems = [
{ href: "/usage", label: "Usage & Billing", icon: Activity },
{ href: "/payments", label: "Payments", icon: CreditCard },
{ href: "/esim", label: "eSIM Profiles", icon: Radio },
{ href: "/analytics", label: "Analytics", icon: BarChart3 },
{ href: "/churn", label: "Churn Analysis", icon: TrendingDown },
{ href: "/fraud", label: "Fraud Detection", icon: AlertTriangle },
{ href: "/pricing", label: "Pricing", icon: DollarSign },
{ href: "/maintenance", label: "Maintenance", icon: Wrench },
{ href: "/health", label: "System Health", icon: HeartPulse },
{ href: "/chaos", label: "Chaos Engineering", icon: Shield },
{ href: "/settings", label: "Settings", icon: Settings },
@@ -0,0 +1,34 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface ProgressProps {
value?: number
max?: number
className?: string
}
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
({ value = 0, max = 100, className }, ref) => {
const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
return (
<div
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
>
<div
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - percentage}%)` }}
/>
</div>
)
}
)
Progress.displayName = "Progress"
export { Progress }
@@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
@@ -0,0 +1,106 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface TabsProps {
defaultValue?: string
value?: string
onValueChange?: (value: string) => void
children: React.ReactNode
className?: string
}
interface TabsListProps {
children: React.ReactNode
className?: string
}
interface TabsTriggerProps {
value: string
children: React.ReactNode
className?: string
disabled?: boolean
}
interface TabsContentProps {
value: string
children: React.ReactNode
className?: string
}
const TabsContext = React.createContext<{
value?: string
onValueChange?: (value: string) => void
}>({})
const Tabs: React.FC<TabsProps> = ({ defaultValue, value, onValueChange, children, className }) => {
const [internalValue, setInternalValue] = React.useState(defaultValue)
const currentValue = value ?? internalValue
const handleValueChange = onValueChange ?? setInternalValue
return (
<TabsContext.Provider value={{ value: currentValue, onValueChange: handleValueChange }}>
<div className={cn("w-full", className)}>
{children}
</div>
</TabsContext.Provider>
)
}
const TabsList: React.FC<TabsListProps> = ({ children, className }) => {
return (
<div className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}>
{children}
</div>
)
}
const TabsTrigger: React.FC<TabsTriggerProps> = ({ value, children, className, disabled }) => {
const { value: currentValue, onValueChange } = React.useContext(TabsContext)
const isActive = currentValue === value
return (
<button
type="button"
role="tab"
aria-selected={isActive}
disabled={disabled}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive && "bg-background text-foreground shadow-sm",
className
)}
onClick={() => onValueChange?.(value)}
>
{children}
</button>
)
}
const TabsContent: React.FC<TabsContentProps> = ({ value, children, className }) => {
const { value: currentValue } = React.useContext(TabsContext)
const isActive = currentValue === value
if (!isActive) {
return null
}
return (
<div
role="tabpanel"
aria-selected={isActive}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
>
{children}
</div>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }
+261
View File
@@ -0,0 +1,261 @@
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
export interface ApiResponse<T> {
data?: T;
error?: string;
message?: string;
}
export interface ChurnMetrics {
totalSubscribers: number;
churnedSubscribers: number;
churnRate: number;
monthlyChurnRate: number;
annualChurnRate: number;
averageTenureDays: number;
riskDistribution: {
low: number;
medium: number;
high: number;
critical: number;
};
}
export interface ChurnPrediction {
profileId: string;
riskLevel: string;
riskScore: number;
predictedChurnDate?: string;
reasons: string[];
recommendations: string[];
lastUpdated: string;
}
export interface FraudAlert {
id: string;
type: string;
severity: string;
profileId: string;
description: string;
riskScore: number;
evidence: string[];
ipAddress: string;
timestamp: string;
status: string;
actionsTaken: string[];
}
export interface FraudMetrics {
totalAlerts: number;
resolvedAlerts: number;
falsePositives: number;
resolutionRate: number;
falsePositiveRate: number;
byType: Record<string, number>;
bySeverity: Record<string, number>;
}
export interface MarketMetrics {
totalMarketSize: number;
ourSubscribers: number;
marketShare: number;
growthRate: number;
byCountry: Record<string, {
marketSize: number;
ourSubs: number;
penetration: number;
}>;
}
export interface PricingMetrics {
totalRevenue: number;
arpu: number;
priceElasticity: number;
competitiveIndex: number;
optimizationRoi: number;
byPlan: Record<string, number>;
byRegion: Record<string, number>;
}
export interface PricingOptimizationResult {
ratePlanId: string;
strategy: string;
currentPrice: number;
optimalPrice: number;
priceChangePct: number;
expectedRevenue: number;
expectedDemand: number;
confidence: number;
reasoning: string[];
risks: string[];
recommendations: string[];
}
export interface MaintenanceMetrics {
totalAssets: number;
healthyAssets: number;
assetsNeedingAttention: number;
uptime: number;
meanTimeToFailure: number;
meanTimeToRepair: number;
byType: Record<string, number>;
byStatus: Record<string, number>;
}
export interface Asset {
id: string;
name: string;
type: string;
health: number;
status: string;
lastMaintenance: string;
nextMaintenance?: string;
predictedFailure?: string;
riskFactors: string[];
}
class ApiClient {
private baseUrl: string;
private headers: Record<string, string>;
constructor() {
this.baseUrl = API_BASE_URL;
this.headers = {
'Content-Type': 'application/json',
};
// Add auth token if available
const token = this.getAuthToken();
if (token) {
this.headers['Authorization'] = `Bearer ${token}`;
}
}
private getAuthToken(): string | null {
if (typeof window !== 'undefined') {
return localStorage.getItem('auth_token');
}
return null;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
try {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
...this.headers,
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return { data };
} catch (error) {
console.error('API request failed:', error);
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
// Analytics API
async getChurnMetrics(period: string = 'monthly'): Promise<ApiResponse<ChurnMetrics>> {
return this.request<ChurnMetrics>(`/api/v1/analytics/churn/metrics?period=${period}`);
}
async getChurnPredictions(riskLevel?: string, limit?: number): Promise<ApiResponse<ChurnPrediction[]>> {
const params = new URLSearchParams();
if (riskLevel) params.append('risk_level', riskLevel);
if (limit) params.append('limit', limit.toString());
return this.request<ChurnPrediction[]>(`/api/v1/analytics/churn/predictions?${params}`);
}
async predictChurn(profileId: string): Promise<ApiResponse<ChurnPrediction>> {
return this.request<ChurnPrediction>('/api/v1/analytics/churn/predict', {
method: 'POST',
body: JSON.stringify({ profile_id: profileId }),
});
}
// Fraud Detection API
async getFraudAlerts(severity?: string): Promise<ApiResponse<FraudAlert[]>> {
const params = severity ? `?severity=${severity}` : '';
return this.request<FraudAlert[]>(`/api/v1/security/fraud/alerts${params}`);
}
async getFraudMetrics(period: string = 'monthly'): Promise<ApiResponse<FraudMetrics>> {
return this.request<FraudMetrics>(`/api/v1/security/fraud/metrics?period=${period}`);
}
async analyzeTransaction(transaction: Record<string, any>): Promise<ApiResponse<FraudAlert>> {
return this.request<FraudAlert>('/api/v1/security/fraud/analyze', {
method: 'POST',
body: JSON.stringify(transaction),
});
}
async updateFraudAlert(alertId: string, status: string, actions: string[]): Promise<ApiResponse<void>> {
return this.request<void>(`/api/v1/security/fraud/alerts/${alertId}`, {
method: 'PUT',
body: JSON.stringify({ status, actions }),
});
}
// Market Analytics API
async getMarketMetrics(period: string = 'monthly'): Promise<ApiResponse<MarketMetrics>> {
return this.request<MarketMetrics>(`/api/v1/analytics/market/metrics?period=${period}`);
}
async getCompetitors(): Promise<ApiResponse<Record<string, any>[]>> {
return this.request<Record<string, any>[]>('/api/v1/analytics/market/competitors');
}
async getMarketOpportunities(): Promise<ApiResponse<Record<string, any>[]>> {
return this.request<Record<string, any>[]>('/api/v1/analytics/market/opportunities');
}
// Pricing API
async getPricingMetrics(period: string = 'monthly'): Promise<ApiResponse<PricingMetrics>> {
return this.request<PricingMetrics>(`/api/v1/analytics/pricing/metrics?period=${period}`);
}
async optimizePricing(ratePlanIds: string[], strategy: string): Promise<ApiResponse<PricingOptimizationResult[]>> {
return this.request<PricingOptimizationResult[]>('/api/v1/analytics/pricing/optimize', {
method: 'POST',
body: JSON.stringify({ rate_plan_ids: ratePlanIds, strategy }),
});
}
async getPriceElasticity(): Promise<ApiResponse<Record<string, number>>> {
return this.request<Record<string, number>>('/api/v1/analytics/pricing/elasticity');
}
// Maintenance API
async getMaintenanceMetrics(period: string = 'monthly'): Promise<ApiResponse<MaintenanceMetrics>> {
return this.request<MaintenanceMetrics>(`/api/v1/analytics/maintenance/metrics?period=${period}`);
}
async getAssets(): Promise<ApiResponse<Asset[]>> {
return this.request<Asset[]>('/api/v1/analytics/maintenance/assets');
}
async getMaintenanceAlerts(): Promise<ApiResponse<Record<string, any>[]>> {
return this.request<Record<string, any>[]>('/api/v1/analytics/maintenance/alerts');
}
async predictFailure(assetId: string): Promise<ApiResponse<Record<string, any>>> {
return this.request<Record<string, any>>(`/api/v1/analytics/maintenance/predict/${assetId}`, {
method: 'POST',
});
}
}
export const apiClient = new ApiClient();
@@ -1,55 +0,0 @@
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client';
import { SetContextLink } from '@apollo/client/link/context';
// GraphQL API endpoint
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_API_SERVER_URL || 'http://localhost:8080/v1/graphql',
});
// Auth link to add authentication token to requests
const authLink = new SetContextLink((prevContext, operation) => {
// Get the authentication token from local storage if it exists
const token = localStorage.getItem('auth_token');
return {
...prevContext,
headers: {
...prevContext.headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
// Create Apollo Client instance
export const apolloClient = new ApolloClient({
link: ApolloLink.from([authLink, httpLink]),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
query: {
fetchPolicy: 'network-only',
},
mutate: {
fetchPolicy: 'network-only',
},
},
});
// GraphQL queries and mutations can be defined here
// Example:
/*
import { gql } from '@apollo/client';
export const GET_SERVICES = gql`
query GetServices {
services {
id
name
status
health
}
}
`;
*/