diff --git a/app/api/trajets/[id]/validate/route.ts b/app/api/trajets/[id]/validate/route.ts new file mode 100644 index 0000000..b8b5abc --- /dev/null +++ b/app/api/trajets/[id]/validate/route.ts @@ -0,0 +1,115 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// POST - Valider un trajet et déduire les heures du chauffeur +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const body = await request.json(); + const { dureeHeures } = body; + + if (!dureeHeures || dureeHeures <= 0) { + return NextResponse.json( + { error: 'La durée du trajet est requise' }, + { status: 400 } + ); + } + + // Récupérer le trajet avec le chauffeur + const trajet = await prisma.trajet.findUnique({ + where: { id: params.id }, + include: { + chauffeur: true, + }, + }); + + if (!trajet) { + return NextResponse.json({ error: 'Trajet non trouvé' }, { status: 404 }); + } + + if (!trajet.chauffeur) { + return NextResponse.json( + { error: 'Aucun chauffeur assigné à ce trajet' }, + { status: 400 } + ); + } + + if (trajet.statut === 'Validé' || trajet.statut === 'Terminé') { + return NextResponse.json( + { error: 'Ce trajet a déjà été validé' }, + { status: 400 } + ); + } + + // Vérifier que le chauffeur a assez d'heures disponibles + const heuresRestantes = trajet.chauffeur.heuresRestantes; + const heuresADeduire = Math.round(dureeHeures); + + if (heuresRestantes < heuresADeduire) { + return NextResponse.json( + { + error: `Le chauffeur n'a que ${heuresRestantes}h disponibles. Le trajet nécessite ${heuresADeduire}h.`, + }, + { status: 400 } + ); + } + + // Mettre à jour le trajet et déduire les heures du chauffeur + const [trajetUpdated, chauffeurUpdated] = await Promise.all([ + prisma.trajet.update({ + where: { id: params.id }, + data: { + statut: 'Validé', + }, + include: { + adherent: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + email: true, + }, + }, + chauffeur: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + }, + }, + }, + }), + prisma.chauffeur.update({ + where: { id: trajet.chauffeur.id }, + data: { + heuresRestantes: heuresRestantes - heuresADeduire, + }, + }), + ]); + + return NextResponse.json({ + trajet: trajetUpdated, + chauffeur: { + id: chauffeurUpdated.id, + heuresRestantes: chauffeurUpdated.heuresRestantes, + }, + heuresDeduites: heuresADeduire, + }); + } catch (error) { + console.error('Erreur lors de la validation du trajet:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/components/CalendrierTrajets.tsx b/components/CalendrierTrajets.tsx index 8212d2c..dc9238d 100644 --- a/components/CalendrierTrajets.tsx +++ b/components/CalendrierTrajets.tsx @@ -1,6 +1,9 @@ 'use client'; import { useState, useEffect } from 'react'; +import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, useDraggable, useDroppable } from '@dnd-kit/core'; +import { CSS } from '@dnd-kit/utilities'; +import TrajetDetailModal from './TrajetDetailModal'; interface Trajet { id: string; @@ -28,12 +31,135 @@ interface CalendrierTrajetsProps { refreshTrigger?: number; } +// Composant draggable pour un événement de trajet +function DraggableTrajetEvent({ trajet, onClick }: { trajet: Trajet; onClick: (e: React.MouseEvent, trajet: Trajet) => void }) { + // Empêcher le drag pour les trajets annulés ou validés + const canDrag = trajet.statut !== 'Annulé' && trajet.statut !== 'Validé' && trajet.statut !== 'Terminé'; + + const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ + id: trajet.id, + data: { trajet }, + disabled: !canDrag, + }); + + const style = { + transform: CSS.Translate.toString(transform), + opacity: isDragging ? 0.5 : 1, + }; + + const getStatutStyle = (statut: string) => { + switch (statut) { + case 'Validé': + return { + bg: 'bg-purple-500', + text: 'text-white', + border: 'border-purple-600', + }; + case 'Terminé': + return { + bg: 'bg-green-500', + text: 'text-white', + border: 'border-green-600', + }; + case 'En cours': + return { + bg: 'bg-blue-500', + text: 'text-white', + border: 'border-blue-600', + }; + case 'Annulé': + return { + bg: 'bg-red-500', + text: 'text-white', + border: 'border-red-600', + }; + default: + return { + bg: 'bg-lblue', + text: 'text-white', + border: 'border-lblue', + }; + } + }; + + const formatTime = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); + }; + + const styleObj = getStatutStyle(trajet.statut); + + return ( + + ); +} + +// Composant droppable pour une cellule de jour +function DroppableDayCell({ + date, + children, + isToday, + isSelected, + onDateClick +}: { + date: Date; + children: React.ReactNode; + isToday: boolean; + isSelected: boolean; + onDateClick: (date: Date) => void; +}) { + // Créer un ID basé sur les composants de date pour éviter les problèmes de fuseau horaire + const dayId = `day-${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + + const { setNodeRef, isOver } = useDroppable({ + id: dayId, + data: { date }, + }); + + return ( + + ); +} + export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsProps) { const [trajets, setTrajets] = useState([]); const [loading, setLoading] = useState(true); const [currentDate, setCurrentDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(null); const [selectedTrajets, setSelectedTrajets] = useState([]); + const [selectedTrajet, setSelectedTrajet] = useState(null); + const [activeId, setActiveId] = useState(null); + const [draggedTrajet, setDraggedTrajet] = useState(null); const month = currentDate.getMonth(); const year = currentDate.getFullYear(); @@ -47,11 +173,13 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP const adjustedStartingDay = startingDayOfWeek === 0 ? 6 : startingDayOfWeek - 1; useEffect(() => { - fetchTrajets(); + fetchTrajets(true); }, [currentDate, refreshTrigger]); - const fetchTrajets = async () => { - setLoading(true); + const fetchTrajets = async (showLoading = true) => { + if (showLoading) { + setLoading(true); + } try { const startDate = new Date(year, month, 1).toISOString(); const endDate = new Date(year, month + 1, 0, 23, 59, 59).toISOString(); @@ -62,19 +190,45 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP if (response.ok) { const data = await response.json(); setTrajets(data); + + // Mettre à jour la liste des trajets du jour sélectionné après le fetch + if (selectedDate) { + const trajetsDuJour = data.filter((t: Trajet) => { + const trajetDate = new Date(t.date); + return ( + trajetDate.getDate() === selectedDate.getDate() && + trajetDate.getMonth() === selectedDate.getMonth() && + trajetDate.getFullYear() === selectedDate.getFullYear() + ); + }); + setSelectedTrajets(trajetsDuJour); + } + } else { + console.error('Erreur lors de la récupération des trajets:', response.statusText); } } catch (error) { console.error('Erreur lors du chargement des trajets:', error); + // Ne pas vider les trajets en cas d'erreur } finally { - setLoading(false); + if (showLoading) { + setLoading(false); + } } }; const getTrajetsForDate = (date: Date): Trajet[] => { - const dateStr = date.toISOString().split('T')[0]; + // Utiliser les composants de date directement pour éviter les problèmes de fuseau horaire + const targetYear = date.getFullYear(); + const targetMonth = date.getMonth(); + const targetDay = date.getDate(); + return trajets.filter((trajet) => { - const trajetDate = new Date(trajet.date).toISOString().split('T')[0]; - return trajetDate === dateStr; + const trajetDate = new Date(trajet.date); + return ( + trajetDate.getFullYear() === targetYear && + trajetDate.getMonth() === targetMonth && + trajetDate.getDate() === targetDay + ); }); }; @@ -84,6 +238,89 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP setSelectedTrajets(trajetsDuJour); }; + const handleTrajetClick = (e: React.MouseEvent, trajet: Trajet) => { + e.stopPropagation(); // Empêcher le clic sur la date + setSelectedTrajet(trajet); + }; + + const handleDragStart = (event: DragStartEvent) => { + setActiveId(event.active.id as string); + const trajet = event.active.data.current?.trajet as Trajet; + setDraggedTrajet(trajet); + }; + + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + setActiveId(null); + setDraggedTrajet(null); + + if (!over || !active.data.current?.trajet) { + return; + } + + const trajet = active.data.current.trajet as Trajet; + + // Vérifier que le trajet peut être déplacé + if (trajet.statut === 'Annulé' || trajet.statut === 'Validé' || trajet.statut === 'Terminé') { + return; + } + + const targetDayId = over.id as string; + + // Extraire la date de la cible (format: "day-2026-01-23") + if (typeof targetDayId === 'string' && targetDayId.startsWith('day-')) { + const dateStr = targetDayId.replace('day-', ''); + const [year, month, day] = dateStr.split('-').map(Number); + + // Vérifier que les composants de date sont valides + if (isNaN(year) || isNaN(month) || isNaN(day)) { + return; + } + + // Conserver l'heure du trajet original, changer seulement la date + const originalDate = new Date(trajet.date); + + // Créer la nouvelle date avec les composants locaux pour correspondre exactement au calendrier + // Note: month - 1 car les mois sont indexés à partir de 0 en JavaScript + // On crée la date directement avec l'heure originale mais la date cible + // Pour éviter les problèmes de fuseau horaire, on crée d'abord à midi puis on ajuste + const newDate = new Date(year, month - 1, day, 12, 0, 0, 0); + newDate.setHours(originalDate.getHours(), originalDate.getMinutes(), originalDate.getSeconds(), originalDate.getMilliseconds()); + + // Vérifier que la date a changé + const originalDateOnly = new Date(originalDate.getFullYear(), originalDate.getMonth(), originalDate.getDate()); + const newDateOnly = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate()); + + if (originalDateOnly.getTime() === newDateOnly.getTime()) { + return; // Même jour, pas besoin de mettre à jour + } + + // Mettre à jour le trajet + try { + const response = await fetch(`/api/trajets/${trajet.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + date: newDate.toISOString(), + }), + }); + + if (response.ok) { + // Utiliser fetchTrajets sans loading pour éviter de vider les trajets pendant le chargement + await fetchTrajets(false); + } else { + const error = await response.json(); + alert(error.error || 'Erreur lors du déplacement du trajet'); + } + } catch (error) { + console.error('Erreur lors du déplacement:', error); + alert('Erreur lors du déplacement du trajet'); + } + } + }; + const goToPreviousMonth = () => { setCurrentDate(new Date(year, month - 1, 1)); }; @@ -162,71 +399,80 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP
Chargement...
) : ( <> - {/* Grille du calendrier */} -
- {/* En-têtes des jours */} - {days.map((day) => ( -
- {day} -
- ))} + {/* Grille du calendrier avec drag and drop */} + +
+ {/* En-têtes des jours */} + {days.map((day) => ( +
+ {day} +
+ ))} - {/* Jours du calendrier */} - {calendarDays.map((date, index) => { - if (!date) { - return
; - } + {/* Jours du calendrier */} + {calendarDays.map((date, index) => { + if (!date) { + return
; + } - const trajetsDuJour = getTrajetsForDate(date); - const isToday = - date.getDate() === new Date().getDate() && - date.getMonth() === new Date().getMonth() && - date.getFullYear() === new Date().getFullYear(); - const isSelected = - selectedDate && - date.getDate() === selectedDate.getDate() && - date.getMonth() === selectedDate.getMonth() && - date.getFullYear() === selectedDate.getFullYear(); + const trajetsDuJour = getTrajetsForDate(date); + const isToday = + date.getDate() === new Date().getDate() && + date.getMonth() === new Date().getMonth() && + date.getFullYear() === new Date().getFullYear(); + const isSelected = + selectedDate && + date.getDate() === selectedDate.getDate() && + date.getMonth() === selectedDate.getMonth() && + date.getFullYear() === selectedDate.getFullYear(); - return ( - - ); - })} -
+ {trajetsDuJour.length > 0 && ( +
+ {trajetsDuJour.slice(0, 3).map((trajet) => ( + + ))} + {trajetsDuJour.length > 3 && ( +
+ +{trajetsDuJour.length - 3} +
+ )} +
+ )} + + ); + })} +
+ + {/* Overlay pour l'élément en cours de drag */} + + {draggedTrajet ? ( +
+ + {draggedTrajet.chauffeur + ? `${draggedTrajet.chauffeur.prenom.charAt(0)}${draggedTrajet.chauffeur.nom.charAt(0)}` + : '?'} + + + {formatTime(draggedTrajet.date)} +
+ ) : null} +
+ {/* Détails des trajets du jour sélectionné */} {selectedDate && selectedTrajets.length > 0 && ( @@ -235,68 +481,81 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP Trajets du {formatDate(selectedDate)}
- {selectedTrajets.map((trajet) => ( -
-
-
-
- - {trajet.adherent.prenom} {trajet.adherent.nom} - - - {formatTime(trajet.date)} - - - {trajet.statut} - -
-
- Départ: {trajet.adresseDepart} -
-
- Arrivée: {trajet.adresseArrivee} -
- {trajet.chauffeur ? ( -
- - - - - Chauffeur: {trajet.chauffeur.prenom} {trajet.chauffeur.nom} + {selectedTrajets.map((trajet) => { + const getStatutColor = (statut: string) => { + switch (statut) { + case 'Validé': + return 'bg-purple-100 text-purple-700 border-purple-200'; + case 'Terminé': + return 'bg-green-100 text-green-700 border-green-200'; + case 'En cours': + return 'bg-blue-100 text-blue-700 border-blue-200'; + case 'Annulé': + return 'bg-red-100 text-red-700 border-red-200'; + default: + return 'bg-gray-100 text-gray-700 border-gray-200'; + } + }; + + return ( + + ); + })}
)} @@ -308,6 +567,18 @@ export default function CalendrierTrajets({ refreshTrigger }: CalendrierTrajetsP )} )} + + {/* Modal de détails du trajet */} + {selectedTrajet && ( + setSelectedTrajet(null)} + onUpdate={() => { + fetchTrajets(); + setSelectedTrajet(null); + }} + /> + )}
); } diff --git a/components/TrajetDetailModal.tsx b/components/TrajetDetailModal.tsx new file mode 100644 index 0000000..48be88a --- /dev/null +++ b/components/TrajetDetailModal.tsx @@ -0,0 +1,316 @@ +'use client'; + +import { useState } from 'react'; +import TrajetForm from './TrajetForm'; +import ValidationModal from './ValidationModal'; + +interface Trajet { + id: string; + date: string; + adresseDepart: string; + adresseArrivee: string; + commentaire?: string | null; + statut: string; + adherent: { + id: string; + nom: string; + prenom: string; + telephone: string; + email: string; + }; + chauffeur?: { + id: string; + nom: string; + prenom: string; + telephone: string; + } | null; +} + +interface TrajetDetailModalProps { + trajet: Trajet; + onClose: () => void; + onUpdate: () => void; +} + +export default function TrajetDetailModal({ trajet, onClose, onUpdate }: TrajetDetailModalProps) { + const [showEditForm, setShowEditForm] = useState(false); + const [showValidationModal, setShowValidationModal] = useState(false); + const [loading, setLoading] = useState(false); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric', + }); + }; + + const formatTime = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); + }; + + const handleCancel = async () => { + if (!confirm('Êtes-vous sûr de vouloir annuler ce trajet ?')) { + return; + } + + setLoading(true); + try { + const response = await fetch(`/api/trajets/${trajet.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + statut: 'Annulé', + }), + }); + + if (response.ok) { + onUpdate(); + onClose(); + } else { + const error = await response.json(); + alert(error.error || 'Erreur lors de l\'annulation du trajet'); + } + } catch (error) { + console.error('Erreur lors de l\'annulation:', error); + alert('Erreur lors de l\'annulation du trajet'); + } finally { + setLoading(false); + } + }; + + const getInitials = (nom: string, prenom: string) => { + return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase(); + }; + + const getStatutColor = (statut: string) => { + switch (statut) { + case 'Terminé': + return 'bg-green-100 text-green-700 border-green-200'; + case 'En cours': + return 'bg-blue-100 text-blue-700 border-blue-200'; + case 'Annulé': + return 'bg-red-100 text-red-700 border-red-200'; + case 'Validé': + return 'bg-purple-100 text-purple-700 border-purple-200'; + default: + return 'bg-gray-100 text-gray-700 border-gray-200'; + } + }; + + if (showEditForm) { + return ( + setShowEditForm(false)} + onSuccess={() => { + onUpdate(); + setShowEditForm(false); + onClose(); + }} + trajetToEdit={trajet} + /> + ); + } + + if (showValidationModal) { + return ( + setShowValidationModal(false)} + onSuccess={() => { + onUpdate(); + setShowValidationModal(false); + onClose(); + }} + /> + ); + } + + return ( +
+
+ {/* Header */} +
+
+
+

Détails du trajet

+

Informations complètes du trajet

+
+ +
+
+ + {/* Content */} +
+
+ {/* Statut */} +
+ Statut + + {trajet.statut} + +
+ + {/* Adhérent */} +
+ +
+
+ {getInitials(trajet.adherent.nom, trajet.adherent.prenom)} +
+
+
+ {trajet.adherent.prenom} {trajet.adherent.nom} +
+
{trajet.adherent.email}
+
{trajet.adherent.telephone}
+
+
+
+ + {/* Chauffeur */} + {trajet.chauffeur ? ( +
+ +
+
+ {getInitials(trajet.chauffeur.nom, trajet.chauffeur.prenom)} +
+
+
+ {trajet.chauffeur.prenom} {trajet.chauffeur.nom} +
+
{trajet.chauffeur.telephone}
+
+
+
+ ) : ( +
+ +
+
+ + + + Aucun chauffeur assigné +
+
+
+ )} + + {/* Date et heure */} +
+
+ +
+ + + + {formatDate(trajet.date)} +
+
+
+ +
+ + + + {formatTime(trajet.date)} +
+
+
+ + {/* Adresses */} +
+ +
+
+
+ A +
+ {trajet.adresseDepart} +
+
+
+ +
+ +
+
+
+ B +
+ {trajet.adresseArrivee} +
+
+
+ + {/* Commentaire */} + {trajet.commentaire && ( +
+ +
+

{trajet.commentaire}

+
+
+ )} +
+
+ + {/* Footer */} +
+
+ +
+ + {trajet.chauffeur && trajet.statut === 'Planifié' && ( + + )} +
+
+
+
+
+ ); +} diff --git a/components/TrajetForm.tsx b/components/TrajetForm.tsx index 37e2255..eb9dd93 100644 --- a/components/TrajetForm.tsx +++ b/components/TrajetForm.tsx @@ -24,9 +24,19 @@ interface Chauffeur { interface TrajetFormProps { onClose: () => void; onSuccess: () => void; + trajetToEdit?: { + id: string; + date: string; + adresseDepart: string; + adresseArrivee: string; + commentaire?: string | null; + statut: string; + adherentId: string; + chauffeurId?: string | null; + }; } -export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) { +export default function TrajetForm({ onClose, onSuccess, trajetToEdit }: TrajetFormProps) { const [loading, setLoading] = useState(false); const [adherents, setAdherents] = useState([]); const [chauffeurs, setChauffeurs] = useState([]); @@ -38,20 +48,20 @@ export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) { const chauffeurDropdownRef = useRef(null); const [formData, setFormData] = useState({ - adherentId: '', + adherentId: trajetToEdit?.adherentId || '', adherentNom: '', adherentPrenom: '', adherentAdresse: '', adherentTelephone: '', - chauffeurId: '', + chauffeurId: trajetToEdit?.chauffeurId || '', chauffeurNom: '', chauffeurPrenom: '', chauffeurTelephone: '', - date: '', - heure: '', - adresseDepart: '', - adresseArrivee: '', - commentaire: '', + date: trajetToEdit ? new Date(trajetToEdit.date).toISOString().split('T')[0] : '', + heure: trajetToEdit ? new Date(trajetToEdit.date).toTimeString().slice(0, 5) : '', + adresseDepart: trajetToEdit?.adresseDepart || '', + adresseArrivee: trajetToEdit?.adresseArrivee || '', + commentaire: trajetToEdit?.commentaire || '', }); useEffect(() => { @@ -59,6 +69,49 @@ export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) { fetchChauffeurs(); }, []); + useEffect(() => { + // Si on modifie un trajet, charger les données de l'adhérent et du chauffeur + if (trajetToEdit) { + if (trajetToEdit.adherentId) { + fetch(`/api/adherents/${trajetToEdit.adherentId}`) + .then(res => res.json()) + .then(data => { + if (data) { + setFormData(prev => ({ + ...prev, + adherentId: data.id, + adherentNom: data.nom, + adherentPrenom: data.prenom, + adherentAdresse: data.adresse, + adherentTelephone: data.telephone, + adresseDepart: data.adresse, + })); + setSearchAdherent(`${data.prenom} ${data.nom}`); + } + }) + .catch(console.error); + } + + if (trajetToEdit.chauffeurId) { + fetch(`/api/chauffeurs/${trajetToEdit.chauffeurId}`) + .then(res => res.json()) + .then(data => { + if (data) { + setFormData(prev => ({ + ...prev, + chauffeurId: data.id, + chauffeurNom: data.nom, + chauffeurPrenom: data.prenom, + chauffeurTelephone: data.telephone, + })); + setSearchChauffeur(`${data.prenom} ${data.nom}`); + } + }) + .catch(console.error); + } + } + }, [trajetToEdit]); + // Fermer les dropdowns quand on clique en dehors useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -160,8 +213,11 @@ export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) { ? new Date(`${formData.date}T09:00`).toISOString() : new Date().toISOString(); - const response = await fetch('/api/trajets', { - method: 'POST', + const url = trajetToEdit ? `/api/trajets/${trajetToEdit.id}` : '/api/trajets'; + const method = trajetToEdit ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, headers: { 'Content-Type': 'application/json', }, @@ -170,7 +226,7 @@ export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) { adresseDepart: formData.adresseDepart, adresseArrivee: formData.adresseArrivee, commentaire: formData.commentaire || null, - statut: 'Planifié', + statut: trajetToEdit?.statut || 'Planifié', adherentId: formData.adherentId, chauffeurId: formData.chauffeurId || null, }), @@ -181,11 +237,11 @@ export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) { onClose(); } else { const error = await response.json(); - alert(`Erreur: ${error.error || 'Erreur lors de la création du trajet'}`); + alert(`Erreur: ${error.error || `Erreur lors de la ${trajetToEdit ? 'modification' : 'création'} du trajet`}`); } } catch (error) { - console.error('Erreur lors de la création du trajet:', error); - alert('Erreur lors de la création du trajet'); + console.error(`Erreur lors de la ${trajetToEdit ? 'modification' : 'création'} du trajet:`, error); + alert(`Erreur lors de la ${trajetToEdit ? 'modification' : 'création'} du trajet`); } finally { setLoading(false); } @@ -202,8 +258,12 @@ export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) {
-

Nouveau trajet

-

Créez un nouveau trajet pour un adhérent

+

+ {trajetToEdit ? 'Modifier le trajet' : 'Nouveau trajet'} +

+

+ {trajetToEdit ? 'Modifiez les informations du trajet' : 'Créez un nouveau trajet pour un adhérent'} +

diff --git a/components/ValidationModal.tsx b/components/ValidationModal.tsx new file mode 100644 index 0000000..b1d6a49 --- /dev/null +++ b/components/ValidationModal.tsx @@ -0,0 +1,259 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +interface Trajet { + id: string; + date: string; + adresseDepart: string; + adresseArrivee: string; + chauffeur?: { + id: string; + nom: string; + prenom: string; + } | null; +} + +interface ValidationModalProps { + trajet: Trajet; + onClose: () => void; + onSuccess: () => void; +} + +export default function ValidationModal({ trajet, onClose, onSuccess }: ValidationModalProps) { + const [loading, setLoading] = useState(false); + const [dureeTrajet, setDureeTrajet] = useState(null); + + // Calculer la durée du trajet en heures + const calculateDuration = async () => { + if (!trajet.adresseDepart || !trajet.adresseArrivee) { + return; + } + + try { + // Géocoder l'adresse de départ + const departResponse = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(trajet.adresseDepart)}&limit=1&countrycodes=fr`, + { + headers: { + 'User-Agent': 'MAD Platform', + 'Accept-Language': 'fr-FR,fr;q=0.9', + }, + } + ); + + if (!departResponse.ok) { + return; + } + + // Attendre avant la deuxième requête + await new Promise(resolve => setTimeout(resolve, 1000)); + + const arriveeResponse = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(trajet.adresseArrivee)}&limit=1&countrycodes=fr`, + { + headers: { + 'User-Agent': 'MAD Platform', + 'Accept-Language': 'fr-FR,fr;q=0.9', + }, + } + ); + + if (departResponse.ok && arriveeResponse.ok) { + const [departData, arriveeData] = await Promise.all([ + departResponse.json(), + arriveeResponse.json(), + ]); + + if (departData.length > 0 && arriveeData.length > 0) { + const lat1 = parseFloat(departData[0].lat); + const lon1 = parseFloat(departData[0].lon); + const lat2 = parseFloat(arriveeData[0].lat); + const lon2 = parseFloat(arriveeData[0].lon); + + // Calcul de la distance (Haversine) + const R = 6371; // Rayon de la Terre en km + const dLat = ((lat2 - lat1) * Math.PI) / 180; + const dLon = ((lon2 - lon1) * Math.PI) / 180; + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos((lat1 * Math.PI) / 180) * + Math.cos((lat2 * Math.PI) / 180) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const distance = R * c; // Distance en km + + // Estimation du temps : vitesse moyenne de 50 km/h en ville + // On multiplie par 1.3 pour tenir compte des détours + const distanceWithDetour = distance * 1.3; + const vitesseMoyenne = 50; // km/h + const dureeEnHeures = distanceWithDetour / vitesseMoyenne; + + setDureeTrajet(Math.round(dureeEnHeures * 10) / 10); // Arrondir à 1 décimale + } + } + } catch (error) { + console.error('Erreur lors du calcul de la durée:', error); + } + }; + + useEffect(() => { + calculateDuration(); + }, []); + + const handleValidate = async () => { + if (!trajet.chauffeur || !dureeTrajet) { + alert('Impossible de calculer la durée du trajet'); + return; + } + + setLoading(true); + try { + const response = await fetch(`/api/trajets/${trajet.id}/validate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + dureeHeures: dureeTrajet, + }), + }); + + if (response.ok) { + onSuccess(); + onClose(); + } else { + const error = await response.json(); + alert(error.error || 'Erreur lors de la validation du trajet'); + } + } catch (error) { + console.error('Erreur lors de la validation:', error); + alert('Erreur lors de la validation du trajet'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+
+
+

Valider le trajet

+

Confirmer la validation du trajet

+
+ +
+
+ + {/* Content */} +
+
+
+
+ + + +
+

+ En validant ce trajet, les heures seront déduites du contrat du chauffeur. +

+

+ Cette action ne peut pas être annulée. +

+
+
+
+ + {trajet.chauffeur && ( +
+ +
+ + {trajet.chauffeur.prenom} {trajet.chauffeur.nom} + +
+
+ )} + + {dureeTrajet !== null ? ( +
+ +
+
+ Temps estimé + + {dureeTrajet.toFixed(1)}h + +
+
+

+ {dureeTrajet.toFixed(1)} heure{dureeTrajet >= 2 ? 's' : ''} sera{dureeTrajet < 2 ? '' : 'ont'} déduite{dureeTrajet < 2 ? '' : 's'} des heures disponibles du chauffeur. +

+
+
+
+ ) : ( +
+
+ + + + + Calcul de la durée du trajet... +
+
+ )} +
+
+ + {/* Footer */} +
+
+ + +
+
+
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 9b9b3d5..2f2b695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "platform", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@prisma/client": "^5.19.1", "@types/leaflet": "^1.9.21", "bcryptjs": "^2.4.3", @@ -55,6 +58,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", diff --git a/package.json b/package.json index 1c8efa0..cd50a33 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "setup": "tsx scripts/setup.ts" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@prisma/client": "^5.19.1", "@types/leaflet": "^1.9.21", "bcryptjs": "^2.4.3", diff --git a/prisma/dev.db b/prisma/dev.db index 519c9c8..34d5678 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ