AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
590
frontend/src/components/billing/AdvancedCostAnalytics.tsx
Normal file
590
frontend/src/components/billing/AdvancedCostAnalytics.tsx
Normal file
@@ -0,0 +1,590 @@
|
||||
import React, { useState, useEffect, useMemo, Suspense } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
Grid,
|
||||
Chip,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
Tabs,
|
||||
Tab,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Clock,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
BarChart3,
|
||||
Zap,
|
||||
Calendar
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
LazyBarChart,
|
||||
LazyPieChart,
|
||||
Bar,
|
||||
Pie,
|
||||
Cell,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip as RechartsTooltip,
|
||||
ResponsiveContainer,
|
||||
Legend,
|
||||
ChartLoadingFallback
|
||||
} from '../../utils/lazyRecharts';
|
||||
|
||||
// Types
|
||||
import { UsageLog } from '../../types/billing';
|
||||
|
||||
// Services
|
||||
import { billingService, formatCurrency, formatNumber } from '../../services/billingService';
|
||||
|
||||
// Components
|
||||
import DailyCostHeatmap from './DailyCostHeatmap';
|
||||
|
||||
interface AdvancedCostAnalyticsProps {
|
||||
userId?: string;
|
||||
terminalTheme?: boolean;
|
||||
}
|
||||
|
||||
interface TimeOfDayData {
|
||||
hour: number;
|
||||
label: string;
|
||||
cost: number;
|
||||
calls: number;
|
||||
avgCostPerCall: number;
|
||||
}
|
||||
|
||||
interface UserActionData {
|
||||
endpoint: string;
|
||||
action: string;
|
||||
cost: number;
|
||||
calls: number;
|
||||
avgCostPerCall: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
|
||||
interface EfficiencyMetric {
|
||||
label: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
description: string;
|
||||
}
|
||||
|
||||
const AdvancedCostAnalytics: React.FC<AdvancedCostAnalyticsProps> = ({
|
||||
userId,
|
||||
terminalTheme = false
|
||||
}) => {
|
||||
const [usageLogs, setUsageLogs] = useState<UsageLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUsageLogs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const currentDate = new Date();
|
||||
const billingPeriod = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
console.log('[AdvancedCostAnalytics] Fetching usage logs for period:', billingPeriod);
|
||||
|
||||
// Try with billing period first, then without if no results
|
||||
let response = await billingService.getUsageLogs(2000, 0, undefined, undefined, billingPeriod);
|
||||
let logs = response.logs || [];
|
||||
|
||||
// If no logs for current period, try without billing period filter to get recent logs
|
||||
if (logs.length === 0) {
|
||||
console.log('[AdvancedCostAnalytics] No logs for current period, fetching all recent logs...');
|
||||
response = await billingService.getUsageLogs(2000, 0);
|
||||
logs = response.logs || [];
|
||||
}
|
||||
|
||||
console.log('[AdvancedCostAnalytics] Received logs:', {
|
||||
total: logs.length,
|
||||
sample: logs.slice(0, 3),
|
||||
totalCost: logs.reduce((sum, log) => sum + (log.cost_total || 0), 0),
|
||||
totalCalls: logs.length,
|
||||
logsWithCost: logs.filter(log => (log.cost_total || 0) > 0).length,
|
||||
logsWithTokens: logs.filter(log => (log.tokens_total || 0) > 0).length,
|
||||
sampleLogStructure: logs[0] ? {
|
||||
id: logs[0].id,
|
||||
cost_total: logs[0].cost_total,
|
||||
tokens_total: logs[0].tokens_total,
|
||||
status: logs[0].status,
|
||||
status_code: logs[0].status_code,
|
||||
response_time: logs[0].response_time
|
||||
} : null
|
||||
});
|
||||
|
||||
setUsageLogs(logs);
|
||||
} catch (err) {
|
||||
console.error('[AdvancedCostAnalytics] Error fetching usage logs:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch usage logs');
|
||||
// Set empty array on error to prevent showing stale data
|
||||
setUsageLogs([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUsageLogs();
|
||||
}, [userId]);
|
||||
|
||||
// Time of Day Analysis
|
||||
const timeOfDayData = useMemo(() => {
|
||||
const hourlyData: Record<number, { cost: number; calls: number }> = {};
|
||||
|
||||
usageLogs.forEach(log => {
|
||||
// Include all logs, not just those with cost > 0, for accurate call counts
|
||||
const cost = log.cost_total || 0;
|
||||
|
||||
try {
|
||||
const date = new Date(log.timestamp);
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn('[AdvancedCostAnalytics] Invalid timestamp:', log.timestamp);
|
||||
return;
|
||||
}
|
||||
const hour = date.getHours();
|
||||
|
||||
if (!hourlyData[hour]) {
|
||||
hourlyData[hour] = { cost: 0, calls: 0 };
|
||||
}
|
||||
hourlyData[hour].cost += cost;
|
||||
hourlyData[hour].calls += 1;
|
||||
} catch (err) {
|
||||
console.warn('[AdvancedCostAnalytics] Error processing log timestamp:', err, log);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from({ length: 24 }, (_, hour) => {
|
||||
const data = hourlyData[hour] || { cost: 0, calls: 0 };
|
||||
return {
|
||||
hour,
|
||||
label: `${hour.toString().padStart(2, '0')}:00`,
|
||||
cost: data.cost,
|
||||
calls: data.calls,
|
||||
avgCostPerCall: data.calls > 0 ? data.cost / data.calls : 0
|
||||
} as TimeOfDayData;
|
||||
});
|
||||
}, [usageLogs]);
|
||||
|
||||
// User Action Breakdown
|
||||
const userActionData = useMemo(() => {
|
||||
const actionMap: Record<string, { cost: number; calls: number; responseTime: number }> = {};
|
||||
|
||||
usageLogs.forEach(log => {
|
||||
// Include all logs for accurate call counts
|
||||
const cost = log.cost_total || 0;
|
||||
|
||||
// Extract action from endpoint (e.g., /api/blog-writer/generate -> blog-writer)
|
||||
const endpoint = log.endpoint || '';
|
||||
const endpointParts = endpoint.split('/');
|
||||
const action = endpointParts.length > 2 ? endpointParts[2] : 'other';
|
||||
|
||||
if (!actionMap[action]) {
|
||||
actionMap[action] = { cost: 0, calls: 0, responseTime: 0 };
|
||||
}
|
||||
actionMap[action].cost += cost;
|
||||
actionMap[action].calls += 1;
|
||||
actionMap[action].responseTime += log.response_time || 0;
|
||||
});
|
||||
|
||||
return Object.entries(actionMap)
|
||||
.map(([endpoint, data]) => ({
|
||||
endpoint,
|
||||
action: endpoint.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||
cost: data.cost,
|
||||
calls: data.calls,
|
||||
avgCostPerCall: data.calls > 0 ? data.cost / data.calls : 0,
|
||||
avgResponseTime: data.calls > 0 ? data.responseTime / data.calls : 0
|
||||
} as UserActionData))
|
||||
.sort((a, b) => b.cost - a.cost)
|
||||
.slice(0, 10); // Top 10 actions
|
||||
}, [usageLogs]);
|
||||
|
||||
// Cost Efficiency Metrics
|
||||
const efficiencyMetrics = useMemo(() => {
|
||||
const totalCost = usageLogs.reduce((sum, log) => sum + (log.cost_total || 0), 0);
|
||||
const totalCalls = usageLogs.length;
|
||||
const totalTokens = usageLogs.reduce((sum, log) => sum + (log.tokens_total || 0), 0);
|
||||
const totalResponseTime = usageLogs.reduce((sum, log) => sum + (log.response_time || 0), 0);
|
||||
const successfulCalls = usageLogs.filter(log => log.status === 'success' || (log.status_code && log.status_code < 400)).length;
|
||||
const failedCalls = usageLogs.filter(log => log.status === 'failed' || (log.status_code && log.status_code >= 400)).length;
|
||||
|
||||
// Debug logging
|
||||
if (usageLogs.length > 0) {
|
||||
console.log('[AdvancedCostAnalytics] Efficiency metrics calculation:', {
|
||||
totalLogs: usageLogs.length,
|
||||
totalCost,
|
||||
totalTokens,
|
||||
totalCalls,
|
||||
successfulCalls,
|
||||
failedCalls,
|
||||
sampleLog: usageLogs[0]
|
||||
});
|
||||
}
|
||||
|
||||
const avgCostPerCall = totalCalls > 0 ? totalCost / totalCalls : 0;
|
||||
const avgCostPerToken = totalTokens > 0 ? totalCost / totalTokens : 0;
|
||||
const avgResponseTime = totalCalls > 0 ? totalResponseTime / totalCalls : 0;
|
||||
const successRate = totalCalls > 0 ? (successfulCalls / totalCalls) * 100 : 0;
|
||||
// Tokens per dollar - handle division by zero
|
||||
const costEfficiency = totalCost > 0 && totalTokens > 0 ? totalTokens / totalCost : 0;
|
||||
|
||||
// Calculate trends (simplified - compare first half vs second half)
|
||||
const midPoint = Math.floor(usageLogs.length / 2);
|
||||
const firstHalf = usageLogs.slice(0, midPoint);
|
||||
const secondHalf = usageLogs.slice(midPoint);
|
||||
|
||||
const firstHalfAvgCost = firstHalf.length > 0
|
||||
? firstHalf.reduce((sum, log) => sum + (log.cost_total || 0), 0) / firstHalf.length
|
||||
: 0;
|
||||
const secondHalfAvgCost = secondHalf.length > 0
|
||||
? secondHalf.reduce((sum, log) => sum + (log.cost_total || 0), 0) / secondHalf.length
|
||||
: 0;
|
||||
|
||||
const costTrend = secondHalfAvgCost > firstHalfAvgCost * 1.05 ? 'up' :
|
||||
secondHalfAvgCost < firstHalfAvgCost * 0.95 ? 'down' : 'stable';
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Avg Cost per Call',
|
||||
value: avgCostPerCall,
|
||||
unit: '$',
|
||||
trend: costTrend,
|
||||
description: 'Average cost per API call'
|
||||
},
|
||||
{
|
||||
label: 'Cost per 1K Tokens',
|
||||
value: avgCostPerToken * 1000,
|
||||
unit: '$',
|
||||
trend: costTrend,
|
||||
description: 'Cost efficiency for token usage'
|
||||
},
|
||||
{
|
||||
label: 'Tokens per Dollar',
|
||||
value: costEfficiency,
|
||||
unit: '',
|
||||
trend: costTrend === 'down' ? 'up' : costTrend === 'up' ? 'down' : 'stable',
|
||||
description: 'How many tokens you get per dollar spent'
|
||||
},
|
||||
{
|
||||
label: 'Avg Response Time',
|
||||
value: avgResponseTime,
|
||||
unit: 's',
|
||||
trend: 'stable',
|
||||
description: 'Average API response time'
|
||||
},
|
||||
{
|
||||
label: 'Success Rate',
|
||||
value: successRate,
|
||||
unit: '%',
|
||||
trend: successRate > 95 ? 'up' : successRate < 90 ? 'down' : 'stable',
|
||||
description: 'Percentage of successful API calls'
|
||||
},
|
||||
{
|
||||
label: 'Failed Calls',
|
||||
value: failedCalls,
|
||||
unit: '',
|
||||
trend: failedCalls > 0 ? 'down' : 'stable',
|
||||
description: 'Number of failed API calls'
|
||||
}
|
||||
] as EfficiencyMetric[];
|
||||
}, [usageLogs]);
|
||||
|
||||
const COLORS = ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe', '#43e97b', '#fa709a', '#fee140', '#30cfd0', '#a8edea'];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card sx={{ p: 3, textAlign: 'center' }}>
|
||||
<CircularProgress />
|
||||
<Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.7)' }}>
|
||||
Loading analytics...
|
||||
</Typography>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card sx={{ p: 3 }}>
|
||||
<Alert severity="error">
|
||||
{error}
|
||||
<Typography variant="body2" sx={{ mt: 1, color: 'rgba(255,255,255,0.7)' }}>
|
||||
Unable to load usage analytics. Please try refreshing the page.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Show message if no logs available
|
||||
if (usageLogs.length === 0) {
|
||||
return (
|
||||
<Card sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Alert severity="info">
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
No Usage Data Available
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
Usage analytics will appear here once you start making API calls.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ pb: 1 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', color: '#ffffff', mb: 2 }}>
|
||||
<BarChart3 size={20} />
|
||||
Advanced Cost Analytics
|
||||
</Typography>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, newValue) => setActiveTab(newValue)}
|
||||
sx={{
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
||||
mb: 2,
|
||||
'& .MuiTab-root': {
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
'&.Mui-selected': {
|
||||
color: '#667eea',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
backgroundColor: '#667eea'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab icon={<Clock size={16} />} label="Time of Day" iconPosition="start" />
|
||||
<Tab icon={<Activity size={16} />} label="User Actions" iconPosition="start" />
|
||||
<Tab icon={<Zap size={16} />} label="Efficiency Metrics" iconPosition="start" />
|
||||
<Tab icon={<Calendar size={16} />} label="Daily Heatmap" iconPosition="start" />
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
|
||||
<CardContent sx={{ pt: 0 }}>
|
||||
{/* Time of Day Tab */}
|
||||
{activeTab === 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
|
||||
Cost Distribution by Hour of Day
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ height: 300, mb: 3 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<Suspense fallback={<ChartLoadingFallback />}>
|
||||
<LazyBarChart data={timeOfDayData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
stroke="rgba(255,255,255,0.9)"
|
||||
fontSize={10}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="rgba(255,255,255,0.9)"
|
||||
fontSize={12}
|
||||
tickFormatter={(value) => `$${value.toFixed(2)}`}
|
||||
/>
|
||||
<RechartsTooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 8
|
||||
}}
|
||||
formatter={(value: number) => [formatCurrency(value), 'Cost']}
|
||||
/>
|
||||
<Bar dataKey="cost" fill="#667eea" radius={[4, 4, 0, 0]} />
|
||||
</LazyBarChart>
|
||||
</Suspense>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
|
||||
{/* Peak Hours Summary */}
|
||||
<Grid container spacing={2}>
|
||||
{timeOfDayData
|
||||
.sort((a, b) => b.cost - a.cost)
|
||||
.slice(0, 3)
|
||||
.map((data, idx) => (
|
||||
<Grid item xs={4} key={data.hour}>
|
||||
<Box sx={{ p: 1.5, backgroundColor: 'rgba(102, 126, 234, 0.1)', borderRadius: 1, textAlign: 'center' }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
Peak Hour {idx + 1}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#667eea' }}>
|
||||
{data.label}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
{formatCurrency(data.cost)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* User Actions Tab */}
|
||||
{activeTab === 1 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
|
||||
Cost Breakdown by User Action
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ height: 300, mb: 3 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<Suspense fallback={<ChartLoadingFallback />}>
|
||||
<LazyPieChart>
|
||||
<Pie
|
||||
data={userActionData}
|
||||
dataKey="cost"
|
||||
nameKey="action"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={100}
|
||||
label={(entry: any) => {
|
||||
const data = userActionData[entry.index];
|
||||
return data ? `${data.action}: ${(entry.percent * 100).toFixed(0)}%` : '';
|
||||
}}
|
||||
>
|
||||
{userActionData.map((_, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<RechartsTooltip
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
</LazyPieChart>
|
||||
</Suspense>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
|
||||
{/* Top Actions Table */}
|
||||
<Box sx={{ maxHeight: 200, overflowY: 'auto' }}>
|
||||
{userActionData.map((action, idx) => (
|
||||
<Box
|
||||
key={action.endpoint}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
mb: 1,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
|
||||
{action.action}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
{action.calls} calls • {formatCurrency(action.avgCostPerCall)} avg
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: COLORS[idx % COLORS.length] }}>
|
||||
{formatCurrency(action.cost)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Efficiency Metrics Tab */}
|
||||
{activeTab === 2 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
|
||||
Cost Efficiency Metrics
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{efficiencyMetrics.map((metric, idx) => (
|
||||
<Grid item xs={6} sm={4} key={metric.label}>
|
||||
<Tooltip title={metric.description} arrow>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
textAlign: 'center',
|
||||
cursor: 'help'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
{metric.label}
|
||||
</Typography>
|
||||
{metric.trend === 'up' && <TrendingUp size={14} color="#22c55e" />}
|
||||
{metric.trend === 'down' && <TrendingUp size={14} color="#ef4444" style={{ transform: 'rotate(180deg)' }} />}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
color: metric.trend === 'up' ? '#22c55e' :
|
||||
metric.trend === 'down' ? '#ef4444' : '#ffffff'
|
||||
}}
|
||||
>
|
||||
{metric.unit === '$' ? formatCurrency(metric.value) :
|
||||
metric.unit === '%' ? `${metric.value.toFixed(1)}%` :
|
||||
metric.unit === 's' ? `${metric.value.toFixed(2)}s` :
|
||||
formatNumber(metric.value)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Daily Heatmap Tab */}
|
||||
{activeTab === 3 && (
|
||||
<Box>
|
||||
<DailyCostHeatmap
|
||||
usageLogs={usageLogs}
|
||||
currentMonth={new Date().getMonth()}
|
||||
currentYear={new Date().getFullYear()}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedCostAnalytics;
|
||||
Reference in New Issue
Block a user