Seera/src/pages/ModernDashboard.tsx
2025-11-03 13:38:27 +05:30

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;