788 lines
34 KiB
JavaScript
788 lines
34 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { X, AlertCircle, Upload, FileText, Image as ImageIcon, Trash2 } from 'lucide-react';
|
||
|
||
const EditLeaveRequestModal = ({
|
||
isOpen,
|
||
onClose,
|
||
request,
|
||
onRequestUpdated,
|
||
availableLeaveCounters,
|
||
userId,
|
||
userEmail,
|
||
userName,
|
||
accessToken
|
||
}) => {
|
||
// ========================================
|
||
// ÉTATS
|
||
// ========================================
|
||
const [selectedTypes, setSelectedTypes] = useState([]);
|
||
const [startDate, setStartDate] = useState('');
|
||
const [endDate, setEndDate] = useState('');
|
||
const [reason, setReason] = useState('');
|
||
const [businessDays, setBusinessDays] = useState(0);
|
||
const [saturdayCount, setSaturdayCount] = useState(0);
|
||
|
||
// Répartition manuelle (multi-types)
|
||
const [repartition, setRepartition] = useState({});
|
||
|
||
// Période par type (Matin/Après-midi/Journée entière)
|
||
const [periodeSelection, setPeriodeSelection] = useState({});
|
||
|
||
// Documents médicaux
|
||
const [medicalDocuments, setMedicalDocuments] = useState([]);
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
|
||
// Compteurs
|
||
const [countersData, setCountersData] = useState(null);
|
||
const [isLoadingCounters, setIsLoadingCounters] = useState(true);
|
||
|
||
// UI
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [submitMessage, setSubmitMessage] = useState({ type: '', text: '' });
|
||
const [validationErrors, setValidationErrors] = useState([]);
|
||
|
||
const availableTypes = [
|
||
{ id: 'CP', label: 'Congé payé', color: '#3b82f6' },
|
||
{ id: 'RTT', label: 'RTT', color: '#8b5cf6' },
|
||
{ id: 'Récup', label: 'Récupération', color: '#10b981' },
|
||
{ id: 'ABS', label: 'Arrêt maladie', color: '#ef4444' },
|
||
{ id: 'Formation', label: 'Formation', color: '#f59e0b' }
|
||
];
|
||
|
||
// ========================================
|
||
// INITIALISATION
|
||
// ========================================
|
||
useEffect(() => {
|
||
if (isOpen && request) {
|
||
console.log('📝 Initialisation EditModal avec request:', request);
|
||
|
||
// Dates
|
||
setStartDate(request.startDate || '');
|
||
setEndDate(request.endDate || '');
|
||
setReason(request.reason || '');
|
||
|
||
// Types (mapping inverse)
|
||
const typeMapping = {
|
||
'Congé payé': 'CP',
|
||
'RTT': 'RTT',
|
||
'Récupération': 'Récup',
|
||
'Congé maladie': 'ABS',
|
||
'Formation': 'Formation'
|
||
};
|
||
|
||
if (request.type) {
|
||
const types = request.type.split(', ').map(t => typeMapping[t] || t);
|
||
setSelectedTypes(types);
|
||
console.log('✅ Types initialisés:', types);
|
||
}
|
||
|
||
// Calculer jours ouvrés
|
||
if (request.startDate && request.endDate) {
|
||
const days = calculateBusinessDays(request.startDate, request.endDate);
|
||
setBusinessDays(days.businessDays);
|
||
setSaturdayCount(days.saturdayCount);
|
||
}
|
||
}
|
||
}, [isOpen, request]);
|
||
|
||
// Charger les compteurs
|
||
useEffect(() => {
|
||
if (isOpen && userId) {
|
||
loadCounters();
|
||
}
|
||
}, [isOpen, userId]);
|
||
|
||
// ========================================
|
||
// FONCTIONS UTILITAIRES
|
||
// ========================================
|
||
const calculateBusinessDays = (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) {
|
||
workingDays++;
|
||
}
|
||
current.setDate(current.getDate() + 1);
|
||
}
|
||
|
||
return { businessDays: workingDays, saturdayCount: saturdays };
|
||
};
|
||
|
||
const loadCounters = async () => {
|
||
setIsLoadingCounters(true);
|
||
try {
|
||
const response = await fetch(`/api/getDetailedLeaveCounters?user_id=${userId}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
setCountersData(data);
|
||
console.log('✅ Compteurs chargés:', data);
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Erreur chargement compteurs:', error);
|
||
} finally {
|
||
setIsLoadingCounters(false);
|
||
}
|
||
};
|
||
|
||
// ========================================
|
||
// GESTION DES TYPES
|
||
// ========================================
|
||
const handleTypeToggle = (typeId) => {
|
||
if (typeId === 'ABS' && selectedTypes.length > 0 && !selectedTypes.includes('ABS')) {
|
||
alert('⚠️ L\'arrêt maladie ne peut pas être combiné avec d\'autres types');
|
||
return;
|
||
}
|
||
|
||
if (selectedTypes.includes('ABS') && typeId !== 'ABS') {
|
||
alert('⚠️ L\'arrêt maladie ne peut pas être combiné avec d\'autres types');
|
||
return;
|
||
}
|
||
|
||
setSelectedTypes(prev => {
|
||
if (prev.includes(typeId)) {
|
||
const newTypes = prev.filter(t => t !== typeId);
|
||
const newRep = { ...repartition };
|
||
delete newRep[typeId];
|
||
setRepartition(newRep);
|
||
|
||
const newPeriodes = { ...periodeSelection };
|
||
delete newPeriodes[typeId];
|
||
setPeriodeSelection(newPeriodes);
|
||
|
||
return newTypes;
|
||
} else {
|
||
return [...prev, typeId];
|
||
}
|
||
});
|
||
};
|
||
|
||
// ========================================
|
||
// GESTION RÉPARTITION
|
||
// ========================================
|
||
const handleRepartitionChange = (typeId, value) => {
|
||
const numValue = parseFloat(value) || 0;
|
||
const maxValue = businessDays;
|
||
|
||
if (numValue > maxValue) {
|
||
alert(`Maximum ${maxValue} jours`);
|
||
return;
|
||
}
|
||
|
||
setRepartition(prev => ({
|
||
...prev,
|
||
[typeId]: numValue
|
||
}));
|
||
};
|
||
|
||
const handlePeriodeChange = (typeId, periode) => {
|
||
setPeriodeSelection(prev => ({
|
||
...prev,
|
||
[typeId]: periode
|
||
}));
|
||
|
||
// Calcul automatique si un seul type
|
||
if (selectedTypes.length === 1 && startDate === endDate) {
|
||
if (periode === 'Matin' || periode === 'Après-midi') {
|
||
setRepartition({ [typeId]: 0.5 });
|
||
} else {
|
||
setRepartition({ [typeId]: businessDays });
|
||
}
|
||
}
|
||
};
|
||
|
||
// ========================================
|
||
// GESTION FICHIERS MÉDICAUX
|
||
// ========================================
|
||
const handleFileSelect = (e) => {
|
||
const files = Array.from(e.target.files);
|
||
addFiles(files);
|
||
};
|
||
|
||
const handleDrop = (e) => {
|
||
e.preventDefault();
|
||
setIsDragging(false);
|
||
const files = Array.from(e.dataTransfer.files);
|
||
addFiles(files);
|
||
};
|
||
|
||
const addFiles = (files) => {
|
||
const validFiles = files.filter(file => {
|
||
const isValidType = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'].includes(file.type);
|
||
const isValidSize = file.size <= 5 * 1024 * 1024;
|
||
|
||
if (!isValidType) {
|
||
alert(`❌ ${file.name}: Type non autorisé (PDF, JPG, PNG uniquement)`);
|
||
return false;
|
||
}
|
||
if (!isValidSize) {
|
||
alert(`❌ ${file.name}: Taille max 5MB`);
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
setMedicalDocuments(prev => [...prev, ...validFiles]);
|
||
};
|
||
|
||
const removeFile = (index) => {
|
||
setMedicalDocuments(prev => prev.filter((_, i) => i !== index));
|
||
};
|
||
|
||
// ========================================
|
||
// VALIDATION
|
||
// ========================================
|
||
const validateForm = () => {
|
||
const errors = [];
|
||
|
||
// Dates
|
||
if (!startDate || !endDate) {
|
||
errors.push('Les dates sont obligatoires');
|
||
} else if (new Date(startDate) > new Date(endDate)) {
|
||
errors.push('La date de fin doit être après la date de début');
|
||
}
|
||
|
||
// Types
|
||
if (selectedTypes.length === 0) {
|
||
errors.push('Sélectionnez au moins un type de congé');
|
||
}
|
||
|
||
// Documents pour ABS
|
||
if (selectedTypes.includes('ABS') && medicalDocuments.length === 0) {
|
||
errors.push('Un justificatif médical est obligatoire pour un arrêt maladie');
|
||
}
|
||
|
||
// Répartition
|
||
if (selectedTypes.length > 1) {
|
||
const total = Object.values(repartition).reduce((sum, val) => sum + val, 0);
|
||
if (Math.abs(total - businessDays) > 0.01) {
|
||
errors.push(`La répartition (${total.toFixed(1)}j) ne correspond pas au total (${businessDays}j)`);
|
||
}
|
||
}
|
||
|
||
// Compteurs (si chargés)
|
||
if (countersData?.data?.totalDisponible) {
|
||
const safeCounters = {
|
||
availableCP: countersData.data.cpN?.solde || 0,
|
||
availableRTT: countersData.data.rttN?.solde || 0,
|
||
availableRecup: countersData.data.recupN?.solde || 0
|
||
};
|
||
|
||
selectedTypes.forEach(type => {
|
||
if (type === 'CP') {
|
||
const cpDemande = selectedTypes.length === 1 ? businessDays : (repartition[type] || 0);
|
||
if (cpDemande > safeCounters.availableCP) {
|
||
errors.push(`Solde CP insuffisant (${safeCounters.availableCP.toFixed(1)}j disponibles)`);
|
||
}
|
||
}
|
||
|
||
if (type === 'RTT') {
|
||
const rttDemande = selectedTypes.length === 1 ? businessDays : (repartition[type] || 0);
|
||
if (rttDemande > safeCounters.availableRTT) {
|
||
errors.push(`Solde RTT insuffisant (${safeCounters.availableRTT.toFixed(1)}j disponibles)`);
|
||
}
|
||
}
|
||
|
||
if (type === 'Récup') {
|
||
const recupDemande = selectedTypes.length === 1 ? businessDays : (repartition[type] || 0);
|
||
if (recupDemande > safeCounters.availableRecup) {
|
||
errors.push(`Solde Récup insuffisant (${safeCounters.availableRecup.toFixed(1)}j disponibles)`);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
setValidationErrors(errors);
|
||
return errors.length === 0;
|
||
};
|
||
|
||
// ========================================
|
||
// SOUMISSION
|
||
// ========================================
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault();
|
||
|
||
if (!validateForm()) {
|
||
return;
|
||
}
|
||
|
||
setIsSubmitting(true);
|
||
setSubmitMessage({ type: '', text: '' });
|
||
|
||
try {
|
||
const formDataToSend = new FormData();
|
||
|
||
// ⭐ CHAMPS REQUIS PAR LE BACKEND
|
||
formDataToSend.append('requestId', request.id.toString());
|
||
formDataToSend.append('userId', userId.toString());
|
||
formDataToSend.append('userEmail', userEmail);
|
||
formDataToSend.append('userName', userName);
|
||
formDataToSend.append('accessToken', accessToken || '');
|
||
|
||
// ⭐ DATES
|
||
formDataToSend.append('DateDebut', startDate);
|
||
formDataToSend.append('DateFin', endDate);
|
||
formDataToSend.append('startDate', startDate);
|
||
formDataToSend.append('endDate', endDate);
|
||
|
||
// ⭐ COMMENTAIRE
|
||
formDataToSend.append('Commentaire', reason || 'Aucun commentaire');
|
||
formDataToSend.append('reason', reason || 'Aucun commentaire');
|
||
|
||
// ⭐ CALCUL NOMBRE DE JOURS TOTAL
|
||
let totalJoursToSend = businessDays;
|
||
|
||
if (selectedTypes.length === 1 && startDate === endDate) {
|
||
const type = selectedTypes[0];
|
||
const periode = periodeSelection[type];
|
||
|
||
if ((type === 'CP' || type === 'RTT' || type === 'Récup') &&
|
||
(periode === 'Matin' || periode === 'Après-midi')) {
|
||
totalJoursToSend = 0.5;
|
||
}
|
||
}
|
||
|
||
formDataToSend.append('NombreJours', totalJoursToSend.toString());
|
||
formDataToSend.append('businessDays', totalJoursToSend.toString());
|
||
|
||
// ⭐ RÉPARTITION (CORRECTION ICI)
|
||
const repartitionArray = selectedTypes.map(type => {
|
||
let nombreJours;
|
||
let periodeJournee = 'Journée entière';
|
||
|
||
if (selectedTypes.length === 1) {
|
||
const periode = periodeSelection[type] || 'Journée entière';
|
||
|
||
if ((type === 'CP' || type === 'RTT' || type === 'Récup') &&
|
||
startDate === endDate &&
|
||
(periode === 'Matin' || periode === 'Après-midi')) {
|
||
nombreJours = 0.5;
|
||
periodeJournee = periode;
|
||
} else {
|
||
nombreJours = businessDays;
|
||
}
|
||
} else {
|
||
nombreJours = repartition[type] || 0;
|
||
periodeJournee = periodeSelection[type] || 'Journée entière';
|
||
}
|
||
|
||
return {
|
||
TypeConge: type,
|
||
NombreJours: nombreJours,
|
||
PeriodeJournee: ['CP', 'RTT', 'Récup'].includes(type) ? periodeJournee : 'Journée entière'
|
||
};
|
||
});
|
||
|
||
// ⭐ STRINGIFIER LA RÉPARTITION (CRITIQUE POUR FORMDATA)
|
||
formDataToSend.append('Repartition', JSON.stringify(repartitionArray));
|
||
|
||
// ⭐ TYPE DE CONGÉ (pour compatibilité backend)
|
||
const leaveTypeMapping = {
|
||
'CP': 1,
|
||
'RTT': 2,
|
||
'ABS': 3,
|
||
'Formation': 4,
|
||
'Récup': 5
|
||
};
|
||
const leaveTypeId = leaveTypeMapping[selectedTypes[0]] || 1;
|
||
formDataToSend.append('leaveType', leaveTypeId.toString());
|
||
|
||
// Documents médicaux EN DERNIER
|
||
if (medicalDocuments.length > 0) {
|
||
medicalDocuments.forEach((file) => {
|
||
formDataToSend.append('medicalDocuments', file);
|
||
});
|
||
}
|
||
|
||
console.log('📤 Envoi modification demande...');
|
||
console.log('📊 Répartition envoyée:', JSON.stringify(repartitionArray, null, 2));
|
||
|
||
for (let pair of formDataToSend.entries()) {
|
||
if (pair[0] !== 'medicalDocuments') {
|
||
console.log(pair[0], ':', pair[1]);
|
||
}
|
||
}
|
||
|
||
const response = await fetch('/api/updateRequest', {
|
||
method: 'POST',
|
||
body: formDataToSend
|
||
});
|
||
|
||
const responseText = await response.text();
|
||
console.log('📥 Réponse brute:', responseText);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status} - ${responseText}`);
|
||
}
|
||
|
||
let data;
|
||
try {
|
||
data = JSON.parse(responseText);
|
||
} catch (parseError) {
|
||
console.error('❌ Erreur parsing JSON:', parseError);
|
||
throw new Error('Réponse serveur invalide: ' + responseText);
|
||
}
|
||
|
||
if (data.success) {
|
||
setSubmitMessage({
|
||
type: 'success',
|
||
text: '✅ Demande modifiée avec succès !'
|
||
});
|
||
|
||
setTimeout(() => {
|
||
onRequestUpdated();
|
||
onClose();
|
||
}, 1500);
|
||
} else {
|
||
setSubmitMessage({
|
||
type: 'error',
|
||
text: `❌ ${data.message || 'Erreur lors de la modification'}`
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Erreur:', error);
|
||
setSubmitMessage({
|
||
type: 'error',
|
||
text: `❌ ${error.message || 'Une erreur est survenue'}`
|
||
});
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// ========================================
|
||
// RECALCUL AUTO JOURS OUVRÉS
|
||
// ========================================
|
||
useEffect(() => {
|
||
if (startDate && endDate) {
|
||
const days = calculateBusinessDays(startDate, endDate);
|
||
setBusinessDays(days.businessDays);
|
||
setSaturdayCount(days.saturdayCount);
|
||
|
||
// Réinitialiser répartition si changement de dates
|
||
if (selectedTypes.length === 1) {
|
||
const type = selectedTypes[0];
|
||
setRepartition({ [type]: days.businessDays });
|
||
}
|
||
}
|
||
}, [startDate, endDate]);
|
||
|
||
// ========================================
|
||
// RENDER
|
||
// ========================================
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||
{/* HEADER */}
|
||
<div className="sticky top-0 bg-white border-b px-6 py-4 flex justify-between items-center">
|
||
<h2 className="text-2xl font-bold text-gray-800">
|
||
✏️ Modifier la demande
|
||
</h2>
|
||
<button
|
||
onClick={onClose}
|
||
className="p-2 hover:bg-gray-100 rounded-full transition"
|
||
>
|
||
<X size={24} />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||
{/* 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>
|
||
<input
|
||
type="date"
|
||
value={startDate}
|
||
onChange={(e) => setStartDate(e.target.value)}
|
||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Date de fin *
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={endDate}
|
||
onChange={(e) => setEndDate(e.target.value)}
|
||
min={startDate}
|
||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* RÉSUMÉ PÉRIODE */}
|
||
{businessDays > 0 && (
|
||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||
<div className="flex items-center gap-2 text-blue-800">
|
||
<span className="font-semibold">📅 Période :</span>
|
||
<span>{businessDays} jour(s) ouvré(s)</span>
|
||
{saturdayCount > 0 && (
|
||
<span className="text-sm">+ {saturdayCount} samedi(s)</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TYPES DE CONGÉ */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||
Types de congé *
|
||
</label>
|
||
|
||
{isLoadingCounters ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||
<span className="ml-3 text-gray-600">Chargement des compteurs...</span>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{availableTypes.map(type => {
|
||
const isSelected = selectedTypes.includes(type.id);
|
||
let counterDisplay = null;
|
||
|
||
if (countersData?.data) {
|
||
if (type.id === 'CP') {
|
||
const solde = countersData.data.cpN?.solde || 0;
|
||
counterDisplay = `${solde.toFixed(1)}j`;
|
||
} else if (type.id === 'RTT') {
|
||
const solde = countersData.data.rttN?.solde || 0;
|
||
counterDisplay = `${solde.toFixed(1)}j`;
|
||
} else if (type.id === 'Récup') {
|
||
const solde = countersData.data.recupN?.solde || 0;
|
||
counterDisplay = `${solde.toFixed(1)}j`;
|
||
}
|
||
}
|
||
|
||
return (
|
||
<label
|
||
key={type.id}
|
||
className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition ${isSelected
|
||
? 'border-blue-500 bg-blue-50'
|
||
: 'border-gray-300 hover:border-gray-400'
|
||
}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={isSelected}
|
||
onChange={() => handleTypeToggle(type.id)}
|
||
className="w-5 h-5"
|
||
/>
|
||
<div
|
||
className="w-4 h-4 rounded"
|
||
style={{ backgroundColor: type.color }}
|
||
/>
|
||
<span className="font-medium">{type.label}</span>
|
||
{counterDisplay && (
|
||
<span className="ml-auto text-sm text-gray-600 font-mono">
|
||
{counterDisplay}
|
||
</span>
|
||
)}
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* RÉPARTITION SI MULTI-TYPES */}
|
||
{selectedTypes.length > 1 && (
|
||
<div className="bg-gray-50 border rounded-lg p-4 space-y-3">
|
||
<h3 className="font-semibold text-gray-800">📊 Répartition des jours</h3>
|
||
{selectedTypes.map(type => (
|
||
<div key={type} className="flex items-center gap-3">
|
||
<label className="w-32 font-medium">{type}</label>
|
||
<input
|
||
type="number"
|
||
step="0.5"
|
||
min="0"
|
||
max={businessDays}
|
||
value={repartition[type] || 0}
|
||
onChange={(e) => handleRepartitionChange(type, e.target.value)}
|
||
className="w-24 px-3 py-2 border rounded-lg"
|
||
/>
|
||
<span className="text-sm text-gray-600">jour(s)</span>
|
||
|
||
{/* Période */}
|
||
{(type === 'CP' || type === 'RTT' || type === 'Récup') && (
|
||
<div className="ml-auto flex gap-2">
|
||
{['Matin', 'Après-midi', 'Journée entière'].map(p => (
|
||
<button
|
||
key={p}
|
||
type="button"
|
||
onClick={() => handlePeriodeChange(type, p)}
|
||
className={`px-3 py-1 text-sm rounded ${periodeSelection[type] === p
|
||
? 'bg-blue-500 text-white'
|
||
: 'bg-gray-200 text-gray-700'
|
||
}`}
|
||
>
|
||
{p}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* PÉRIODE (UN SEUL TYPE) */}
|
||
{selectedTypes.length === 1 && startDate === endDate &&
|
||
(selectedTypes[0] === 'CP' || selectedTypes[0] === 'RTT' || selectedTypes[0] === 'Récup') && (
|
||
<div className="bg-gray-50 border rounded-lg p-4">
|
||
<h3 className="font-semibold text-gray-800 mb-3">⏰ Période de la journée</h3>
|
||
<div className="flex gap-3">
|
||
{['Matin', 'Après-midi', 'Journée entière'].map(p => (
|
||
<button
|
||
key={p}
|
||
type="button"
|
||
onClick={() => handlePeriodeChange(selectedTypes[0], p)}
|
||
className={`flex-1 py-2 px-4 rounded-lg font-medium transition ${periodeSelection[selectedTypes[0]] === p
|
||
? 'bg-blue-500 text-white'
|
||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||
}`}
|
||
>
|
||
{p}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* DOCUMENTS MÉDICAUX */}
|
||
{selectedTypes.includes('ABS') && (
|
||
<div className="space-y-3">
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Documents médicaux *
|
||
</label>
|
||
|
||
<div
|
||
onDrop={handleDrop}
|
||
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
||
onDragLeave={() => setIsDragging(false)}
|
||
className={`border-2 border-dashed rounded-lg p-6 text-center transition ${isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
|
||
}`}
|
||
>
|
||
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-3" />
|
||
<p className="text-sm text-gray-600 mb-2">
|
||
Glissez vos fichiers ici ou cliquez pour sélectionner
|
||
</p>
|
||
<input
|
||
type="file"
|
||
accept=".pdf,.jpg,.jpeg,.png"
|
||
multiple
|
||
onChange={handleFileSelect}
|
||
className="hidden"
|
||
id="medical-upload"
|
||
/>
|
||
<label
|
||
htmlFor="medical-upload"
|
||
className="inline-block px-4 py-2 bg-blue-500 text-white rounded-lg cursor-pointer hover:bg-blue-600"
|
||
>
|
||
Choisir des fichiers
|
||
</label>
|
||
</div>
|
||
|
||
{medicalDocuments.length > 0 && (
|
||
<div className="space-y-2">
|
||
{medicalDocuments.map((file, index) => (
|
||
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
|
||
{file.type === 'application/pdf' ? (
|
||
<FileText className="text-red-500" size={24} />
|
||
) : (
|
||
<ImageIcon className="text-green-500" size={24} />
|
||
)}
|
||
<span className="flex-1 text-sm truncate">{file.name}</span>
|
||
<span className="text-xs text-gray-500">
|
||
{(file.size / 1024).toFixed(0)} KB
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeFile(index)}
|
||
className="p-1 hover:bg-red-100 rounded text-red-500"
|
||
>
|
||
<Trash2 size={18} />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* COMMENTAIRE */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Commentaire
|
||
</label>
|
||
<textarea
|
||
value={reason}
|
||
onChange={(e) => setReason(e.target.value)}
|
||
rows="3"
|
||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
placeholder="Motif de la modification..."
|
||
/>
|
||
</div>
|
||
|
||
{/* ERREURS DE VALIDATION */}
|
||
{validationErrors.length > 0 && (
|
||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||
<div className="flex gap-2">
|
||
<AlertCircle className="text-red-500 flex-shrink-0" size={20} />
|
||
<div className="space-y-1">
|
||
{validationErrors.map((err, i) => (
|
||
<p key={i} className="text-sm text-red-700">{err}</p>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* MESSAGE SOUMISSION */}
|
||
{submitMessage.text && (
|
||
<div className={`p-4 rounded-lg ${submitMessage.type === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
|
||
}`}>
|
||
{submitMessage.text}
|
||
</div>
|
||
)}
|
||
|
||
{/* BOUTONS */}
|
||
<div className="flex gap-3 justify-end pt-4 border-t">
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||
disabled={isSubmitting}
|
||
>
|
||
Annuler
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmitting || isLoadingCounters}
|
||
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||
>
|
||
{isSubmitting ? 'Enregistrement...' : 'Enregistrer les modifications'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default EditLeaveRequestModal; |