Commi_GTFRH

This commit is contained in:
2025-09-15 15:52:15 +02:00
parent 12962a4081
commit 83e2b786c7
17 changed files with 5315 additions and 0 deletions

View File

@@ -0,0 +1,525 @@
import React, { useState } from 'react';
import { Users, Download, Calendar, Clock, FileText, Filter } from 'lucide-react';
interface TimeEntry {
id: string;
formateur: string;
campus: string;
date: string;
type: 'preparation' | 'correction';
hours: number;
description: string;
status: 'pending' | 'approved' | 'rejected';
}
const RHDashboard: React.FC = () => {
const [selectedCampus, setSelectedCampus] = useState<string>('all');
const [selectedMonth, setSelectedMonth] = useState<string>('2025-01');
const [selectedFormateur, setSelectedFormateur] = useState<string>('all');
const [showFormateursList, setShowFormateursList] = useState(false);
const campuses = ['Cergy', 'Nantes', 'SQY', 'Marseille'];
// Liste complète des formateurs par campus
const formateursByCampus = {
'Cergy': [
'Jean Dupont', 'Marie Dubois', 'Pierre Martin', 'Sophie Leroy',
'Antoine Bernard', 'Claire Moreau', 'Nicolas Petit', 'Isabelle Roux'
],
'Nantes': [
'Marie Martin', 'Thomas Durand', 'Julie Blanc', 'François Simon',
'Camille Garnier', 'Olivier Faure', 'Nathalie Girard', 'Julien Morel'
],
'SQY': [
'Pierre Durand', 'Sandrine Lefebvre', 'Maxime Rousseau', 'Émilie Mercier',
'Sébastien Fournier', 'Valérie Bonnet', 'Christophe Lambert', 'Aurélie Muller'
],
'Marseille': [
'Sophie Leroy', 'David Fontaine', 'Laure Chevalier', 'Romain Gauthier',
'Céline Masson', 'Fabrice Dupuis', 'Stéphanie Roy', 'Alexandre Perrin'
]
};
// Données d'exemple
const timeEntries: TimeEntry[] = [
{
id: '1',
formateur: 'Jean Dupont',
campus: 'Cergy',
date: '2025-01-15',
type: 'preparation',
hours: 4,
description: 'Préparation cours React avancé',
status: 'approved'
},
{
id: '2',
formateur: 'Marie Martin',
campus: 'Nantes',
date: '2025-01-14',
type: 'correction',
hours: 6,
description: 'Correction examens PHP',
status: 'pending'
},
{
id: '3',
formateur: 'Pierre Durand',
campus: 'SQY',
date: '2025-01-13',
type: 'preparation',
hours: 3.5,
description: 'Recherche documentation Node.js',
status: 'approved'
},
{
id: '4',
formateur: 'Sophie Leroy',
campus: 'Marseille',
date: '2025-01-12',
type: 'correction',
hours: 5,
description: 'Correction projets JavaScript',
status: 'approved'
},
{
id: '5',
formateur: 'Jean Dupont',
campus: 'Cergy',
date: '2025-01-11',
type: 'preparation',
hours: 2,
description: 'Préparation TP bases de données',
status: 'pending'
},
{
id: '6',
formateur: 'Marie Martin',
campus: 'Nantes',
date: '2025-01-10',
type: 'preparation',
hours: 4.5,
description: 'Préparation cours architecture logicielle',
status: 'approved'
},
{
id: '7',
formateur: 'Thomas Durand',
campus: 'Nantes',
date: '2025-01-09',
type: 'preparation',
hours: 3,
description: 'Préparation cours DevOps',
status: 'approved'
},
{
id: '8',
formateur: 'Sandrine Lefebvre',
campus: 'SQY',
date: '2025-01-08',
type: 'correction',
hours: 4,
description: 'Correction projets Angular',
status: 'pending'
},
{
id: '9',
formateur: 'David Fontaine',
campus: 'Marseille',
date: '2025-01-07',
type: 'preparation',
hours: 2.5,
description: 'Recherche nouvelles technologies',
status: 'approved'
}
];
// Filtrage des données
const filteredEntries = timeEntries.filter(entry => {
const campusMatch = selectedCampus === 'all' || entry.campus === selectedCampus;
const formateurMatch = selectedFormateur === 'all' || entry.formateur === selectedFormateur;
const monthMatch = entry.date.startsWith(selectedMonth);
return campusMatch && formateurMatch && monthMatch;
});
// Liste des formateurs uniques
const allFormateurs = Object.values(formateursByCampus).flat();
const formateurs = Array.from(new Set(allFormateurs));
// Statistiques par campus
const getStatsForCampus = (campus: string) => {
const campusEntries = timeEntries.filter(entry => entry.campus === campus);
const totalHours = campusEntries.reduce((sum, entry) => sum + entry.hours, 0);
const preparationHours = campusEntries
.filter(entry => entry.type === 'preparation')
.reduce((sum, entry) => sum + entry.hours, 0);
const correctionHours = campusEntries
.filter(entry => entry.type === 'correction')
.reduce((sum, entry) => sum + entry.hours, 0);
return { totalHours, preparationHours, correctionHours };
};
const handleExport = () => {
const csvHeaders = ['Date', 'Formateur', 'Campus', 'Type', 'Heures', 'Description', 'Statut'];
const csvData = filteredEntries.map(entry => [
new Date(entry.date).toLocaleDateString('fr-FR'),
entry.formateur,
entry.campus,
entry.type === 'preparation' ? 'Préparation' : 'Correction',
entry.hours.toString(),
entry.description,
entry.status === 'approved' ? 'Approuvé' : entry.status === 'pending' ? 'En attente' : 'Rejeté'
]);
const csvContent = [csvHeaders, ...csvData]
.map(row => row.map(cell => `"${cell}"`).join(','))
.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `declarations_heures_${selectedMonth}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long'
});
};
const getStatusBadge = (status: string) => {
const statusConfig = {
approved: { bg: 'bg-green-100', text: 'text-green-800', label: 'Approuvé' },
pending: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'En attente' },
rejected: { bg: 'bg-red-100', text: 'text-red-800', label: 'Rejeté' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
{config.label}
</span>
);
};
const handleStatusChange = (entryId: string, newStatus: 'approved' | 'rejected') => {
console.log(`Changement de statut pour l'entrée ${entryId}: ${newStatus}`);
};
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-800 flex items-center gap-3">
<Users className="text-blue-600" />
Vue RH - GTF
</h1>
<p className="text-gray-600 mt-2">
Gestion et suivi des déclarations
</p>
</div>
{/* Filtres */}
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
<div className="flex items-center gap-2 mb-4">
<Filter className="text-gray-600" size={20} />
<h2 className="text-lg font-semibold text-gray-800">Filtres</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Campus
</label>
<select
value={selectedCampus}
onChange={(e) => setSelectedCampus(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tous les campus</option>
{campuses.map(campus => (
<option key={campus} value={campus}>{campus}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Formateur
</label>
<select
value={selectedFormateur}
onChange={(e) => setSelectedFormateur(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tous les formateurs</option>
{formateurs.map(formateur => (
<option key={formateur} value={formateur}>{formateur}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Mois
</label>
<input
type="month"
value={selectedMonth}
onChange={(e) => setSelectedMonth(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex items-end">
<button
onClick={handleExport}
className="w-full bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 transition-colors"
>
<Download size={20} />
Exporter CSV
</button>
</div>
</div>
</div>
{/* Statistiques par campus */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{campuses.map(campus => {
const stats = getStatsForCampus(campus);
const campusFormateurs = formateursByCampus[campus as keyof typeof formateursByCampus];
return (
<div key={campus} className="bg-white rounded-xl shadow-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-800">{campus}</h3>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">{campusFormateurs.length} formateurs</span>
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Total:</span>
<span className="font-semibold text-gray-800">{stats.totalHours}h</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-blue-600">Préparation:</span>
<span className="font-medium text-blue-600">{stats.preparationHours}h</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-orange-600">Correction:</span>
<span className="font-medium text-orange-600">{stats.correctionHours}h</span>
</div>
</div>
<button
onClick={() => setShowFormateursList(!showFormateursList)}
className="mt-3 text-xs text-blue-600 hover:text-blue-800 underline"
>
Voir les formateurs
</button>
</div>
);
})}
</div>
{/* Liste des formateurs par campus */}
{showFormateursList && (
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Users className="text-blue-600" />
Formateurs par campus
</h3>
<button
onClick={() => setShowFormateursList(false)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{campuses.map(campus => {
const campusFormateurs = formateursByCampus[campus as keyof typeof formateursByCampus];
return (
<div key={campus} className="border border-gray-200 rounded-lg p-4">
<h4 className="font-semibold text-gray-800 mb-3 flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
{campus}
</h4>
<div className="space-y-2">
{campusFormateurs.map(formateur => {
const formateurEntries = timeEntries.filter(entry =>
entry.formateur === formateur && entry.campus === campus
);
const totalHours = formateurEntries.reduce((sum, entry) => sum + entry.hours, 0);
return (
<div key={formateur} className="flex justify-between items-center py-1">
<span className="text-sm text-gray-700">{formateur}</span>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">{totalHours}h</span>
<div className={`w-2 h-2 rounded-full ${
totalHours > 0 ? 'bg-green-400' : 'bg-gray-300'
}`}></div>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Résumé des résultats */}
<div className="bg-white rounded-xl shadow-lg p-6 mb-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-800">
Résultats filtrés
</h3>
<p className="text-gray-600">
{filteredEntries.length} déclaration(s) trouvée(s)
</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-blue-600">
{filteredEntries.reduce((sum, entry) => sum + entry.hours, 0)}h
</p>
<p className="text-sm text-gray-600">Total des heures</p>
</div>
</div>
</div>
{/* Tableau des déclarations */}
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
<div className="p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Calendar className="text-blue-600" />
Déclarations d'heures
</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Formateur
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Campus
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Heures
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Statut
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredEntries.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">
Aucune déclaration trouvée pour les critères sélectionnés
</td>
</tr>
) : (
filteredEntries.map((entry) => (
<tr key={entry.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatDate(entry.date)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{entry.formateur}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{entry.campus}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
entry.type === 'preparation'
? 'bg-blue-100 text-blue-800'
: 'bg-orange-100 text-orange-800'
}`}>
<div className="flex items-center gap-1">
{entry.type === 'preparation' ? (
<FileText size={12} />
) : (
<Clock size={12} />
)}
{entry.type === 'preparation' ? 'Préparation' : 'Correction'}
</div>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">
{entry.hours}h
</td>
<td className="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">
{entry.description}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(entry.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{entry.status === 'pending' ? (
<div className="flex gap-2">
<button
onClick={() => handleStatusChange(entry.id, 'approved')}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-xs transition-colors"
>
Approuver
</button>
<button
onClick={() => handleStatusChange(entry.id, 'rejected')}
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-xs transition-colors"
>
Rejeter
</button>
</div>
) : (
<span className="text-gray-400 text-xs">
{entry.status === 'approved' ? 'Approuvé' : 'Rejeté'}
</span>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
export default RHDashboard;