Modif_GestionDoublonsGTARH_Fonctionnel_V1
This commit is contained in:
@@ -0,0 +1,887 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Calendar, Lock, Unlock, Download, Menu, AlertCircle,
|
||||
ChevronLeft, ChevronRight, FileText, RefreshCw,
|
||||
Eye, Search, Users, CheckCircle, XCircle, Clock, LogOut, ArrowLeft, Filter
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
|
||||
const CompteRenduRH = () => {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [token, setToken] = useState<string>('');
|
||||
|
||||
// États
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [viewMode, setViewMode] = useState('mois');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [collaborateurs, setCollaborateurs] = useState<any[]>([]);
|
||||
const [selectedCollab, setSelectedCollab] = useState<any>(null);
|
||||
const [compteRenduData, setCompteRenduData] = useState<any[]>([]);
|
||||
const [mensuelData, setMensuelData] = useState<any>(null);
|
||||
const [statsGlobales, setStatsGlobales] = useState<any>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [selectedJour, setSelectedJour] = useState<any>(null);
|
||||
|
||||
// 🆕 NOUVEAUX ÉTATS POUR LE FILTRE
|
||||
const [services, setServices] = useState<any[]>([]);
|
||||
const [selectedService, setSelectedService] = useState<string>('all');
|
||||
|
||||
const annee = currentDate.getFullYear();
|
||||
const mois = currentDate.getMonth() + 1;
|
||||
|
||||
// ✅ ÉTAPE 1 : Charger l'utilisateur et vérifier l'accès
|
||||
useEffect(() => {
|
||||
const userData = localStorage.getItem('user');
|
||||
const userToken = localStorage.getItem('token');
|
||||
|
||||
if (!userData || !userToken) {
|
||||
toast.error("Session expirée, reconnexion nécessaire");
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUser = JSON.parse(userData);
|
||||
|
||||
if (!['RH', 'Admin'].includes(parsedUser?.role)) {
|
||||
toast.error("Accès réservé aux RH et Administrateurs");
|
||||
navigate('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(parsedUser);
|
||||
setToken(userToken);
|
||||
}, [navigate]);
|
||||
|
||||
// 🆕 ÉTAPE 1.5 : Charger la liste des services
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/compte-rendu/services', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('📋 Services disponibles:', data);
|
||||
setServices(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur chargement services:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadServices();
|
||||
}, [token]);
|
||||
|
||||
// ✅ ÉTAPE 2 : Charger les collaborateurs (avec filtre service côté client)
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
|
||||
const loadCollaborateurs = async () => {
|
||||
console.log('📥 Chargement des collaborateurs...');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// ✅ Charger TOUS les employés
|
||||
const response = await fetch('/api/employes', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📊 Réponse API employes:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('❌ Erreur API:', errorText);
|
||||
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('📋 Collaborateurs reçus:', data.length);
|
||||
|
||||
// ✅ Filtrer les forfaits jour actifs
|
||||
let forfaitJour = data.filter((emp: any) => {
|
||||
const isForfaitJour = emp.TypeContrat === 'forfait_jour';
|
||||
const isActif = emp.Actif === 1 || emp.Actif === true || emp.Actif === null;
|
||||
return isForfaitJour && isActif;
|
||||
});
|
||||
|
||||
// 🆕 APPLIQUER LE FILTRE SERVICE CÔTÉ CLIENT
|
||||
if (selectedService && selectedService !== 'all') {
|
||||
forfaitJour = forfaitJour.filter((emp: any) =>
|
||||
emp.ServiceId?.toString() === selectedService.toString() ||
|
||||
emp.serviceId?.toString() === selectedService.toString()
|
||||
);
|
||||
console.log(`🔍 Filtré par service ${selectedService}: ${forfaitJour.length} collaborateurs`);
|
||||
}
|
||||
|
||||
console.log('✅ Forfaits jour actifs:', forfaitJour.length);
|
||||
setCollaborateurs(forfaitJour);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur chargement collaborateurs:', error);
|
||||
toast.error('Erreur lors du chargement des collaborateurs: ' + error.message);
|
||||
setCollaborateurs([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadCollaborateurs();
|
||||
}, [token, selectedService]); // 🆕 Recharger quand le service change
|
||||
|
||||
// ✅ ÉTAPE 3 : Charger les stats globales
|
||||
useEffect(() => {
|
||||
if (!token || collaborateurs.length === 0) return;
|
||||
|
||||
const loadStatsGlobales = async () => {
|
||||
console.log('📊 Chargement stats globales...');
|
||||
|
||||
try {
|
||||
let url = `/api/compte-rendu/stats?annee=${annee}&mois=${mois}`;
|
||||
if (selectedService && selectedService !== 'all') {
|
||||
url += `&service_id=${selectedService}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('✅ Stats globales:', data);
|
||||
setStatsGlobales(data);
|
||||
} else {
|
||||
console.warn('⚠️ Stats non disponibles, utilisation valeurs par défaut');
|
||||
setStatsGlobales({
|
||||
totalCollaborateurs: collaborateurs.length,
|
||||
mensuelsValides: 0,
|
||||
mensuelsEnAttente: collaborateurs.length
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur stats globales:', error);
|
||||
setStatsGlobales({
|
||||
totalCollaborateurs: collaborateurs.length,
|
||||
mensuelsValides: 0,
|
||||
mensuelsEnAttente: collaborateurs.length
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadStatsGlobales();
|
||||
}, [token, collaborateurs.length, annee, mois, selectedService]);
|
||||
|
||||
// ✅ ÉTAPE 4 : Charger le compte-rendu quand on sélectionne un collaborateur
|
||||
useEffect(() => {
|
||||
if (!selectedCollab || !token) return;
|
||||
|
||||
const loadCompteRendu = async () => {
|
||||
setIsLoading(true);
|
||||
console.log(`📥 Chargement compte-rendu pour collaborateur ${selectedCollab.id}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/compte-rendu-activites?user_id=${selectedCollab.id}&annee=${annee}&mois=${mois}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setCompteRenduData(data.jours || []);
|
||||
setMensuelData(data.mensuel);
|
||||
console.log('✅ Compte-rendu chargé:', data.jours?.length, 'jours');
|
||||
} else {
|
||||
toast.error(data.message || 'Erreur lors du chargement');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur chargement compte-rendu:', error);
|
||||
toast.error('Erreur lors du chargement du compte-rendu');
|
||||
setCompteRenduData([]);
|
||||
setMensuelData(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadCompteRendu();
|
||||
}, [selectedCollab, annee, mois, token]);
|
||||
|
||||
// Verrouiller/Déverrouiller un mois
|
||||
const handleToggleLock = async (unlock = false) => {
|
||||
if (!selectedCollab || !token) return;
|
||||
|
||||
const action = unlock ? 'déverrouiller' : 'verrouiller';
|
||||
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
|
||||
if (!confirm(`Confirmer ${action} le mois de ${monthNames[mois - 1]} ${annee} pour ${selectedCollab.nom} ?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = unlock
|
||||
? '/api/compte-rendu/deverrouiller'
|
||||
: '/api/compte-rendu/verrouiller';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collaborateur_id: selectedCollab.id,
|
||||
annee: annee,
|
||||
mois: mois
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
toast.success(`Mois ${action === 'verrouiller' ? 'verrouillé' : 'déverrouillé'} avec succès`);
|
||||
|
||||
// Recharger les données
|
||||
const reloadResponse = await fetch(
|
||||
`/api/compte-rendu-activites?user_id=${selectedCollab.id}&annee=${annee}&mois=${mois}`,
|
||||
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||
);
|
||||
const reloadData = await reloadResponse.json();
|
||||
if (reloadData.success) {
|
||||
setCompteRenduData(reloadData.jours || []);
|
||||
setMensuelData(reloadData.mensuel);
|
||||
}
|
||||
|
||||
// Recharger les stats
|
||||
let statsUrl = `/api/compte-rendu/stats?annee=${annee}&mois=${mois}`;
|
||||
if (selectedService && selectedService !== 'all') {
|
||||
statsUrl += `&service_id=${selectedService}`;
|
||||
}
|
||||
const statsResponse = await fetch(statsUrl, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (statsResponse.ok) {
|
||||
const statsData = await statsResponse.json();
|
||||
setStatsGlobales(statsData);
|
||||
}
|
||||
} else {
|
||||
toast.error(data.message || 'Erreur');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erreur ${action}:`, error);
|
||||
toast.error('Erreur serveur');
|
||||
}
|
||||
};
|
||||
|
||||
// Export PDF
|
||||
const handleExportPDF = async () => {
|
||||
if (!selectedCollab || !token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/export-compte-rendu-pdf?user_id=${selectedCollab.id}&annee=${annee}&mois=${mois}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de l\'export PDF');
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
console.log('📄 Données PDF:', data);
|
||||
toast.info(data.message || 'Export PDF en cours de développement');
|
||||
|
||||
const dataWindow = window.open('', '_blank');
|
||||
if (dataWindow) {
|
||||
dataWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Compte-rendu - ${selectedCollab.prenom} ${selectedCollab.nom}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||
h1 { color: #0ea5e9; }
|
||||
.info { background: #f0f9ff; padding: 15px; border-radius: 8px; margin: 10px 0; }
|
||||
.label { font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Compte-rendu d'activités</h1>
|
||||
<div class="info">
|
||||
<p><span class="label">Collaborateur:</span> ${data.data?.collaborateur?.prenom} ${data.data?.collaborateur?.nom}</p>
|
||||
<p><span class="label">Email:</span> ${data.data?.collaborateur?.email}</p>
|
||||
<p><span class="label">Service:</span> ${data.data?.collaborateur?.service || 'Non défini'}</p>
|
||||
<p><span class="label">Période:</span> ${data.data?.periode}</p>
|
||||
</div>
|
||||
<h2>Résumé</h2>
|
||||
<div class="info">
|
||||
<p><span class="label">Jours travaillés:</span> ${data.data?.mensuel?.NbJoursTravailles || 0}</p>
|
||||
<p><span class="label">Non-respect repos quotidien:</span> ${data.data?.mensuel?.NbJoursNonRespectsReposQuotidien || 0}</p>
|
||||
<p><span class="label">Non-respect repos hebdo:</span> ${data.data?.mensuel?.NbJoursNonRespectsReposHebdo || 0}</p>
|
||||
<p><span class="label">Statut:</span> ${data.data?.mensuel?.Verrouille ? '🔒 Verrouillé' : '🔓 Ouvert'}</p>
|
||||
</div>
|
||||
<p style="color: #6b7280; font-size: 12px; margin-top: 20px;">
|
||||
Export généré le ${data.data?.dateExport}<br>
|
||||
Note: La génération PDF sera implémentée prochainement
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `compte-rendu-${selectedCollab.nom}-${annee}-${String(mois).padStart(2, '0')}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
toast.success('PDF téléchargé avec succès');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ Erreur export PDF:', error);
|
||||
toast.error('Erreur lors de l\'export PDF: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Navigation mois
|
||||
const navigateMonth = (direction: string) => {
|
||||
setCurrentDate(prev => {
|
||||
const newDate = new Date(prev);
|
||||
if (direction === 'prev') {
|
||||
newDate.setMonth(prev.getMonth() - 1);
|
||||
} else {
|
||||
newDate.setMonth(prev.getMonth() + 1);
|
||||
}
|
||||
return newDate;
|
||||
});
|
||||
};
|
||||
|
||||
// Ouvrir le détail d'un jour
|
||||
const handleViewDetail = (jourData: any) => {
|
||||
setSelectedJour(jourData);
|
||||
setShowDetailModal(true);
|
||||
};
|
||||
|
||||
// 🆕 Gérer le changement de service
|
||||
const handleServiceChange = (serviceId: string) => {
|
||||
setSelectedService(serviceId);
|
||||
setSelectedCollab(null); // Réinitialiser le collaborateur sélectionné
|
||||
};
|
||||
|
||||
// Filtrer les collaborateurs par recherche
|
||||
const filteredCollaborateurs = collaborateurs.filter(collab =>
|
||||
collab.nom?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
collab.prenom?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
collab.email?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
return new Date(dateStr).toLocaleDateString('fr-FR');
|
||||
};
|
||||
|
||||
const isVerrouille = () => {
|
||||
return mensuelData?.Verrouille === true || mensuelData?.Verrouille === 1;
|
||||
};
|
||||
|
||||
// ✅ Affichage du loader pendant le chargement initial
|
||||
if (isLoading && collaborateurs.length === 0) {
|
||||
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-cyan-600 mx-auto mb-4" />
|
||||
<p className="text-gray-600">Chargement des collaborateurs...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-subtle">
|
||||
{/* Header */}
|
||||
<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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate('/dashboard')}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Retour
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Gestion des Comptes-Rendus d'Activités
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Suivi et validation des forfaits jour
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* 🆕 FILTRE PAR SERVICE */}
|
||||
<Card className="shadow-card mb-6">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Filter className="w-5 h-5 text-gray-600" />
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Filtrer par service
|
||||
</label>
|
||||
<select
|
||||
value={selectedService}
|
||||
onChange={(e) => handleServiceChange(e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">Tous les services</option>
|
||||
{services.map(service => (
|
||||
<option key={service.Id} value={service.Id.toString()}>
|
||||
{service.Nom} ({service.nombreForfaitJour} forfait{service.nombreForfaitJour > 1 ? 's' : ''} jour)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Stats globales */}
|
||||
{statsGlobales && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<Card className="shadow-card">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-blue-100 rounded-lg">
|
||||
<Users className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Forfaits jour
|
||||
{selectedService !== 'all' && (
|
||||
<span className="ml-1 text-xs text-blue-600">(filtré)</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{statsGlobales.totalCollaborateurs}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-card">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-green-100 rounded-lg">
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Mois validés</p>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{statsGlobales.mensuelsValides}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-card">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-orange-100 rounded-lg">
|
||||
<Clock className="w-6 h-6 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">En attente</p>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{statsGlobales.mensuelsEnAttente}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Liste des collaborateurs */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="shadow-card">
|
||||
<CardHeader className="bg-gradient-to-r from-cyan-500 to-blue-500 text-white">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2">
|
||||
<Users className="w-5 h-5" />
|
||||
Collaborateurs ({collaborateurs.length})
|
||||
{selectedService !== 'all' && (
|
||||
<span className="text-xs bg-white/20 px-2 py-1 rounded">
|
||||
Filtré
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
</CardHeader>
|
||||
|
||||
{/* Recherche */}
|
||||
<CardContent className="p-4 border-b">
|
||||
<div className="relative">
|
||||
<Search className="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Liste */}
|
||||
<div className="overflow-y-auto max-h-[600px]">
|
||||
{filteredCollaborateurs.length === 0 ? (
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
{searchTerm ? 'Aucun résultat' : 'Aucun collaborateur en forfait jour'}
|
||||
</div>
|
||||
) : (
|
||||
filteredCollaborateurs.map(collab => (
|
||||
<div
|
||||
key={collab.id}
|
||||
onClick={() => setSelectedCollab(collab)}
|
||||
className={`
|
||||
p-4 border-b cursor-pointer transition-all
|
||||
hover:bg-gray-50
|
||||
${selectedCollab?.id === collab.id ? 'bg-blue-50 border-l-4 border-blue-500' : ''}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-foreground">
|
||||
{collab.prenom} {collab.nom}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{collab.email}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{collab.service || 'Service non défini'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Détails du compte-rendu */}
|
||||
<div className="lg:col-span-2">
|
||||
{!selectedCollab ? (
|
||||
<Card className="shadow-card p-8 text-center">
|
||||
<Users className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">
|
||||
Sélectionnez un collaborateur pour voir ses comptes-rendus
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* En-tête collaborateur */}
|
||||
<Card className="shadow-card">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground">
|
||||
{selectedCollab.prenom} {selectedCollab.nom}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selectedCollab.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isVerrouille() ? (
|
||||
<span className="flex items-center gap-2 px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-sm font-medium">
|
||||
<Lock className="w-4 h-4" />
|
||||
Verrouillé
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2 px-3 py-1.5 bg-green-100 text-green-700 rounded-lg text-sm font-medium">
|
||||
<Unlock className="w-4 h-4" />
|
||||
Ouvert
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation mois */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<button
|
||||
onClick={() => navigateMonth('prev')}
|
||||
className="p-2 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-blue-600" />
|
||||
</button>
|
||||
|
||||
<h3 className="text-lg font-semibold">
|
||||
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||
</h3>
|
||||
|
||||
<button
|
||||
onClick={() => navigateMonth('next')}
|
||||
className="p-2 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-blue-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
{isVerrouille() ? (
|
||||
<Button
|
||||
onClick={() => handleToggleLock(true)}
|
||||
className="flex-1 bg-orange-600 hover:bg-orange-700"
|
||||
title="Permettre au collaborateur de modifier ses saisies"
|
||||
>
|
||||
<Unlock className="w-4 h-4 mr-2" />
|
||||
Déverrouiller pour modification
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => handleToggleLock(false)}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||
title="Verrouiller le mois"
|
||||
>
|
||||
<Lock className="w-4 h-4 mr-2" />
|
||||
Verrouiller
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleExportPDF}
|
||||
className="flex-1 bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export PDF
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Stats du mois */}
|
||||
{mensuelData && (
|
||||
<Card className="shadow-card">
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-lg font-bold text-foreground mb-4">
|
||||
Statistiques du mois
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-blue-600">
|
||||
{mensuelData.NbJoursTravailles || 0}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Jours travaillés</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-orange-600">
|
||||
{mensuelData.NbJoursNonRespectsReposQuotidien || 0}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Non-respect repos quotidien</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-2xl font-bold text-red-600">
|
||||
{mensuelData.NbJoursNonRespectsReposHebdo || 0}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Non-respect repos hebdo</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Liste des jours */}
|
||||
<Card className="shadow-card">
|
||||
<CardHeader className="bg-gray-50 border-b">
|
||||
<h3 className="font-bold text-foreground">
|
||||
Jours saisis ({compteRenduData.length})
|
||||
</h3>
|
||||
</CardHeader>
|
||||
|
||||
<div className="divide-y max-h-[500px] overflow-y-auto">
|
||||
{compteRenduData.length === 0 ? (
|
||||
<div className="p-8 text-center text-muted-foreground">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||
<p>Aucune saisie pour ce mois</p>
|
||||
</div>
|
||||
) : (
|
||||
compteRenduData.map((jour, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-4 hover:bg-gray-50 transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="font-semibold text-foreground">
|
||||
{formatDate(jour.JourDate)}
|
||||
</p>
|
||||
|
||||
{jour.JourTravaille ? (
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded">
|
||||
Travaillé
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">
|
||||
Non travaillé
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!jour.ReposQuotidienRespect && (
|
||||
<span className="px-2 py-1 bg-orange-100 text-orange-700 text-xs rounded">
|
||||
⚠️ Repos quotidien
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!jour.ReposHebdomadaireRespect && (
|
||||
<span className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">
|
||||
⚠️ Repos hebdo
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{jour.CommentaireRepos && (
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
💬 {jour.CommentaireRepos}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewDetail(jour)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Modal détail jour */}
|
||||
{showDetailModal && selectedJour && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="bg-gradient-to-r from-cyan-500 to-blue-500 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold">
|
||||
Détail du {formatDate(selectedJour.JourDate)}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowDetailModal(false)}
|
||||
className="text-white hover:bg-white/20 p-1 rounded"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Statut</p>
|
||||
<p className="font-semibold">
|
||||
{selectedJour.JourTravaille ? '✅ Jour travaillé' : '❌ Jour non travaillé'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Repos quotidien respecté</p>
|
||||
<p className="font-semibold">
|
||||
{selectedJour.ReposQuotidienRespect ? '✅ Oui' : '⚠️ Non'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Repos hebdomadaire respecté</p>
|
||||
<p className="font-semibold">
|
||||
{selectedJour.ReposHebdomadaireRespect ? '✅ Oui' : '⚠️ Non'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedJour.CommentaireRepos && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Commentaire</p>
|
||||
<p className="text-sm bg-gray-50 p-3 rounded">
|
||||
{selectedJour.CommentaireRepos}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Date de saisie</p>
|
||||
<p className="text-sm">
|
||||
{selectedJour.DateSaisie ? formatDate(selectedJour.DateSaisie) : 'Non disponible'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedJour.DateModification && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Dernière modification</p>
|
||||
<p className="text-sm">
|
||||
{formatDate(selectedJour.DateModification)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompteRenduRH;
|
||||
Reference in New Issue
Block a user