mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-22 12:44:29 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
*/
|
||||
Reference in New Issue
Block a user