Files
GTARH/src/pages/Dashboard.tsx
2025-12-02 17:57:33 +01:00

563 lines
23 KiB
TypeScript

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Calendar, Users, Clock, CheckCircle, XCircle, AlertCircle,
Plus, LogOut, FileSpreadsheet, RefreshCw, Edit, ClipboardList, FileText
} from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { useSSE } from "@/hooks/useSSE";
import { PublicClientApplication } from "@azure/msal-browser";
// Configuration MSAL
const msalConfig = {
auth: {
clientId: "4bb4cc24-bac3-427c-b02c-5d14fc67b561",
authority: "https://login.microsoftonline.com/9840a2a0-6ae1-4688-b03d-d2ec291be0f9",
redirectUri: "https://mygta-rh.ensup-adm.net",
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
},
};
interface Stats {
enAttente: number;
valideeCeMois: number;
nombreEquipes: number;
}
interface Demande {
Id: number;
nomEmploye: string;
typesConge: string;
DateDebut: string;
Statut: string;
}
const Dashboard = () => {
const navigate = useNavigate();
const [stats, setStats] = useState<Stats>({ enAttente: 0, valideeCeMois: 0, nombreEquipes: 0 });
const [recentRequests, setRecentRequests] = useState<Demande[]>([]);
const [user, setUser] = useState<any>(null);
const [token, setToken] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
// ✅ Chargement initial de l'utilisateur
useEffect(() => {
const userData = localStorage.getItem('user');
const userToken = localStorage.getItem('token');
if (!userData || !userToken) {
toast.error("Session expirée, reconnexion nécessaire");
navigate('/');
return;
}
try {
const parsedUser = JSON.parse(userData);
setUser(parsedUser);
setToken(userToken);
} catch (error) {
console.error('Erreur parsing user:', error);
toast.error("Erreur lors de la récupération des données utilisateur");
navigate('/');
}
}, [navigate]);
// ✅ Fonction de chargement des données
const chargerDonnees = async () => {
if (!token) {
console.log('⏳ Pas de token, attente...');
return;
}
console.log('📥 Chargement des données du dashboard...');
setIsLoading(true);
try {
// Charger les stats
const respStats = await fetch('/api/stats', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!respStats.ok) {
throw new Error(`Erreur stats: ${respStats.status}`);
}
const dataStats = await respStats.json();
console.log('📊 Stats reçues:', dataStats);
setStats(dataStats);
// Charger les demandes récentes
const respDemandes = await fetch('/api/demandes?limit=5', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!respDemandes.ok) {
throw new Error(`Erreur demandes: ${respDemandes.status}`);
}
const dataDemandes = await respDemandes.json();
console.log('📋 Demandes reçues:', dataDemandes.length);
setRecentRequests(dataDemandes);
} catch (error: any) {
console.error("❌ Erreur chargement données:", error);
toast.error("Erreur lors du chargement des données: " + error.message);
} finally {
setIsLoading(false);
}
};
// ✅ Charger les données quand le token est disponible
useEffect(() => {
if (token) {
chargerDonnees();
}
}, [token]);
// 🔔 CONNEXION SSE
useSSE((event) => {
console.log('🔔 Événement SSE reçu dans Dashboard:', event.type);
switch (event.type) {
case 'connected':
console.log('✅ Dashboard connecté au serveur temps réel');
toast.success("Connexion temps réel établie", { duration: 2000 });
break;
case 'demande-created':
console.log('🆕 Nouvelle demande créée');
toast.info("Nouvelle demande de congé", { duration: 3000 });
if (token) chargerDonnees();
break;
case 'demande-updated':
console.log('🔄 Demande mise à jour');
if (token) chargerDonnees();
break;
case 'demande-validated':
console.log('✅ Demande validée/refusée');
toast.info("Une demande a été traitée", { duration: 3000 });
if (token) chargerDonnees();
break;
// 🆕 GESTION DES ANNULATIONS
case 'demande-cancelled':
console.log('🚫 Demande annulée');
toast.warning("Une demande a été annulée", {
description: "La liste a été mise à jour",
duration: 4000
});
if (token) chargerDonnees();
break;
case 'demande-deleted':
console.log('🗑️ Demande supprimée');
if (token) chargerDonnees();
break;
case 'demande-list-updated':
console.log('📋 Liste des demandes mise à jour');
if (token) chargerDonnees();
break;
case 'heartbeat':
// Heartbeat pour garder la connexion
break;
default:
console.log('❓ Type d\'événement inconnu:', event.type);
}
});
const handleLogout = async () => {
try {
const msalInstance = new PublicClientApplication(msalConfig);
await msalInstance.initialize();
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
// Déconnexion popup au lieu de redirect
await msalInstance.logoutPopup({
account: accounts[0],
mainWindowRedirectUri: "https://mygta-rh.ensup-adm.net"
});
}
// Nettoyer tout le stockage
localStorage.removeItem('token');
localStorage.removeItem('user');
sessionStorage.clear();
toast.success("Déconnexion réussie");
navigate("/");
} catch (error) {
console.error("Erreur déconnexion:", error);
// Forcer la déconnexion locale même en cas d'erreur
localStorage.removeItem('token');
localStorage.removeItem('user');
sessionStorage.clear();
toast.success("Déconnexion réussie");
navigate("/");
}
};
// ✅ FONCTION MISE À JOUR AVEC LE STATUT "ANNULÉE"
const getStatusBadge = (status: string) => {
switch (status) {
case "En attente":
return <Badge variant="outline" className="bg-warning/10 text-warning border-warning/20">
<Clock className="w-3 h-3 mr-1" />
En attente
</Badge>;
case "Validée":
return <Badge variant="outline" className="bg-success/10 text-success border-success/20">
<CheckCircle className="w-3 h-3 mr-1" />
Validée
</Badge>;
case "Refusée":
return <Badge variant="outline" className="bg-destructive/10 text-destructive border-destructive/20">
<XCircle className="w-3 h-3 mr-1" />
Refusée
</Badge>;
case "Annulée":
return <Badge variant="outline" className="bg-gray-100 text-gray-700 border-gray-300 dark:bg-gray-800 dark:text-gray-300">
<AlertCircle className="w-3 h-3 mr-1" />
Annulée
</Badge>;
default:
return <Badge variant="outline" className="bg-gray-100 text-gray-700">
{status}
</Badge>;
}
};
const formatDate = (dateString: string) => {
if (!dateString) return '';
try {
return new Date(dateString).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
} catch (error) {
console.error('Erreur formatage date:', error);
return dateString;
}
};
const statsCards = [
{
title: "Demandes en attente",
value: stats.enAttente,
icon: Clock,
color: "text-warning",
bg: "bg-warning/10",
action: () => navigate("/validation")
},
{
title: "Validées ce mois",
value: stats.valideeCeMois,
icon: CheckCircle,
color: "text-success",
bg: "bg-success/10",
action: () => navigate("/validation")
},
{
title: "Total équipes",
value: stats.nombreEquipes,
icon: Users,
color: "text-primary",
bg: "bg-primary/10",
action: () => navigate("/teams")
},
];
const actionCards = [
{
title: "Saisie manuelle",
description: "Enregistrer une demande",
icon: Plus,
color: "bg-[#7e5aa2]",
action: () => navigate("/saisie-manuelle")
},
{
title: "Validation",
description: "Valider les demandes",
icon: CheckCircle,
color: "bg-[#7e5aa2]", // Mauve GTARH comme les autres
action: () => navigate("/validation")
},
{
title: "Récupération",
description: "Gérer les samedis travaillés",
icon: Clock,
color: "bg-purple-600",
action: () => navigate("/gestion-recuperation")
},
{
title: "Export paie",
description: "Générer un rapport",
icon: FileSpreadsheet,
color: "bg-blue-600",
action: () => navigate("/export-paie")
},
{
title: "Compteurs",
description: "Gérer les soldes",
icon: RefreshCw,
color: "bg-purple-600",
action: () => navigate("/compteurs")
},
{
title: "Équipes",
description: "Voir toutes les équipes",
icon: Users,
color: "bg-indigo-600",
action: () => navigate("/teams")
},
{
title: "Historique",
description: "Consulter l'historique",
icon: ClipboardList,
color: "bg-orange-600",
action: () => navigate("/historique")
},
{
title: "Comptes-Rendus RH",
description: "Gérer les forfaits jour",
icon: FileText,
color: "bg-cyan-600",
action: () => navigate("/compte-rendu-rh")
}
];
const handleDeleteRequest = async (id: number) => {
if (!confirm("Voulez-vous vraiment supprimer cette demande ?")) return;
if (!token) {
toast.error("Utilisateur non authentifié");
return;
}
try {
console.log('🗑️ Suppression demande:', id);
const resp = await fetch(`/api/demandes/${id}`, {
method: "DELETE",
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
const data = await resp.json();
if (resp.ok) {
toast.success("Demande supprimée avec succès");
// Le SSE rechargera automatiquement, mais on peut forcer un rechargement immédiat
await chargerDonnees();
} else {
toast.error(data.error || "Erreur lors de la suppression");
}
} catch (error: any) {
console.error("❌ Erreur suppression:", error);
toast.error("Erreur lors de la suppression: " + error.message);
}
};
// ✅ Affichage du loader pendant le chargement initial
if (isLoading && !user) {
return (
<div className="min-h-screen bg-gradient-subtle flex items-center justify-center">
<div className="text-center">
<RefreshCw className="w-12 h-12 animate-spin text-[#7e5aa2] mx-auto mb-4" />
<p className="text-gray-600">Chargement du tableau de bord...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-subtle">
<header className="bg-card border-b border-border shadow-card">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}>
<Calendar className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">GTARH</h1>
{user && (
<p className="text-sm text-muted-foreground">
{user.prenom} {user.nom} - {user.role}
</p>
)}
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleLogout}
className="transition-smooth"
>
<LogOut className="w-4 h-4 mr-2" />
Déconnexion
</Button>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{statsCards.map((stat, index) => (
<Card
key={index}
className="shadow-card border-0 bg-gradient-card transition-smooth hover:shadow-elegant cursor-pointer"
onClick={stat.action}
>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground mb-1">{stat.title}</p>
<p className="text-3xl font-bold text-foreground">{stat.value}</p>
</div>
<div className={`w-12 h-12 rounded-xl ${stat.bg} flex items-center justify-center`}>
<stat.icon className={`w-6 h-6 ${stat.color}`} />
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Action Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{actionCards.map((action, index) => (
<Card
key={index}
className="shadow-card border-0 hover:shadow-elegant transition-smooth cursor-pointer group"
onClick={action.action}
>
<CardContent className="p-6">
<div className="flex items-start gap-4">
<div className={`w-12 h-12 rounded-xl ${action.color} flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform`}>
<action.icon className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg mb-1">{action.title}</h3>
<p className="text-sm text-muted-foreground">{action.description}</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Recent Requests */}
<Card className="shadow-card border-0">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Demandes récentes</CardTitle>
<CardDescription>Les dernières demandes soumises</CardDescription>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => chargerDonnees()}
disabled={isLoading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
Actualiser
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate("/validation")}
>
Voir tout
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{isLoading && recentRequests.length === 0 ? (
<div className="text-center py-12">
<RefreshCw className="w-12 h-12 animate-spin text-[#7e5aa2] mx-auto mb-4" />
<p className="text-muted-foreground">Chargement des demandes...</p>
</div>
) : recentRequests.length > 0 ? (
<div className="space-y-4">
{recentRequests.map((request) => (
<div
key={request.Id}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-gradient-card hover:shadow-card transition-smooth cursor-pointer group"
onClick={() => navigate(`/modification-demande/${request.Id}`)}
>
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<p className="font-medium text-foreground">{request.nomEmploye}</p>
<Badge variant="secondary" className="text-xs">{request.typesConge}</Badge>
</div>
<p className="text-sm text-muted-foreground">
À partir du {formatDate(request.DateDebut)}
</p>
</div>
<div className="flex items-center gap-2">
{getStatusBadge(request.Statut)}
{/* Bouton Edit */}
<Button
variant="ghost"
size="sm"
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
navigate(`/modification-demande/${request.Id}`);
}}
>
<Edit className="w-4 h-4" />
</Button>
{/* Bouton Supprimer */}
<Button
variant="destructive"
size="sm"
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
handleDeleteRequest(request.Id);
}}
>
<XCircle className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12">
<AlertCircle className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
<p className="text-muted-foreground text-lg">Aucune demande récente</p>
</div>
)}
</CardContent>
</Card>
</main>
</div>
);
};
export default Dashboard;