Files
GTA/project/src/components/EditLeaveRequestModal.jsx
2025-12-02 17:49:04 +01:00

515 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { X, Calendar, AlertCircle, Upload } from 'lucide-react';
const EditLeaveRequestModal = ({
onClose,
request,
availableLeaveCounters,
accessToken,
userId,
userEmail,
userRole,
userName,
onRequestUpdated
}) => {
const [leaveType, setLeaveType] = useState(request.typeId || '');
const [startDate, setStartDate] = useState(request.startDate || '');
const [endDate, setEndDate] = useState(request.endDate || '');
const [reason, setReason] = useState(request.reason || '');
const [businessDays, setBusinessDays] = useState(request.days || 0);
const [saturdayCount, setSaturdayCount] = useState(0);
const [medicalDocuments, setMedicalDocuments] = useState([]);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitMessage, setSubmitMessage] = useState({ type: '', text: '' });
// ⭐ Types de congés disponibles selon le rôle
const getLeaveTypes = () => {
const baseTypes = [
{ id: 1, name: 'Congé payé', key: 'CP', counter: availableLeaveCounters.availableCP },
];
// Ajouter RTT sauf pour les apprentis
if (userRole !== 'Apprenti') {
baseTypes.push({
id: 2,
name: 'RTT',
key: 'RTT',
counter: availableLeaveCounters.availableRTT
});
}
// Ajouter les types sans compteur
baseTypes.push(
{ id: 3, name: 'Arrêt maladie', key: 'ABS', counter: null },
{ id: 5, name: 'Récupération (samedi)', key: 'Récup', counter: null }
);
// Ajouter Formation pour les apprentis
if (userRole === 'Apprenti') {
baseTypes.push({ id: 4, name: 'Formation', key: 'Formation', counter: null });
}
return baseTypes;
};
const leaveTypes = getLeaveTypes();
// ⭐ Calcul des jours ouvrés ET des samedis
useEffect(() => {
if (startDate && endDate) {
const result = calculateBusinessDaysAndSaturdays(startDate, endDate);
setBusinessDays(result.workingDays);
setSaturdayCount(result.saturdays);
}
}, [startDate, endDate]);
const calculateBusinessDaysAndSaturdays = (start, end) => {
const startD = new Date(start);
const endD = new Date(end);
let workingDays = 0;
let saturdays = 0;
const current = new Date(startD);
while (current <= endD) {
const dayOfWeek = current.getDay();
if (dayOfWeek === 6) {
saturdays++;
} else if (dayOfWeek !== 0) { // Pas dimanche
workingDays++;
}
current.setDate(current.getDate() + 1);
}
return { workingDays, saturdays };
};
const validateForm = () => {
const newErrors = {};
if (!leaveType) {
newErrors.leaveType = 'Veuillez sélectionner un type de congé';
}
if (!startDate) {
newErrors.startDate = 'La date de début est requise';
}
if (!endDate) {
newErrors.endDate = 'La date de fin est requise';
}
if (startDate && endDate && new Date(startDate) > new Date(endDate)) {
newErrors.endDate = 'La date de fin doit être après la date de début';
}
// ⭐ Validation spécifique pour Récupération
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
if (selectedType?.key === 'Récup') {
if (saturdayCount === 0) {
newErrors.days = 'Une récupération nécessite au moins un samedi dans la période sélectionnée';
}
}
// ⭐ Validation spécifique pour Arrêt maladie
if (selectedType?.key === 'ABS' && medicalDocuments.length === 0) {
newErrors.medical = 'Un justificatif médical est obligatoire pour un arrêt maladie';
}
// Vérification du solde disponible (CP et RTT uniquement)
if (selectedType && selectedType.counter !== null && businessDays > selectedType.counter) {
newErrors.days = `Solde insuffisant. Disponible : ${selectedType.counter} jour(s)`;
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleFileUpload = (e) => {
const files = Array.from(e.target.files);
const validFiles = [];
const maxSize = 5 * 1024 * 1024; // 5MB
for (const file of files) {
const validTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
if (!validTypes.includes(file.type)) {
setSubmitMessage({
type: 'error',
text: `Le fichier "${file.name}" n'est pas un format valide.`
});
continue;
}
if (file.size > maxSize) {
setSubmitMessage({
type: 'error',
text: `Le fichier "${file.name}" est trop volumineux (max 5MB).`
});
continue;
}
validFiles.push(file);
}
setMedicalDocuments(prev => [...prev, ...validFiles]);
e.target.value = '';
};
const removeDocument = (index) => {
setMedicalDocuments(prev => prev.filter((_, i) => i !== index));
};
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setSubmitMessage({ type: '', text: '' });
try {
const formDataToSend = new FormData();
formDataToSend.append('requestId', request.id);
formDataToSend.append('leaveType', parseInt(leaveType));
formDataToSend.append('startDate', startDate);
formDataToSend.append('endDate', endDate);
formDataToSend.append('reason', reason);
formDataToSend.append('userId', userId);
formDataToSend.append('userEmail', userEmail);
formDataToSend.append('userName', userName);
formDataToSend.append('accessToken', accessToken);
// ⭐ Calcul des jours selon le type
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
const daysToSend = selectedType?.key === 'Récup' ? saturdayCount : businessDays;
formDataToSend.append('businessDays', daysToSend);
// ⭐ Documents médicaux
medicalDocuments.forEach((file) => {
formDataToSend.append('medicalDocuments', file);
});
const response = await fetch('http://localhost:3000/updateRequest', {
method: 'POST',
body: formDataToSend
});
const data = await response.json();
if (data.success) {
setSubmitMessage({
type: 'success',
text: '✅ Demande modifiée avec succès ! Le manager a été informé par email.'
});
setTimeout(() => {
onRequestUpdated();
onClose();
}, 2000);
} else {
setSubmitMessage({
type: 'error',
text: `${data.message || 'Erreur lors de la modification'}`
});
}
} catch (error) {
console.error('Erreur:', error);
setSubmitMessage({
type: 'error',
text: '❌ Une erreur est survenue. Veuillez réessayer.'
});
} finally {
setIsSubmitting(false);
}
};
const getMinDate = () => {
const today = new Date();
return today.toISOString().split('T')[0];
};
const selectedType = leaveTypes.find(t => t.id === parseInt(leaveType));
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-black bg-opacity-50"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center rounded-t-2xl">
<h2 className="text-2xl font-bold text-gray-900">Modifier la demande</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Body */}
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Message de statut */}
{submitMessage.text && (
<div className={`p-4 rounded-lg ${submitMessage.type === 'success'
? 'bg-green-50 text-green-800 border border-green-200'
: 'bg-red-50 text-red-800 border border-red-200'
}`}>
{submitMessage.text}
</div>
)}
{/* Info - Demande originale */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="font-semibold text-blue-900 mb-2 flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
Demande actuelle
</h3>
<div className="text-sm text-blue-800 space-y-1">
<p><strong>Type :</strong> {request.type}</p>
<p><strong>Dates :</strong> {request.dateDisplay}</p>
<p><strong>Jours :</strong> {request.days}</p>
</div>
</div>
{/* Type de congé */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Type de congé *
</label>
<select
value={leaveType}
onChange={(e) => setLeaveType(e.target.value)}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.leaveType ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Sélectionnez un type</option>
{leaveTypes.map(type => (
<option key={type.id} value={type.id}>
{type.name}
{type.counter !== null && ` (${type.counter.toFixed(1)} jours disponibles)`}
</option>
))}
</select>
{errors.leaveType && (
<p className="mt-1 text-sm text-red-600">{errors.leaveType}</p>
)}
</div>
{/* Dates */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Date de début *
</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
min={getMinDate()}
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.startDate ? 'border-red-500' : 'border-gray-300'
}`}
/>
</div>
{errors.startDate && (
<p className="mt-1 text-sm text-red-600">{errors.startDate}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Date de fin *
</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
min={startDate || getMinDate()}
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errors.endDate ? 'border-red-500' : 'border-gray-300'
}`}
/>
</div>
{errors.endDate && (
<p className="mt-1 text-sm text-red-600">{errors.endDate}</p>
)}
</div>
</div>
{/* ⭐ Résumé de la période */}
{startDate && endDate && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-sm font-medium text-blue-900 mb-1">Résumé de la période :</p>
<div className="text-xs text-blue-700 space-y-1">
<p> <strong>{businessDays} jour(s) ouvré(s)</strong> (lundi-vendredi)</p>
{saturdayCount > 0 && (
<>
<p className="text-purple-700"> {saturdayCount} samedi(s) détecté(s)</p>
{selectedType?.key !== 'Récup' && (
<p className="text-orange-700 font-medium">
Les samedis seront ignorés (sélectionnez "Récupération" pour les inclure)
</p>
)}
{selectedType?.key === 'Récup' && (
<p className="text-green-700 font-medium">
Récupération : {saturdayCount} samedi(s) seront comptabilisés
</p>
)}
</>
)}
</div>
</div>
)}
{/* Nombre de jours */}
{(businessDays > 0 || saturdayCount > 0) && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<p className="text-sm text-gray-700">
<strong>Nombre de jours {selectedType?.key === 'Récup' ? '(samedis)' : 'ouvrés'} :</strong>{' '}
{selectedType?.key === 'Récup' ? saturdayCount : businessDays}
</p>
{errors.days && (
<p className="mt-1 text-sm text-red-600">{errors.days}</p>
)}
</div>
)}
{/* ⭐ Upload documents médicaux pour Arrêt maladie */}
{selectedType?.key === 'ABS' && (
<div>
<label className="block text-sm font-medium text-gray-900 mb-3">
Justificatif médical <span className="text-red-600">*</span>
</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
<div className="w-12 h-12 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<Upload className="w-6 h-6 text-gray-400" />
</div>
<p className="text-gray-700 text-sm mb-2">
Glissez vos documents ici ou cliquez pour sélectionner
</p>
<p className="text-gray-500 text-xs mb-4">
Formats acceptés : PDF, JPG, PNG (max 5MB par fichier)
</p>
<input
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png"
onChange={handleFileUpload}
className="hidden"
id="medical-documents"
/>
<label
htmlFor="medical-documents"
className="inline-block px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors text-sm font-medium text-gray-700"
>
Sélectionner des fichiers
</label>
</div>
{medicalDocuments.length > 0 && (
<div className="mt-4 space-y-2">
<p className="text-sm font-medium text-gray-900 mb-2">
Fichiers sélectionnés ({medicalDocuments.length}) :
</p>
{medicalDocuments.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-blue-100 rounded flex items-center justify-center flex-shrink-0">
{file.type === 'application/pdf' ? (
<svg className="w-4 h-4 text-red-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 truncate">{file.name}</p>
<p className="text-xs text-gray-500">{formatFileSize(file.size)}</p>
</div>
</div>
<button
type="button"
onClick={() => removeDocument(index)}
className="text-gray-400 hover:text-red-600 ml-2 flex-shrink-0"
>
<X className="w-5 h-5" />
</button>
</div>
))}
</div>
)}
{errors.medical && (
<p className="mt-2 text-sm text-red-600">{errors.medical}</p>
)}
</div>
)}
{/* Motif */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Motif (optionnel)
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={3}
placeholder="Précisez le motif de votre demande..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
</div>
{/* Info importante */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<p className="text-sm text-yellow-800">
<strong>Important :</strong> Votre manager sera automatiquement informé par email de cette modification.
</p>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 font-medium transition-colors"
>
Annuler
</button>
<button
type="submit"
disabled={isSubmitting}
className={`flex-1 px-6 py-3 bg-blue-600 text-white rounded-lg font-medium transition-colors ${isSubmitting
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-blue-700'
}`}
>
{isSubmitting ? 'Modification...' : 'Modifier la demande'}
</button>
</div>
</form>
</div>
</div>
);
};
export default EditLeaveRequestModal;