GTARH_Fonctionnel_V1
This commit is contained in:
563
src/pages/Dashboard.tsx
Normal file
563
src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,563 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user