410 lines
17 KiB
TypeScript
410 lines
17 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useAuth, useDashboardStats, useUserDetails, useNumberCards } from '../hooks/useApi';
|
|
import ApiTest from '../components/ApiTest';
|
|
import ChartTile from '../components/ChartTile';
|
|
|
|
// Define interfaces locally
|
|
interface UserDetails {
|
|
user_id: string;
|
|
full_name: string;
|
|
email: string;
|
|
user_image?: string;
|
|
roles: string[];
|
|
permissions: Record<string, {
|
|
read: boolean;
|
|
write: boolean;
|
|
create: boolean;
|
|
delete: boolean;
|
|
}>;
|
|
last_login?: string;
|
|
enabled: boolean;
|
|
creation: string;
|
|
modified: string;
|
|
language: string;
|
|
}
|
|
|
|
interface DocTypeRecord {
|
|
name: string;
|
|
creation: string;
|
|
modified: string;
|
|
modified_by: string;
|
|
owner: string;
|
|
docstatus: number;
|
|
[key: string]: any;
|
|
}
|
|
|
|
const Dashboard: React.FC = () => {
|
|
const [user, setUser] = useState<UserDetails | null>(null);
|
|
const [recentRecords, setRecentRecords] = useState<DocTypeRecord[]>([]);
|
|
const navigate = useNavigate();
|
|
const { logout } = useAuth();
|
|
|
|
// Use the new API hooks
|
|
const { loading: statsLoading, error: statsError } = useDashboardStats();
|
|
const { data: numberCards } = useNumberCards();
|
|
const { data: userDetails, loading: userLoading, error: userError } = useUserDetails();
|
|
|
|
useEffect(() => {
|
|
// Set user from stored data or API response
|
|
const storedUser = localStorage.getItem('user');
|
|
if (storedUser) {
|
|
setUser(JSON.parse(storedUser));
|
|
} else if (userDetails) {
|
|
setUser(userDetails);
|
|
}
|
|
|
|
// Set demo records for now (you can replace this with real data later)
|
|
const demoRecords: DocTypeRecord[] = [
|
|
{
|
|
name: 'USER001',
|
|
full_name: 'John Doe',
|
|
email: 'john.doe@seeraarabia.com',
|
|
creation: new Date().toISOString(),
|
|
modified: new Date().toISOString(),
|
|
modified_by: 'system',
|
|
owner: 'system',
|
|
docstatus: 0
|
|
},
|
|
{
|
|
name: 'USER002',
|
|
full_name: 'Jane Smith',
|
|
email: 'jane.smith@seeraarabia.com',
|
|
creation: new Date(Date.now() - 86400000).toISOString(),
|
|
modified: new Date().toISOString(),
|
|
modified_by: 'system',
|
|
owner: 'system',
|
|
docstatus: 0
|
|
},
|
|
{
|
|
name: 'USER003',
|
|
full_name: 'Ahmed Al-Rashid',
|
|
email: 'ahmed.alrashid@seeraarabia.com',
|
|
creation: new Date(Date.now() - 172800000).toISOString(),
|
|
modified: new Date().toISOString(),
|
|
modified_by: 'system',
|
|
owner: 'system',
|
|
docstatus: 0
|
|
},
|
|
{
|
|
name: 'USER004',
|
|
full_name: 'Sarah Johnson',
|
|
email: 'sarah.johnson@seeraarabia.com',
|
|
creation: new Date(Date.now() - 259200000).toISOString(),
|
|
modified: new Date().toISOString(),
|
|
modified_by: 'system',
|
|
owner: 'system',
|
|
docstatus: 0
|
|
},
|
|
{
|
|
name: 'USER005',
|
|
full_name: 'Mohammed Hassan',
|
|
email: 'mohammed.hassan@seeraarabia.com',
|
|
creation: new Date(Date.now() - 345600000).toISOString(),
|
|
modified: new Date().toISOString(),
|
|
modified_by: 'system',
|
|
owner: 'system',
|
|
docstatus: 0
|
|
}
|
|
];
|
|
|
|
setRecentRecords(demoRecords);
|
|
}, [userDetails]);
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await logout();
|
|
localStorage.removeItem('user');
|
|
navigate('/login');
|
|
} catch (err) {
|
|
console.error('Logout error:', err);
|
|
// Force logout even if API call fails
|
|
localStorage.removeItem('user');
|
|
navigate('/login');
|
|
}
|
|
};
|
|
|
|
if (statsLoading || userLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-indigo-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
{/* Header */}
|
|
<header className="bg-white dark:bg-gray-800 shadow">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center py-6">
|
|
<div className="flex items-center">
|
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">
|
|
Welcome, {user?.full_name || 'User'}
|
|
</span>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
{(statsError || userError) && (
|
|
<div className="mb-6 rounded-md bg-red-50 dark:bg-red-900/20 p-4">
|
|
<div className="text-sm text-red-700 dark:text-red-400">
|
|
{statsError || userError || 'Failed to load dashboard data'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats Cards (from Frappe Number Cards) */}
|
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 mb-8">
|
|
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
|
<div className="p-5">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-indigo-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Total Assets</dt>
|
|
<dd className="text-lg font-medium text-gray-900 dark:text-white">
|
|
{numberCards?.total_assets ?? '-'}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
|
<div className="p-5">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Open Work Orders</dt>
|
|
<dd className="text-lg font-medium text-gray-900 dark:text-white">
|
|
{numberCards?.work_orders_open ?? '-'}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
|
<div className="p-5">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-yellow-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">In Progress</dt>
|
|
<dd className="text-lg font-medium text-gray-900 dark:text-white">
|
|
{numberCards?.work_orders_in_progress ?? '-'}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
|
<div className="p-5">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Completed Work Orders</dt>
|
|
<dd className="text-lg font-medium text-gray-900 dark:text-white">
|
|
{numberCards?.work_orders_completed ?? '-'}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Charts Grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
|
|
{[
|
|
'Up & Down Time Chart',
|
|
'Work Order Status Chart',
|
|
'Maintenance - Asset wise Count',
|
|
'Asset Maintenance Assignees Status Count',
|
|
'Asset Maintenance Frequency Chart',
|
|
'PPM Status',
|
|
'PPM Template Counts',
|
|
'Repair Cost',
|
|
].map((name) => (
|
|
<ChartTile key={name} chartName={name} />
|
|
))}
|
|
</div>
|
|
|
|
{/* Recent Records */}
|
|
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-md">
|
|
<div className="px-4 py-5 sm:px-6">
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
|
Recent Records
|
|
</h3>
|
|
<p className="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">
|
|
Latest entries from your Frappe backend
|
|
</p>
|
|
</div>
|
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{recentRecords.map((record) => (
|
|
<li key={record.name}>
|
|
<div className="px-4 py-4 flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
<div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
|
|
<span className="text-sm font-medium text-indigo-600">
|
|
{record.full_name?.charAt(0) || record.name.charAt(0)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{record.full_name || record.name}
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
{record.email || 'No email'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
{new Date(record.creation).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="mt-8">
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-4">
|
|
Quick Actions
|
|
</h3>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
<button
|
|
onClick={() => navigate('/users')}
|
|
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow hover:shadow-md transition-shadow"
|
|
>
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">View Users</h4>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Manage user accounts</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => navigate('/settings')}
|
|
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow hover:shadow-md transition-shadow"
|
|
>
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-gray-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Settings</h4>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Configure your preferences</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => navigate('/events')}
|
|
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow hover:shadow-md transition-shadow"
|
|
>
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Events</h4>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">View calendar events</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => navigate('/reports')}
|
|
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow hover:shadow-md transition-shadow"
|
|
>
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Reports</h4>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">View analytics and reports</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* API Test Component */}
|
|
<div className="mt-8">
|
|
<ApiTest />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard;
|