377 lines
16 KiB
TypeScript
377 lines
16 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useNumberCards, useDashboardChart } from '../hooks/useApi';
|
|
|
|
const ModernDashboard: React.FC = () => {
|
|
const [activeTab, setActiveTab] = useState<'variation1' | 'variation2'>('variation1');
|
|
const { data: numberCards, loading: cardsLoading } = useNumberCards();
|
|
|
|
// Fetch all charts
|
|
const { data: upDownChart, error: upDownError } = useDashboardChart('Up & Down Time Chart');
|
|
const { data: workOrderChart, error: workOrderError } = useDashboardChart('Work Order Status Chart');
|
|
const { data: assetWiseChart, error: assetWiseError } = useDashboardChart('Maintenance - Asset wise Count');
|
|
const { data: assigneesChart, error: assigneesError } = useDashboardChart('Asset Maintenance Assignees Status Count');
|
|
const { data: frequencyChart, error: frequencyError } = useDashboardChart('Asset Maintenance Frequency Chart');
|
|
const { data: ppmStatusChart, error: ppmStatusError } = useDashboardChart('PPM Status');
|
|
const { data: ppmTemplateChart, error: ppmTemplateError } = useDashboardChart('PPM Template Counts');
|
|
|
|
// Log errors for debugging
|
|
React.useEffect(() => {
|
|
if (upDownError) console.error('Up & Down Time Chart error:', upDownError);
|
|
if (workOrderError) console.error('Work Order Status Chart error:', workOrderError);
|
|
if (assetWiseError) console.error('Maintenance - Asset wise Count error:', assetWiseError);
|
|
if (assigneesError) console.error('Asset Maintenance Assignees Status Count error:', assigneesError);
|
|
if (frequencyError) console.error('Asset Maintenance Frequency Chart error:', frequencyError);
|
|
if (ppmStatusError) console.error('PPM Status error:', ppmStatusError);
|
|
if (ppmTemplateError) console.error('PPM Template Counts error:', ppmTemplateError);
|
|
}, [upDownError, workOrderError, assetWiseError, assigneesError, frequencyError, ppmStatusError, ppmTemplateError]);
|
|
|
|
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="p-6 bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
<div className="max-w-7xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">Asset Management</h1>
|
|
<p className="text-gray-600 dark:text-gray-400">Analytics Dashboard</p>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
{/* <div className="flex space-x-2 mb-6">
|
|
<button
|
|
onClick={() => setActiveTab('variation1')}
|
|
className={`px-6 py-2.5 rounded-lg font-medium text-sm transition-all shadow ${
|
|
activeTab === 'variation1'
|
|
? 'bg-indigo-600 text-white'
|
|
: 'bg-white text-gray-600 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
Variation 1
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('variation2')}
|
|
className={`px-6 py-2.5 rounded-lg font-medium text-sm transition-all shadow ${
|
|
activeTab === 'variation2'
|
|
? 'bg-indigo-600 text-white'
|
|
: 'bg-white text-gray-600 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
Variation 2
|
|
</button>
|
|
</div> */}
|
|
|
|
{/* Number Cards - Matching Frappe Style */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<NumberCard
|
|
title="TOTAL NO. OF ASSETS"
|
|
value={numberCards?.total_assets ?? 0}
|
|
subtitle="0 % since yesterday"
|
|
color="border-indigo-500"
|
|
/>
|
|
<NumberCard
|
|
title="OPEN WORK ORDERS"
|
|
value={numberCards?.work_orders_open ?? 0}
|
|
subtitle="0 % since yesterday"
|
|
color="border-blue-500"
|
|
/>
|
|
<NumberCard
|
|
title="WORK ORDERS IN PROGRESS"
|
|
value={numberCards?.work_orders_in_progress ?? 0}
|
|
subtitle="0 % since yesterday"
|
|
color="border-purple-500"
|
|
/>
|
|
<NumberCard
|
|
title="COMPLETED WORK ORDERS"
|
|
value={numberCards?.work_orders_completed ?? 0}
|
|
subtitle="0 % since yesterday"
|
|
color="border-green-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Quick Links */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
|
|
<QuickLink title="Repair Cost" icon="💰" />
|
|
<QuickLink title="Planned PMs" icon="📋" />
|
|
</div>
|
|
|
|
{/* Charts Section */}
|
|
<div className="space-y-6">
|
|
{/* Up & Down Time Chart */}
|
|
<ChartCard title="Up & Down Time Chart" data={upDownChart} type="pie" />
|
|
|
|
{/* Work Order Status Chart */}
|
|
<ChartCard title="Work Order Status Chart" data={workOrderChart} type="bar" />
|
|
|
|
{/* Maintenance - Asset wise Count */}
|
|
<ChartCard title="Maintenance - Asset wise Count" data={assetWiseChart} type="bar" />
|
|
|
|
{/* Asset Maintenance Assignees Status Count */}
|
|
<ChartCard title="Asset Maintenance Assignees Status Count" data={assigneesChart} type="bar" />
|
|
|
|
{/* Asset Maintenance Frequency Chart */}
|
|
<ChartCard title="Asset Maintenance Frequency Chart" data={frequencyChart} type="bar" />
|
|
|
|
{/* PPM Status */}
|
|
<ChartCard title="PPM Status" data={ppmStatusChart} type="bar" />
|
|
|
|
{/* PPM Template Counts */}
|
|
<ChartCard title="PPM Template Counts" data={ppmTemplateChart} type="bar" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Number Card Component (Frappe Style)
|
|
const NumberCard: React.FC<{ title: string; value: number; subtitle: string; color: string }> = ({
|
|
title,
|
|
value,
|
|
subtitle,
|
|
color,
|
|
}) => (
|
|
<div className={`bg-white dark:bg-gray-800 rounded-lg p-6 border-l-4 ${color} shadow-md hover:shadow-lg transition-shadow`}>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">{title}</div>
|
|
<div className="text-4xl font-bold text-gray-900 dark:text-white mb-2">{value.toLocaleString()}</div>
|
|
<div className="text-xs text-gray-400 dark:text-gray-500">{subtitle}</div>
|
|
</div>
|
|
<button className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
|
|
<svg className="w-5 h-5" 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>
|
|
);
|
|
|
|
// Quick Link Component
|
|
const QuickLink: React.FC<{ title: string; icon: string }> = ({ title, icon }) => (
|
|
<button className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow hover:shadow-md transition-all flex items-center space-x-3 text-left group">
|
|
<span className="text-2xl">{icon}</span>
|
|
<div>
|
|
<div className="text-sm font-semibold text-indigo-600 dark:text-indigo-400 group-hover:text-indigo-700 dark:group-hover:text-indigo-300">{title}</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 flex items-center mt-1">
|
|
View details
|
|
<svg className="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
|
|
// Chart Card Component
|
|
const ChartCard: React.FC<{ title: string; data: any; type: 'bar' | 'pie' }> = ({ title, data, type }) => {
|
|
// Log data for debugging
|
|
React.useEffect(() => {
|
|
console.log(`${title} data:`, data);
|
|
}, [title, data]);
|
|
|
|
return (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">{title}</h3>
|
|
<div className="flex items-center space-x-2">
|
|
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
|
|
<svg className="w-5 h-5 text-gray-500 dark:text-gray-400" 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-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
|
|
<svg className="w-5 h-5 text-gray-500 dark:text-gray-400" 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-64 flex items-center justify-center text-gray-400 dark:text-gray-500">
|
|
<div className="text-center">
|
|
<svg className="w-12 h-12 mx-auto mb-2 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
<div className="text-sm">No chart data available</div>
|
|
<div className="text-xs text-gray-400 dark:text-gray-500 mt-2">Check browser console for errors</div>
|
|
</div>
|
|
</div>
|
|
) : type === 'pie' ? (
|
|
<PieChart data={data} />
|
|
) : (
|
|
<BarChart data={data} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Pie Chart Component
|
|
const PieChart: React.FC<{ data: any }> = ({ data }) => {
|
|
const labels = data.labels || [];
|
|
const values = data.datasets[0]?.values || [];
|
|
const colors = data.datasets[0]?.color ? [data.datasets[0].color] : generateColors(values.length);
|
|
|
|
const total = values.reduce((sum: number, val: number) => sum + val, 0);
|
|
const radius = 120;
|
|
const cx = radius + 20;
|
|
const cy = radius + 20;
|
|
|
|
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 % colors.length],
|
|
label: labels[i],
|
|
value: value,
|
|
};
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col items-center">
|
|
<svg width={cx * 2} height={cy * 2} viewBox={`0 0 ${cx * 2} ${cy * 2}`} className="max-w-sm mx-auto">
|
|
{slices.map((slice: any, i: number) => (
|
|
<path key={i} d={slice.path} fill={slice.color} className="hover:opacity-80 transition-opacity" />
|
|
))}
|
|
</svg>
|
|
<div className="mt-6 flex flex-wrap justify-center gap-4">
|
|
{slices.map((slice: any, i: number) => (
|
|
<div key={i} className="flex items-center space-x-2">
|
|
<div className="w-4 h-4 rounded" style={{ backgroundColor: slice.color }}></div>
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">
|
|
{slice.label}
|
|
<span className="ml-2 font-semibold">{slice.value.toLocaleString()}</span>
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Bar Chart Component
|
|
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>;
|
|
}
|
|
|
|
// Calculate max value across all datasets
|
|
const allValues = datasets.flatMap((ds: any) => ds.values || []);
|
|
const max = Math.max(...allValues, 1);
|
|
|
|
const chartHeight = 300;
|
|
const barSpacing = 8;
|
|
const groupWidth = Math.max(60, Math.min(120, (800 - 80) / labels.length));
|
|
|
|
return (
|
|
<div className="overflow-x-auto">
|
|
<div className="min-w-full" style={{ minWidth: `${labels.length * groupWidth + 80}px` }}>
|
|
<div className="flex items-end justify-between h-80 px-4 pb-12 relative">
|
|
{/* Y-axis */}
|
|
<div className="absolute left-0 bottom-12 top-0 flex flex-col justify-between text-xs text-gray-400 dark:text-gray-500 pr-2">
|
|
<span>{max.toFixed(0)}</span>
|
|
<span>{(max * 0.75).toFixed(0)}</span>
|
|
<span>{(max * 0.5).toFixed(0)}</span>
|
|
<span>{(max * 0.25).toFixed(0)}</span>
|
|
<span>0</span>
|
|
</div>
|
|
|
|
{/* Bars */}
|
|
<div className="flex-1 flex items-end justify-around h-full ml-8">
|
|
{labels.map((label: string, i: number) => {
|
|
const barWidth = Math.max(20, (groupWidth - barSpacing * (datasets.length + 1)) / datasets.length);
|
|
|
|
return (
|
|
<div key={i} className="flex flex-col items-center h-full justify-end">
|
|
<div className="flex items-end space-x-1 h-full">
|
|
{datasets.map((dataset: any, dsIndex: number) => {
|
|
const value = dataset.values?.[i] || 0;
|
|
const height = (value / max) * chartHeight;
|
|
const color = dataset.color || generateColors(datasets.length)[dsIndex];
|
|
|
|
return (
|
|
<div
|
|
key={dsIndex}
|
|
className="rounded-t hover:opacity-80 transition-all cursor-pointer group relative"
|
|
style={{
|
|
width: `${barWidth}px`,
|
|
height: `${height}px`,
|
|
backgroundColor: color,
|
|
minHeight: value > 0 ? '4px' : '0px',
|
|
}}
|
|
>
|
|
<div className="absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
|
|
{value.toLocaleString()}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="text-xs text-gray-600 dark:text-gray-400 mt-2 text-center max-w-[80px] truncate" title={label}>
|
|
{label}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Legend */}
|
|
{datasets.length > 1 && (
|
|
<div className="flex flex-wrap justify-center gap-4 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
{datasets.map((dataset: any, i: number) => (
|
|
<div key={i} className="flex items-center space-x-2">
|
|
<div
|
|
className="w-4 h-4 rounded"
|
|
style={{ backgroundColor: dataset.color || generateColors(datasets.length)[i] }}
|
|
></div>
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">{dataset.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Helper: Generate colors
|
|
function generateColors(count: number): string[] {
|
|
const colors = [
|
|
'#4F46E5', // Indigo
|
|
'#10B981', // Green
|
|
'#F59E0B', // Amber
|
|
'#EF4444', // Red
|
|
'#8B5CF6', // Purple
|
|
'#06B6D4', // Cyan
|
|
'#F97316', // Orange
|
|
'#EC4899', // Pink
|
|
'#14B8A6', // Teal
|
|
'#6366F1', // Violet
|
|
];
|
|
return Array.from({ length: count }, (_, i) => colors[i % colors.length]);
|
|
}
|
|
|
|
export default ModernDashboard;
|