Seera/src/pages/ModernDashboard.tsx

1297 lines
48 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;