1297 lines
48 KiB
TypeScript
1297 lines
48 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useNumberCards, useDashboardChart } from '../hooks/useApi';
|
||
import { useWorkOrders } from '../hooks/useWorkOrder';
|
||
import { useAssetMaintenanceLogs } from '../hooks/useAssetMaintenance';
|
||
import { useAssets } from '../hooks/useAsset';
|
||
import { FaShoppingCart, FaChartLine, FaBoxes, FaTools, FaCheckCircle, FaClock, FaExclamationTriangle, FaArrowUp, FaArrowDown } from 'react-icons/fa';
|
||
|
||
const ModernDashboard: React.FC = () => {
|
||
const { data: numberCards, loading: cardsLoading } = useNumberCards();
|
||
const { workOrders } = useWorkOrders({}, 1000, 0);
|
||
const { logs: maintenanceLogs } = useAssetMaintenanceLogs({}, 1000, 0);
|
||
const { assets } = useAssets({}, 1000, 0);
|
||
|
||
const [workOrderChartData, setWorkOrderChartData] = useState<any>(null);
|
||
const [workOrderGroupedChartData, setWorkOrderGroupedChartData] = useState<any>(null);
|
||
const [maintenanceAssetChartData, setMaintenanceAssetChartData] = useState<any>(null);
|
||
const [upDownTimeChartData, setUpDownTimeChartData] = useState<any>(null);
|
||
const [assigneesGroupedChartData, setAssigneesGroupedChartData] = useState<any>(null);
|
||
const [maintenanceFrequencyChartData, setMaintenanceFrequencyChartData] = useState<any>(null);
|
||
|
||
const [realWorkOrderCounts, setRealWorkOrderCounts] = useState({
|
||
open: 0,
|
||
inProgress: 0,
|
||
completed: 0,
|
||
total: 0
|
||
});
|
||
|
||
const [insights, setInsights] = useState({
|
||
assetUptime: 0,
|
||
avgResponseTime: 0,
|
||
maintenanceEfficiency: 0,
|
||
overdueTasks: 0,
|
||
plannedMaintenance: 0
|
||
});
|
||
|
||
// Fetch all charts
|
||
const { data: upDownChart } = useDashboardChart('Up & Down Time Chart');
|
||
const { data: workOrderChart } = useDashboardChart('Work Order Status Chart');
|
||
const { data: assetWiseChart } = useDashboardChart('Maintenance - Asset wise Count');
|
||
const { data: assigneesChart } = useDashboardChart('Asset Maintenance Assignees Status Count');
|
||
const { data: frequencyChart } = useDashboardChart('Asset Maintenance Frequency Chart');
|
||
const { data: ppmStatusChart } = useDashboardChart('PPM Status');
|
||
|
||
// Generate Up & Down Time Chart data from assets
|
||
useEffect(() => {
|
||
if (assets && assets.length > 0) {
|
||
let totalUpTime = 0;
|
||
let totalDownTime = 0;
|
||
|
||
assets.forEach(asset => {
|
||
// Sum up time and down time values
|
||
const upTime = asset.custom_up_time || 0;
|
||
const downTime = asset.custom_down_time || 0;
|
||
|
||
totalUpTime += typeof upTime === 'number' ? upTime : 0;
|
||
totalDownTime += typeof downTime === 'number' ? downTime : 0;
|
||
});
|
||
|
||
// Create pie chart data
|
||
const labels: string[] = [];
|
||
const values: number[] = [];
|
||
|
||
if (totalUpTime > 0) {
|
||
labels.push('Up Time');
|
||
values.push(totalUpTime);
|
||
}
|
||
|
||
if (totalDownTime > 0) {
|
||
labels.push('Down Time');
|
||
values.push(totalDownTime);
|
||
}
|
||
|
||
// If we have data, create the chart
|
||
if (labels.length > 0 && values.length > 0) {
|
||
// Use blue and purple color palette
|
||
const pieColors: string[] = [];
|
||
if (totalUpTime > 0) pieColors.push('#6366F1'); // Indigo for Up Time
|
||
if (totalDownTime > 0) pieColors.push('#8B5CF6'); // Purple for Down Time
|
||
|
||
setUpDownTimeChartData({
|
||
labels: labels,
|
||
datasets: [{
|
||
name: 'Time',
|
||
values: values,
|
||
colors: pieColors
|
||
}],
|
||
type: 'Pie'
|
||
});
|
||
} else {
|
||
setUpDownTimeChartData(null);
|
||
}
|
||
} else {
|
||
setUpDownTimeChartData(null);
|
||
}
|
||
}, [assets]);
|
||
|
||
// Generate Work Order Status Chart data and counts
|
||
useEffect(() => {
|
||
if (workOrders && workOrders.length > 0) {
|
||
const statusCounts: Record<string, number> = {};
|
||
let openCount = 0;
|
||
let inProgressCount = 0;
|
||
let completedCount = 0;
|
||
|
||
workOrders.forEach(wo => {
|
||
const status = wo.repair_status || 'Unknown';
|
||
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
||
|
||
if (status.toLowerCase() === 'open') openCount++;
|
||
if (status.toLowerCase() === 'in progress') inProgressCount++;
|
||
if (status.toLowerCase() === 'completed') completedCount++;
|
||
});
|
||
|
||
const labels = Object.keys(statusCounts);
|
||
const values = Object.values(statusCounts);
|
||
|
||
const colorMap: Record<string, string> = {
|
||
'Open': '#F59E0B',
|
||
'In Progress': '#3B82F6',
|
||
'Pending': '#8B5CF6',
|
||
'Completed': '#10B981',
|
||
'Cancelled': '#EC4899',
|
||
'Unknown': '#6B7280'
|
||
};
|
||
|
||
const colors = labels.map(label => colorMap[label] || '#6366F1');
|
||
|
||
setWorkOrderChartData({
|
||
labels,
|
||
datasets: [{
|
||
name: 'Work Orders',
|
||
values,
|
||
colors
|
||
}],
|
||
type: 'bar'
|
||
});
|
||
|
||
setRealWorkOrderCounts({
|
||
open: openCount,
|
||
inProgress: inProgressCount,
|
||
completed: completedCount,
|
||
total: workOrders.length
|
||
});
|
||
|
||
// Generate grouped chart data by work_order_type and repair_status
|
||
const groupedData: Record<string, Record<string, number>> = {};
|
||
const allStatuses = new Set<string>();
|
||
|
||
// Normalize status names to handle variations
|
||
const normalizeStatus = (status: string): string => {
|
||
const statusLower = status.toLowerCase().trim();
|
||
if (statusLower.includes('open')) return 'Open';
|
||
if (statusLower.includes('work in progress') || statusLower.includes('in progress') || statusLower.includes('wip')) return 'Work In Progress';
|
||
if (statusLower.includes('pending review') || statusLower.includes('pending')) return 'Pending Review';
|
||
if (statusLower.includes('completed') || statusLower.includes('complete')) return 'Completed';
|
||
if (statusLower.includes('closed')) return 'Closed';
|
||
return status; // Return original if no match
|
||
};
|
||
|
||
workOrders.forEach(wo => {
|
||
const workOrderType = wo.work_order_type || 'null';
|
||
const repairStatus = normalizeStatus(wo.repair_status || 'Unknown');
|
||
|
||
allStatuses.add(repairStatus);
|
||
|
||
if (!groupedData[workOrderType]) {
|
||
groupedData[workOrderType] = {};
|
||
}
|
||
|
||
if (!groupedData[workOrderType][repairStatus]) {
|
||
groupedData[workOrderType][repairStatus] = 0;
|
||
}
|
||
|
||
groupedData[workOrderType][repairStatus]++;
|
||
});
|
||
|
||
// Create labels (work_order_type values)
|
||
const workOrderTypeLabels = Object.keys(groupedData);
|
||
|
||
// Create datasets for each status (order them consistently)
|
||
const statusOrder = ['Open', 'Work In Progress', 'Pending Review', 'Completed', 'Closed'];
|
||
const statusList = Array.from(allStatuses).sort((a, b) => {
|
||
const aIndex = statusOrder.indexOf(a);
|
||
const bIndex = statusOrder.indexOf(b);
|
||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||
if (aIndex !== -1) return -1;
|
||
if (bIndex !== -1) return 1;
|
||
return a.localeCompare(b);
|
||
});
|
||
|
||
const datasets = statusList.map(status => {
|
||
const values = workOrderTypeLabels.map(type => groupedData[type][status] || 0);
|
||
return {
|
||
name: status,
|
||
values: values
|
||
};
|
||
});
|
||
|
||
setWorkOrderGroupedChartData({
|
||
labels: workOrderTypeLabels,
|
||
datasets: datasets,
|
||
type: 'Bar'
|
||
});
|
||
} else {
|
||
// Reset if no work orders
|
||
setWorkOrderGroupedChartData(null);
|
||
}
|
||
}, [workOrders]);
|
||
|
||
// Generate maintenance charts and insights
|
||
useEffect(() => {
|
||
if (maintenanceLogs && maintenanceLogs.length > 0) {
|
||
const assetCounts: Record<string, number> = {};
|
||
let completedMaintenance = 0;
|
||
let plannedMaintenance = 0;
|
||
let overdueMaintenance = 0;
|
||
const today = new Date();
|
||
|
||
maintenanceLogs.forEach(log => {
|
||
const assetName = log.asset_name || 'Unknown';
|
||
assetCounts[assetName] = (assetCounts[assetName] || 0) + 1;
|
||
|
||
// Count statuses
|
||
if (log.maintenance_status?.toLowerCase() === 'completed') completedMaintenance++;
|
||
if (log.maintenance_status?.toLowerCase() === 'planned') plannedMaintenance++;
|
||
|
||
// Check if overdue
|
||
if (log.due_date && new Date(log.due_date) < today && log.maintenance_status !== 'Completed') {
|
||
overdueMaintenance++;
|
||
}
|
||
});
|
||
|
||
const sorted = Object.entries(assetCounts).sort(([, a], [, b]) => b - a).slice(0, 10);
|
||
const labels = sorted.map(([name]) => name);
|
||
const values = sorted.map(([, count]) => count);
|
||
const barColors = generateColors(labels.length);
|
||
|
||
setMaintenanceAssetChartData({
|
||
labels,
|
||
datasets: [{ name: 'Maintenance Count', values, colors: barColors }],
|
||
type: 'bar'
|
||
});
|
||
|
||
// Calculate maintenance efficiency
|
||
const maintenanceEfficiency = maintenanceLogs.length > 0
|
||
? ((completedMaintenance / maintenanceLogs.length) * 100)
|
||
: 0;
|
||
|
||
setInsights(prev => ({
|
||
...prev,
|
||
maintenanceEfficiency,
|
||
overdueTasks: overdueMaintenance,
|
||
plannedMaintenance
|
||
}));
|
||
|
||
// Generate Asset Maintenance Assignees Status Count chart data
|
||
const groupedData: Record<string, Record<string, number>> = {};
|
||
const allStatuses = new Set<string>();
|
||
|
||
// Normalize status names to match expected chart statuses
|
||
const normalizeStatus = (status: string, log: any): string => {
|
||
const statusLower = status.toLowerCase().trim();
|
||
const today = new Date();
|
||
const dueDate = log.due_date ? new Date(log.due_date) : null;
|
||
|
||
// Check for overdue first
|
||
if (dueDate && dueDate < today && statusLower !== 'completed' && statusLower !== 'cancelled') {
|
||
return 'Overdue';
|
||
}
|
||
|
||
// Map status variations
|
||
if (statusLower.includes('completed on time') || statusLower === 'completed on time') {
|
||
return 'Completed On Time';
|
||
}
|
||
if (statusLower.includes('completed within') || statusLower.includes('within sla') || statusLower === 'completed') {
|
||
return 'Completed Within SLA';
|
||
}
|
||
if (statusLower.includes('delay') || statusLower.includes('late')) {
|
||
return 'Delay In Completion';
|
||
}
|
||
if (statusLower.includes('pending') || statusLower === 'planned') {
|
||
return 'Pending';
|
||
}
|
||
if (statusLower.includes('overdue')) {
|
||
return 'Overdue';
|
||
}
|
||
if (statusLower.includes('cancelled') || statusLower === 'cancelled') {
|
||
return 'Cancelled';
|
||
}
|
||
|
||
return status; // Return original if no match
|
||
};
|
||
|
||
maintenanceLogs.forEach(log => {
|
||
const assignee = log.assign_to_name || 'null';
|
||
const maintenanceStatus = log.maintenance_status || 'Unknown';
|
||
const normalizedStatus = normalizeStatus(maintenanceStatus, log);
|
||
|
||
allStatuses.add(normalizedStatus);
|
||
|
||
if (!groupedData[assignee]) {
|
||
groupedData[assignee] = {};
|
||
}
|
||
|
||
if (!groupedData[assignee][normalizedStatus]) {
|
||
groupedData[assignee][normalizedStatus] = 0;
|
||
}
|
||
|
||
groupedData[assignee][normalizedStatus]++;
|
||
});
|
||
|
||
// Create labels (assignee names)
|
||
const assigneeLabels = Object.keys(groupedData);
|
||
|
||
// Create datasets for each status (order them consistently)
|
||
const statusOrder = ['Completed On Time', 'Completed Within SLA', 'Delay In Completion', 'Pending', 'Overdue', 'Cancelled'];
|
||
const statusList = Array.from(allStatuses).sort((a, b) => {
|
||
const aIndex = statusOrder.indexOf(a);
|
||
const bIndex = statusOrder.indexOf(b);
|
||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||
if (aIndex !== -1) return -1;
|
||
if (bIndex !== -1) return 1;
|
||
return a.localeCompare(b);
|
||
});
|
||
|
||
const datasets = statusList.map(status => {
|
||
const values = assigneeLabels.map(assignee => groupedData[assignee][status] || 0);
|
||
return {
|
||
name: status,
|
||
values: values
|
||
};
|
||
});
|
||
|
||
setAssigneesGroupedChartData({
|
||
labels: assigneeLabels,
|
||
datasets: datasets,
|
||
type: 'Bar'
|
||
});
|
||
|
||
// Generate Maintenance Frequency Chart data
|
||
const frequencyCounts: Record<string, number> = {};
|
||
|
||
maintenanceLogs.forEach(log => {
|
||
// Use periodicity if available, otherwise fall back to maintenance_type, or 'Other'
|
||
const frequency = log.periodicity || log.maintenance_type || 'Other';
|
||
frequencyCounts[frequency] = (frequencyCounts[frequency] || 0) + 1;
|
||
});
|
||
|
||
// Sort by count (descending) for better visualization
|
||
const sortedFrequencies = Object.entries(frequencyCounts)
|
||
.sort(([, a], [, b]) => b - a);
|
||
|
||
const frequencyLabels = sortedFrequencies.map(([name]) => name);
|
||
const frequencyValues = sortedFrequencies.map(([, count]) => count);
|
||
const frequencyColors = generateColors(frequencyLabels.length);
|
||
|
||
setMaintenanceFrequencyChartData({
|
||
labels: frequencyLabels,
|
||
datasets: [{
|
||
name: 'Frequency',
|
||
values: frequencyValues,
|
||
colors: frequencyColors
|
||
}],
|
||
type: 'bar'
|
||
});
|
||
} else {
|
||
setAssigneesGroupedChartData(null);
|
||
setMaintenanceFrequencyChartData(null);
|
||
}
|
||
}, [maintenanceLogs]);
|
||
|
||
// Calculate asset uptime and response time from work orders
|
||
useEffect(() => {
|
||
if (workOrders && workOrders.length > 0) {
|
||
let totalResponseHours = 0;
|
||
let responseCount = 0;
|
||
|
||
workOrders.forEach(wo => {
|
||
// Calculate response time if we have creation and first_responded_on
|
||
if (wo.creation && wo.first_responded_on) {
|
||
const created = new Date(wo.creation);
|
||
const responded = new Date(wo.first_responded_on);
|
||
const hours = (responded.getTime() - created.getTime()) / (1000 * 60 * 60);
|
||
if (hours >= 0) {
|
||
totalResponseHours += hours;
|
||
responseCount++;
|
||
}
|
||
}
|
||
});
|
||
|
||
const avgResponseTime = responseCount > 0 ? totalResponseHours / responseCount : 0;
|
||
|
||
// Calculate uptime (assets operational vs under maintenance)
|
||
const operationalCount = workOrders.filter(wo =>
|
||
wo.repair_status?.toLowerCase() === 'completed'
|
||
).length;
|
||
const assetUptime = workOrders.length > 0
|
||
? ((operationalCount / workOrders.length) * 100)
|
||
: 0;
|
||
|
||
setInsights(prev => ({
|
||
...prev,
|
||
assetUptime,
|
||
avgResponseTime
|
||
}));
|
||
}
|
||
}, [workOrders]);
|
||
|
||
if (cardsLoading) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto mb-4"></div>
|
||
<div className="text-gray-600 dark:text-gray-400">Loading dashboard...</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||
<div className="max-w-[1600px] mx-auto">
|
||
|
||
|
||
{/* Top Section: Side-by-Side Layout */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-3 mb-6">
|
||
|
||
{/* LEFT SIDE: 7 columns - Two cards stacked */}
|
||
<div className="lg:col-span-7 space-y-2">
|
||
{/* TOP: All 6 Stats in ONE Container */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 px-4 py-4">
|
||
{/* Inside: 12-column grid, each stat takes 2 columns */}
|
||
<div className="grid grid-cols-12 gap-0 w-full">
|
||
{/* Each box takes col-span-2 (6 boxes × 2 = 12 columns) */}
|
||
<div className="col-span-2">
|
||
<StatCard
|
||
icon={<FaBoxes className="text-indigo-600" />}
|
||
value={numberCards?.total_assets ?? 0}
|
||
label="TOTAL NO. OF ASSETS"
|
||
bgColor="bg-indigo-50 dark:bg-indigo-900/20"
|
||
/>
|
||
</div>
|
||
|
||
<div className="col-span-2">
|
||
<StatCard
|
||
icon={<FaShoppingCart className="text-purple-600" />}
|
||
value={realWorkOrderCounts.open || (numberCards?.work_orders_open ?? 0)}
|
||
label="OPEN WORK ORDERS"
|
||
bgColor="bg-purple-50 dark:bg-purple-900/20"
|
||
/>
|
||
</div>
|
||
|
||
<div className="col-span-2">
|
||
<StatCard
|
||
icon={<FaClock className="text-blue-600" />}
|
||
value={realWorkOrderCounts.inProgress || (numberCards?.work_orders_in_progress ?? 0)}
|
||
label="WORK ORDERS IN PROGRESS"
|
||
bgColor="bg-blue-50 dark:bg-blue-900/20"
|
||
/>
|
||
</div>
|
||
|
||
<div className="col-span-2">
|
||
<StatCard
|
||
icon={<FaCheckCircle className="text-pink-600" />}
|
||
value={realWorkOrderCounts.completed || (numberCards?.work_orders_completed ?? 0)}
|
||
label="COMPLETED WORK ORDERS"
|
||
bgColor="bg-pink-50 dark:bg-pink-900/20"
|
||
/>
|
||
</div>
|
||
|
||
<div className="col-span-2">
|
||
<StatCard
|
||
icon={<FaTools className="text-cyan-600" />}
|
||
value={maintenanceLogs?.length || 0}
|
||
label="MAINTENANCE LOGS"
|
||
bgColor="bg-cyan-50 dark:bg-cyan-900/20"
|
||
/>
|
||
</div>
|
||
|
||
<div className="col-span-2">
|
||
<StatCard
|
||
icon={<FaChartLine className="text-purple-600" />}
|
||
value={realWorkOrderCounts.total}
|
||
label="TOTAL WORK ORDERS"
|
||
bgColor="bg-purple-50 dark:bg-purple-900/20"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* BOTTOM: Completion Rate Card */}
|
||
<CompletionRateCard
|
||
totalWorkOrders={realWorkOrderCounts.total}
|
||
completedWorkOrders={realWorkOrderCounts.completed}
|
||
inProgressWorkOrders={realWorkOrderCounts.inProgress}
|
||
/>
|
||
</div>
|
||
|
||
{/* RIGHT SIDE: Asset Maintenance Assignees Status Count Chart (5 columns - Full height) */}
|
||
<div className="lg:col-span-5">
|
||
<GroupedChartCard
|
||
title="Asset Maintenance Assignees Status Count"
|
||
data={assigneesGroupedChartData || assigneesChart}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Insights Row - Key Metrics (All Dynamic Data) */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||
<InsightCard
|
||
icon={<FaCheckCircle className="text-green-500" />}
|
||
title="Asset Uptime"
|
||
value={`${insights.assetUptime.toFixed(1)}%`}
|
||
trend={insights.assetUptime >= 90 ? "Excellent" : insights.assetUptime >= 75 ? "Good" : "Needs Attention"}
|
||
trendUp={insights.assetUptime >= 75}
|
||
bgColor="bg-green-50 dark:bg-green-900/20"
|
||
/>
|
||
|
||
<InsightCard
|
||
icon={<FaClock className="text-blue-500" />}
|
||
title="Avg Response Time"
|
||
value={insights.avgResponseTime > 0 ? `${insights.avgResponseTime.toFixed(1)} hrs` : 'N/A'}
|
||
trend={insights.avgResponseTime > 0 && insights.avgResponseTime < 4 ? "Fast Response" : "Monitor"}
|
||
trendUp={insights.avgResponseTime > 0 && insights.avgResponseTime < 4}
|
||
bgColor="bg-blue-50 dark:bg-blue-900/20"
|
||
/>
|
||
|
||
<InsightCard
|
||
icon={<FaTools className="text-orange-500" />}
|
||
title="Maintenance Efficiency"
|
||
value={`${insights.maintenanceEfficiency.toFixed(1)}%`}
|
||
trend={insights.maintenanceEfficiency >= 70 ? "On Track" : "Needs Attention"}
|
||
trendUp={insights.maintenanceEfficiency >= 70}
|
||
bgColor="bg-orange-50 dark:bg-orange-900/20"
|
||
/>
|
||
|
||
<InsightCard
|
||
icon={<FaExclamationTriangle className="text-red-500" />}
|
||
title="Overdue Maintenance"
|
||
value={insights.overdueTasks}
|
||
trend={insights.overdueTasks === 0 ? "All Clear" : insights.overdueTasks <= 3 ? "Low" : "High Priority"}
|
||
trendUp={insights.overdueTasks <= 3}
|
||
bgColor="bg-red-50 dark:bg-red-900/20"
|
||
/>
|
||
</div>
|
||
|
||
{/* Second Row - Up & Down Time Chart */}
|
||
<div className="grid grid-cols-1 gap-3 mb-6">
|
||
{/* Up & Down Time - Pie Chart */}
|
||
<div className="w-full">
|
||
<CustomerSatisfactionCard
|
||
data={upDownTimeChartData || upDownChart}
|
||
title="Up & Down Time Chart"
|
||
description="Asset uptime and downtime distribution for tracking availability."
|
||
/>
|
||
</div>
|
||
|
||
{/* Metric Cards */}
|
||
{/* <MetricCard
|
||
icon={<FaTools />}
|
||
label="Total Assets"
|
||
value={numberCards?.total_assets?.toLocaleString() || '0'}
|
||
iconColor="text-purple-600"
|
||
bgColor="bg-gradient-to-br from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20"
|
||
/>
|
||
|
||
<MetricCard
|
||
icon={<FaShoppingCart />}
|
||
label="Total Work Orders"
|
||
value={realWorkOrderCounts.total.toLocaleString()}
|
||
iconColor="text-white"
|
||
bgColor="bg-gradient-to-br from-indigo-500 to-purple-600"
|
||
textColor="text-white"
|
||
/>
|
||
|
||
<MetricCard
|
||
icon={<FaCheckCircle />}
|
||
label="Completed Tasks"
|
||
value={realWorkOrderCounts.completed.toLocaleString()}
|
||
iconColor="text-white"
|
||
bgColor="bg-gradient-to-br from-purple-500 to-pink-600"
|
||
textColor="text-white"
|
||
/>
|
||
|
||
<MetricCard
|
||
icon={<FaClock />}
|
||
label="Pending Tasks"
|
||
value={realWorkOrderCounts.open.toLocaleString()}
|
||
iconColor="text-blue-600"
|
||
bgColor="bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20"
|
||
/> */}
|
||
|
||
<div className="md:col-span-2">
|
||
{/* Placeholder for additional content */}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Additional Charts Section */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 ">
|
||
<ChartCard
|
||
title="Maintenance - Asset wise Count"
|
||
data={maintenanceAssetChartData || assetWiseChart}
|
||
type="bar"
|
||
/>
|
||
|
||
<ChartCard
|
||
title="Maintenance Frequency"
|
||
data={maintenanceFrequencyChartData || frequencyChart}
|
||
type="bar"
|
||
/>
|
||
|
||
{/* <ChartCard
|
||
title="PPM Status"
|
||
data={ppmStatusChart}
|
||
type="bar"
|
||
/> */}
|
||
|
||
{/* Work Order Status Distribution */}
|
||
<DepartmentSalesCard
|
||
data={workOrderChartData || workOrderChart}
|
||
totalWorkOrders={realWorkOrderCounts.total}
|
||
completedWorkOrders={realWorkOrderCounts.completed}
|
||
/>
|
||
|
||
{/* Work Order Status Chart */}
|
||
<GroupedChartCard
|
||
title="Work Order Status Chart"
|
||
data={workOrderGroupedChartData || workOrderChart}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Mini Stat Box Component with border-right (Compact)
|
||
const StatCard: React.FC<{ icon: React.ReactNode; value: string | number; label: string; bgColor: string }> = ({
|
||
icon, value, label, bgColor
|
||
}) => (
|
||
<div className="text-center py-2 px-1 border-r border-gray-200 dark:border-gray-700 last:border-r-0">
|
||
<div className={`w-10 h-10 ${bgColor} rounded-lg flex items-center justify-center text-base mx-auto mb-1.5`}>
|
||
{icon}
|
||
</div>
|
||
<div className="text-lg font-semibold text-gray-900 dark:text-white leading-none mb-1">{value}</div>
|
||
<div className="text-[9px] font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide leading-tight px-1">{label}</div>
|
||
</div>
|
||
);
|
||
|
||
// Insight Card Component - Shows key metrics with trends
|
||
const InsightCard: React.FC<{
|
||
icon: React.ReactNode;
|
||
title: string;
|
||
value: string | number;
|
||
trend: string;
|
||
trendUp: boolean;
|
||
bgColor: string;
|
||
}> = ({ icon, title, value, trend, trendUp, bgColor }) => (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-all p-4 border border-gray-200 dark:border-gray-700">
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div className={`w-10 h-10 ${bgColor} rounded-lg flex items-center justify-center text-lg`}>
|
||
{icon}
|
||
</div>
|
||
<div className={`flex items-center gap-1 text-xs font-medium ${trendUp ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
|
||
{trendUp ? <FaArrowUp className="text-[10px]" /> : <FaArrowDown className="text-[10px]" />}
|
||
{trend}
|
||
</div>
|
||
</div>
|
||
<div className="text-2xl font-semibold text-gray-900 dark:text-white mb-1">{value}</div>
|
||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">{title}</div>
|
||
</div>
|
||
);
|
||
|
||
// Completion Rate Card with Mini Area Chart
|
||
const CompletionRateCard: React.FC<{
|
||
totalWorkOrders: number;
|
||
completedWorkOrders: number;
|
||
inProgressWorkOrders: number;
|
||
}> = ({ totalWorkOrders, completedWorkOrders, inProgressWorkOrders }) => {
|
||
const completionRate = totalWorkOrders > 0
|
||
? ((completedWorkOrders / totalWorkOrders) * 100).toFixed(2)
|
||
: '0.00';
|
||
|
||
return (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-all px-5 border border-gray-200 dark:border-gray-700">
|
||
<div className="mb-3">
|
||
<div className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">{completionRate}%</div>
|
||
<div className="text-sm font-medium text-indigo-600 dark:text-indigo-400 mb-1">Completion Rate</div>
|
||
|
||
</div>
|
||
<div className="mt-1">
|
||
<MiniAreaChart />
|
||
</div>
|
||
|
||
{/* Mini Stats */}
|
||
<div className="grid grid-cols-3 gap-3 mt-2 pt-2 mb-2 pb-2 border-t border-gray-200 dark:border-gray-700">
|
||
<div className="text-center">
|
||
<div className="text-lg font-semibold text-indigo-600 dark:text-indigo-400">{totalWorkOrders}</div>
|
||
<div className="text-[9px] font-medium text-gray-500 dark:text-gray-400 uppercase">Total</div>
|
||
</div>
|
||
<div className="text-center">
|
||
<div className="text-lg font-semibold text-blue-600 dark:text-blue-400">{inProgressWorkOrders}</div>
|
||
<div className="text-[9px] font-medium text-gray-500 dark:text-gray-400 uppercase">In Progress</div>
|
||
</div>
|
||
<div className="text-center">
|
||
<div className="text-lg font-semibold text-green-600 dark:text-green-400">{completedWorkOrders}</div>
|
||
<div className="text-[9px] font-medium text-gray-500 dark:text-gray-400 uppercase">Completed</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
|
||
// Department Sales Card (Full Height to match left side total height)
|
||
const DepartmentSalesCard: React.FC<{ data: any; totalWorkOrders: number; completedWorkOrders: number }> = ({
|
||
data, totalWorkOrders, completedWorkOrders
|
||
}) => {
|
||
return (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-all px-5 py-5 border border-gray-200 dark:border-gray-700 flex flex-col" style={{ minHeight: '100%' }}>
|
||
<div className="flex items-center justify-between mb-4 flex-shrink-0">
|
||
<div>
|
||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">Work Order Status Distribution</h3>
|
||
<div className="flex items-center gap-3 mt-1">
|
||
<div className="text-xl font-semibold text-gray-900 dark:text-white">
|
||
{totalWorkOrders} Total
|
||
</div>
|
||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||
<span className="font-medium">{completedWorkOrders} Completed</span>
|
||
<span className="ml-1">({totalWorkOrders > 0 ? ((completedWorkOrders / totalWorkOrders) * 100).toFixed(1) : 0}%)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
||
<svg className="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="flex-1">
|
||
<BarChart data={data} />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Pie Chart Card with Custom Title (Compact Version)
|
||
const CustomerSatisfactionCard: React.FC<{ data: any; title: string; description: string }> = ({
|
||
data, title, description
|
||
}) => {
|
||
return (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-all p-5 border border-gray-200 dark:border-gray-700">
|
||
<h3 className="text-base font-semibold text-gray-900 dark:text-white mb-1">{title}</h3>
|
||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-4">
|
||
{description}
|
||
</p>
|
||
{data ? <PieChart data={data} /> : (
|
||
<div className="h-64 flex items-center justify-center text-gray-400">
|
||
No data available
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Metric Card Component (Bottom Row - Smaller & Compact)
|
||
const MetricCard: React.FC<{
|
||
icon: React.ReactNode;
|
||
label: string;
|
||
value: string;
|
||
iconColor?: string;
|
||
bgColor: string;
|
||
textColor?: string;
|
||
}> = ({ icon, label, value, iconColor = "text-white", bgColor, textColor = "text-gray-900 dark:text-white" }) => (
|
||
<div className={`${bgColor} rounded-lg shadow hover:shadow-md transition-all p-5 border border-gray-200 dark:border-gray-700`}>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className={`text-2xl ${iconColor}`}>
|
||
{icon}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className={`text-[10px] font-medium uppercase tracking-wide mb-1 ${textColor.includes('white') ? 'text-white/80' : 'text-gray-600 dark:text-gray-400'}`}>
|
||
{label}
|
||
</div>
|
||
<div className={`text-2xl font-semibold ${textColor}`}>{value}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// Mini Area Chart Component
|
||
const MiniAreaChart: React.FC = () => {
|
||
const data = [10, 15, 13, 17, 14, 18, 16];
|
||
const max = Math.max(...data);
|
||
const width = 400;
|
||
const height = 80;
|
||
|
||
const points = data.map((value, i) => {
|
||
const x = (i / (data.length - 1)) * width;
|
||
const y = height - (value / max) * height;
|
||
return `${x},${y}`;
|
||
}).join(' ');
|
||
|
||
const areaPoints = `0,${height} ${points} ${width},${height}`;
|
||
|
||
return (
|
||
<svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} className="w-full">
|
||
<defs>
|
||
<linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||
<stop offset="0%" style={{ stopColor: '#8B5CF6', stopOpacity: 0.3 }} />
|
||
<stop offset="100%" style={{ stopColor: '#8B5CF6', stopOpacity: 0.05 }} />
|
||
</linearGradient>
|
||
</defs>
|
||
<polygon points={areaPoints} fill="url(#areaGradient)" />
|
||
<polyline
|
||
points={points}
|
||
fill="none"
|
||
stroke="#8B5CF6"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
</svg>
|
||
);
|
||
};
|
||
|
||
// Grouped Bar Chart Component for multiple datasets
|
||
const GroupedBarChart: React.FC<{ data: any }> = ({ data }) => {
|
||
const labels = data?.labels || [];
|
||
const datasets = data?.datasets || [];
|
||
|
||
if (!datasets.length) {
|
||
return <div className="text-center text-gray-400 py-4 text-sm">No data available</div>;
|
||
}
|
||
|
||
const allValues = datasets.flatMap((ds: any) => ds.values || []);
|
||
const max = Math.max(...allValues, 1);
|
||
const chartHeight = 240;
|
||
const width = Math.max(800, labels.length * 100);
|
||
const groupWidth = Math.min(120, (width - 100) / labels.length);
|
||
const barSpacing = 4;
|
||
const numBars = datasets.length;
|
||
const barWidth = Math.max(12, (groupWidth - barSpacing * (numBars + 1)) / numBars);
|
||
|
||
// Color mapping for status types - Blue and Purple palette
|
||
const statusColors: Record<string, string> = {
|
||
'Completed On Time': '#6366F1', // Indigo
|
||
'Completed Within SLA': '#3B82F6', // Blue
|
||
'Delay In Completion': '#8B5CF6', // Purple
|
||
'Pending': '#A855F7', // Purple variant
|
||
'Overdue': '#EC4899', // Pink (for contrast/alert)
|
||
'Cancelled': '#0EA5E9', // Sky blue
|
||
'Open': '#6366F1', // Indigo
|
||
'Work In Progress': '#3B82F6', // Blue
|
||
'Pending Review': '#8B5CF6', // Purple
|
||
'Completed': '#6366F1', // Indigo
|
||
'Closed': '#A855F7', // Purple variant
|
||
};
|
||
|
||
// Generate colors for datasets
|
||
const getDatasetColor = (datasetName: string, index: number): string => {
|
||
// Try to match by name first
|
||
for (const [key, color] of Object.entries(statusColors)) {
|
||
if (datasetName.toLowerCase().includes(key.toLowerCase())) {
|
||
return color;
|
||
}
|
||
}
|
||
// Fallback to generated colors
|
||
return generateColors(numBars)[index];
|
||
};
|
||
|
||
return (
|
||
<div className="relative w-full overflow-x-auto">
|
||
<svg width="100%" height={chartHeight + 40} viewBox={`0 0 ${width} ${chartHeight + 40}`} className="w-full" preserveAspectRatio="xMidYMid meet">
|
||
<defs>
|
||
{datasets.map((ds: any, i: number) => {
|
||
const color = getDatasetColor(ds.name || '', i);
|
||
return (
|
||
<linearGradient key={i} id={`groupedBarGradient${i}`} x1="0%" y1="0%" x2="0%" y2="100%">
|
||
<stop offset="0%" style={{ stopColor: color, stopOpacity: 0.9 }} />
|
||
<stop offset="100%" style={{ stopColor: color, stopOpacity: 0.7 }} />
|
||
</linearGradient>
|
||
);
|
||
})}
|
||
</defs>
|
||
|
||
{/* Grid lines */}
|
||
{[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
|
||
<line
|
||
key={i}
|
||
x1="60"
|
||
y1={chartHeight - (ratio * chartHeight)}
|
||
x2={width - 20}
|
||
y2={chartHeight - (ratio * chartHeight)}
|
||
stroke="#E5E7EB"
|
||
strokeWidth="1"
|
||
className="dark:stroke-gray-700"
|
||
/>
|
||
))}
|
||
|
||
{/* Y-axis labels */}
|
||
{[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
|
||
<text
|
||
key={i}
|
||
x="45"
|
||
y={chartHeight - (ratio * chartHeight) + 4}
|
||
textAnchor="end"
|
||
className="text-xs fill-gray-400 dark:fill-gray-500"
|
||
>
|
||
{(max * ratio).toFixed(ratio === 0 ? 0 : max < 5 ? 1 : 0)}
|
||
</text>
|
||
))}
|
||
|
||
{/* Y-axis label */}
|
||
<text
|
||
x="15"
|
||
y={chartHeight / 2}
|
||
textAnchor="middle"
|
||
transform={`rotate(-90, 15, ${chartHeight / 2})`}
|
||
className="text-xs fill-gray-600 dark:fill-gray-400 font-medium"
|
||
>
|
||
Count
|
||
</text>
|
||
|
||
{/* Grouped Bars */}
|
||
{labels.map((label: string, labelIndex: number) => {
|
||
const groupX = 80 + (labelIndex * groupWidth);
|
||
|
||
return (
|
||
<g key={labelIndex}>
|
||
{datasets.map((dataset: any, dsIndex: number) => {
|
||
const value = dataset.values?.[labelIndex] || 0;
|
||
const barHeight = (value / max) * chartHeight;
|
||
const x = groupX + barSpacing + (dsIndex * (barWidth + barSpacing));
|
||
const y = chartHeight - barHeight;
|
||
const color = getDatasetColor(dataset.name || '', dsIndex);
|
||
|
||
return (
|
||
<g key={dsIndex}>
|
||
<rect
|
||
x={x}
|
||
y={y}
|
||
width={barWidth}
|
||
height={barHeight}
|
||
fill={color}
|
||
rx="4"
|
||
ry="4"
|
||
className="hover:opacity-80 cursor-pointer transition-opacity"
|
||
/>
|
||
</g>
|
||
);
|
||
})}
|
||
|
||
{/* X-axis label */}
|
||
<text
|
||
x={groupX + groupWidth / 2}
|
||
y={chartHeight + 15}
|
||
textAnchor="middle"
|
||
className="text-xs fill-gray-600 dark:fill-gray-400"
|
||
>
|
||
{label && label.length > 12 ? label.substring(0, 10) + '...' : label || 'null'}
|
||
</text>
|
||
</g>
|
||
);
|
||
})}
|
||
</svg>
|
||
|
||
{/* Legend */}
|
||
<div className="flex flex-wrap items-center justify-center gap-2 mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||
{datasets.map((dataset: any, i: number) => {
|
||
const color = getDatasetColor(dataset.name || '', i);
|
||
return (
|
||
<div key={i} className="flex items-center gap-1.5">
|
||
<div className="w-2.5 h-2.5 rounded" style={{ backgroundColor: color }}></div>
|
||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||
{dataset.name && dataset.name.length > 20 ? dataset.name.substring(0, 18) + '...' : dataset.name || `Series ${i + 1}`}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Bar Chart Component with Line Overlay (Like in Image)
|
||
const BarChart: React.FC<{ data: any }> = ({ data }) => {
|
||
const labels = data?.labels || [];
|
||
const datasets = data?.datasets || [];
|
||
|
||
if (!datasets.length) {
|
||
return <div className="text-center text-gray-400 py-8">No data available</div>;
|
||
}
|
||
|
||
const allValues = datasets.flatMap((ds: any) => ds.values || []);
|
||
const max = Math.max(...allValues, 1);
|
||
const chartHeight = 250;
|
||
// Increase width based on number of labels to prevent overlap
|
||
const minBarSpacing = 60; // Minimum spacing between bars
|
||
const calculatedWidth = Math.max(800, labels.length * minBarSpacing + 100);
|
||
const width = calculatedWidth;
|
||
|
||
// Generate smooth line data (Average line overlay)
|
||
const lineData = datasets[0]?.values || [];
|
||
|
||
return (
|
||
<div className="relative w-full overflow-x-auto">
|
||
<svg width="100%" height="320" viewBox={`0 0 ${width} ${chartHeight + 70}`} className="w-full" preserveAspectRatio="xMidYMid meet">
|
||
<defs>
|
||
<linearGradient id="barGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||
<stop offset="0%" style={{ stopColor: '#8B5CF6', stopOpacity: 0.8 }} />
|
||
<stop offset="100%" style={{ stopColor: '#8B5CF6', stopOpacity: 0.6 }} />
|
||
</linearGradient>
|
||
</defs>
|
||
|
||
{/* Grid lines */}
|
||
{[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
|
||
<line
|
||
key={i}
|
||
x1="60"
|
||
y1={chartHeight - (ratio * chartHeight)}
|
||
x2={width - 20}
|
||
y2={chartHeight - (ratio * chartHeight)}
|
||
stroke="#E5E7EB"
|
||
strokeWidth="1"
|
||
className="dark:stroke-gray-700"
|
||
/>
|
||
))}
|
||
|
||
{/* Y-axis labels */}
|
||
{[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
|
||
<text
|
||
key={i}
|
||
x="45"
|
||
y={chartHeight - (ratio * chartHeight) + 4}
|
||
textAnchor="end"
|
||
className="text-xs fill-gray-400 dark:fill-gray-500"
|
||
>
|
||
{(max * ratio).toFixed(0)}
|
||
</text>
|
||
))}
|
||
|
||
{/* Bars */}
|
||
{labels.map((_label: string, i: number) => {
|
||
const value = datasets[0]?.values?.[i] || 0;
|
||
const barHeight = (value / max) * chartHeight;
|
||
const barWidth = Math.min(40, (width - 100) / labels.length - 10);
|
||
const x = 80 + (i * ((width - 100) / labels.length));
|
||
|
||
return (
|
||
<g key={i}>
|
||
<rect
|
||
x={x}
|
||
y={chartHeight - barHeight}
|
||
width={barWidth}
|
||
height={barHeight}
|
||
fill={`url(#barGradient)`}
|
||
rx="4"
|
||
ry="4"
|
||
className="hover:opacity-80 cursor-pointer transition-opacity"
|
||
/>
|
||
</g>
|
||
);
|
||
})}
|
||
|
||
{/* Line Chart Overlay */}
|
||
{lineData.length > 0 && (
|
||
<polyline
|
||
points={lineData.map((value: number, i: number) => {
|
||
const x = 80 + (i * ((width - 100) / labels.length)) + 20;
|
||
const y = chartHeight - ((value / max) * chartHeight);
|
||
return `${x},${y}`;
|
||
}).join(' ')}
|
||
fill="none"
|
||
stroke="#60A5FA"
|
||
strokeWidth="3"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
className="drop-shadow-md"
|
||
/>
|
||
)}
|
||
|
||
{/* Line data points */}
|
||
{lineData.map((value: number, i: number) => {
|
||
const x = 80 + (i * ((width - 100) / labels.length)) + 20;
|
||
const y = chartHeight - ((value / max) * chartHeight);
|
||
return (
|
||
<circle
|
||
key={i}
|
||
cx={x}
|
||
cy={y}
|
||
r="4"
|
||
fill="#3B82F6"
|
||
className="hover:r-6 cursor-pointer transition-all"
|
||
/>
|
||
);
|
||
})}
|
||
|
||
{/* X-axis labels */}
|
||
{labels.map((label: string, i: number) => {
|
||
const barSpacing = (width - 100) / labels.length;
|
||
const x = 80 + (i * barSpacing) + (barSpacing / 2);
|
||
const truncatedLabel = label && label.length > 15 ? label.substring(0, 13) + '...' : label || 'null';
|
||
|
||
return (
|
||
<text
|
||
key={i}
|
||
x={x}
|
||
y={chartHeight + 50}
|
||
transform={`rotate(-45, ${x}, ${chartHeight + 50})`}
|
||
textAnchor="middle"
|
||
className="text-xs fill-gray-600 dark:fill-gray-400"
|
||
>
|
||
{truncatedLabel}
|
||
</text>
|
||
);
|
||
})}
|
||
</svg>
|
||
|
||
{/* Legend - Inside the card */}
|
||
<div className="flex items-center justify-center gap-3 mt-2">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-3 h-3 rounded" style={{ background: '#8B5CF6' }}></div>
|
||
<span className="text-xs text-gray-600 dark:text-gray-400">Total Sales</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||
<span className="text-xs text-gray-600 dark:text-gray-400">Average</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Pie Chart Component
|
||
const PieChart: React.FC<{ data: any }> = ({ data }) => {
|
||
const labels = data?.labels || [];
|
||
const values = data?.datasets?.[0]?.values || [];
|
||
|
||
// Check if this is an Up & Down Time chart and ALWAYS apply blue/purple colors
|
||
const isUpDownTimeChart = labels.some((label: string) =>
|
||
label.toLowerCase().includes('up time') ||
|
||
label.toLowerCase().includes('down time') ||
|
||
label.toLowerCase().includes('uptime') ||
|
||
label.toLowerCase().includes('downtime')
|
||
);
|
||
|
||
let colors: string[] = [];
|
||
|
||
// Always override colors for Up & Down Time charts to use blue/purple palette
|
||
if (isUpDownTimeChart && labels.length >= 1) {
|
||
// Apply blue/purple colors based on label order and content
|
||
colors = labels.map((label: string) => {
|
||
const labelLower = label.toLowerCase();
|
||
if (labelLower.includes('up time') || labelLower.includes('uptime')) {
|
||
return '#6366F1'; // Indigo for Up Time
|
||
}
|
||
if (labelLower.includes('down time') || labelLower.includes('downtime')) {
|
||
return '#8B5CF6'; // Purple for Down Time
|
||
}
|
||
// If label doesn't match, assign based on position (first = up, second = down)
|
||
const index = labels.indexOf(label);
|
||
return index === 0 ? '#6366F1' : '#8B5CF6';
|
||
});
|
||
} else {
|
||
// For other charts, use custom colors if provided, otherwise generate
|
||
const datasetColors = data?.datasets?.[0]?.colors;
|
||
colors = datasetColors && datasetColors.length === values.length
|
||
? datasetColors
|
||
: generateColors(values.length);
|
||
}
|
||
|
||
const total = values.reduce((sum: number, val: number) => sum + val, 0);
|
||
const radius = 100;
|
||
const cx = radius + 10;
|
||
const cy = radius + 10;
|
||
|
||
let cumulative = 0;
|
||
const slices = values.map((value: number, i: number) => {
|
||
const startAngle = (cumulative / total) * 2 * Math.PI - Math.PI / 2;
|
||
cumulative += value;
|
||
const endAngle = (cumulative / total) * 2 * Math.PI - Math.PI / 2;
|
||
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
|
||
|
||
const x1 = cx + radius * Math.cos(startAngle);
|
||
const y1 = cy + radius * Math.sin(startAngle);
|
||
const x2 = cx + radius * Math.cos(endAngle);
|
||
const y2 = cy + radius * Math.sin(endAngle);
|
||
|
||
return {
|
||
path: `M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`,
|
||
color: colors[i],
|
||
label: labels[i],
|
||
value: value,
|
||
percentage: ((value / total) * 100).toFixed(1)
|
||
};
|
||
});
|
||
|
||
return (
|
||
<div className="flex flex-col md:flex-row items-center justify-around">
|
||
<svg width={cx * 2} height={cy * 2} viewBox={`0 0 ${cx * 2} ${cy * 2}`} className="max-w-xs">
|
||
{slices.map((slice: any, i: number) => (
|
||
<path
|
||
key={i}
|
||
d={slice.path}
|
||
fill={slice.color}
|
||
className="hover:opacity-80 transition-opacity cursor-pointer drop-shadow-lg"
|
||
/>
|
||
))}
|
||
</svg>
|
||
<div className="flex flex-col gap-3 mt-4 md:mt-0">
|
||
{slices.map((slice: any, i: number) => (
|
||
<div key={i} className="flex items-center gap-3">
|
||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: slice.color }}></div>
|
||
<span className="text-sm text-gray-700 dark:text-gray-300">{slice.label}</span>
|
||
<span className="text-sm font-bold text-gray-900 dark:text-white">{slice.value}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Grouped Chart Card Component for charts with multiple datasets
|
||
const GroupedChartCard: React.FC<{ title: string; data: any }> = ({ title, data }) => {
|
||
return (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-all p-3 border border-gray-200 dark:border-gray-700">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h3 className="text-sm font-semibold text-gray-800 dark:text-white">{title}</h3>
|
||
<div className="flex items-center gap-1">
|
||
<button className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
||
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||
</svg>
|
||
</button>
|
||
<button className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
||
<svg className="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{!data || !data.datasets || !data.datasets.length ? (
|
||
<div className="h-48 flex items-center justify-center text-gray-400">
|
||
<div className="text-center">
|
||
<div className="text-xs">No chart data available</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<GroupedBarChart data={data} />
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Chart Card Component (Compact Version)
|
||
const ChartCard: React.FC<{ title: string; data: any; type: 'bar' | 'pie' }> = ({ title, data, type }) => {
|
||
return (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-all p-5 border border-gray-200 dark:border-gray-700">
|
||
<h3 className="text-base font-semibold text-gray-800 dark:text-white mb-4">{title}</h3>
|
||
{!data || !data.datasets || !data.datasets.length ? (
|
||
<div className="h-64 flex items-center justify-center text-gray-400">
|
||
<div className="text-center">
|
||
<div className="text-sm">No chart data available</div>
|
||
</div>
|
||
</div>
|
||
) : type === 'pie' ? (
|
||
<PieChart data={data} />
|
||
) : (
|
||
<BarChart data={data} />
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Helper: Generate colors
|
||
function generateColors(count: number): string[] {
|
||
const colors = [
|
||
'#8B5CF6', // Purple
|
||
'#6366F1', // Indigo
|
||
'#3B82F6', // Blue
|
||
'#06B6D4', // Cyan
|
||
'#14B8A6', // Teal
|
||
'#EC4899', // Pink
|
||
'#A855F7', // Purple variant
|
||
'#0EA5E9', // Sky blue
|
||
'#10B981', // Emerald
|
||
'#F472B6', // Pink variant
|
||
'#7C3AED', // Violet
|
||
'#2DD4BF', // Teal variant
|
||
];
|
||
return Array.from({ length: count }, (_, i) => colors[i % colors.length]);
|
||
}
|
||
|
||
export default ModernDashboard;
|
||
|