From c9f6b53c13502ff3f0f6ed133ed1c0b5e10d64fe Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 21 Jan 2026 17:34:48 +0100 Subject: [PATCH] Added Calendar Page --- app/api/trajets/[id]/route.ts | 134 +++++++ app/api/trajets/route.ts | 129 +++++++ app/dashboard/calendrier/page.tsx | 27 ++ app/globals.css | 53 +++ components/AddressAutocomplete.tsx | 154 ++++++++ components/CalendrierPageContent.tsx | 27 ++ components/CalendrierTrajets.tsx | 313 +++++++++++++++++ components/ListeTrajets.tsx | 343 ++++++++++++++++++ components/TrajetForm.tsx | 501 +++++++++++++++++++++++++++ components/TrajetMap.tsx | 429 +++++++++++++++++++++++ package-lock.json | 63 +++- package.json | 7 +- prisma/dev.db | Bin 40960 -> 49152 bytes prisma/schema.prisma | 17 + 14 files changed, 2188 insertions(+), 9 deletions(-) create mode 100644 app/api/trajets/[id]/route.ts create mode 100644 app/api/trajets/route.ts create mode 100644 app/dashboard/calendrier/page.tsx create mode 100644 components/AddressAutocomplete.tsx create mode 100644 components/CalendrierPageContent.tsx create mode 100644 components/CalendrierTrajets.tsx create mode 100644 components/ListeTrajets.tsx create mode 100644 components/TrajetForm.tsx create mode 100644 components/TrajetMap.tsx diff --git a/app/api/trajets/[id]/route.ts b/app/api/trajets/[id]/route.ts new file mode 100644 index 0000000..df8e403 --- /dev/null +++ b/app/api/trajets/[id]/route.ts @@ -0,0 +1,134 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Récupérer un trajet spécifique +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const trajet = await prisma.trajet.findUnique({ + where: { id: params.id }, + include: { + adherent: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + email: true, + adresse: true, + }, + }, + chauffeur: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + email: true, + }, + }, + }, + }); + + if (!trajet) { + return NextResponse.json({ error: 'Trajet non trouvé' }, { status: 404 }); + } + + return NextResponse.json(trajet); + } catch (error) { + console.error('Erreur lors de la récupération du trajet:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// PUT - Mettre à jour un trajet +export async function PUT( + 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 { date, adresseDepart, adresseArrivee, commentaire, statut, adherentId, chauffeurId } = body; + + const trajet = await prisma.trajet.update({ + where: { id: params.id }, + data: { + ...(date && { date: new Date(date) }), + ...(adresseDepart && { adresseDepart }), + ...(adresseArrivee && { adresseArrivee }), + ...(commentaire !== undefined && { commentaire }), + ...(statut && { statut }), + ...(adherentId && { adherentId }), + ...(chauffeurId !== undefined && { chauffeurId }), + }, + include: { + adherent: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + email: true, + }, + }, + chauffeur: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + }, + }, + }, + }); + + return NextResponse.json(trajet); + } catch (error) { + console.error('Erreur lors de la mise à jour du trajet:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// DELETE - Supprimer un trajet +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + await prisma.trajet.delete({ + where: { id: params.id }, + }); + + return NextResponse.json({ message: 'Trajet supprimé' }); + } catch (error) { + console.error('Erreur lors de la suppression du trajet:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/trajets/route.ts b/app/api/trajets/route.ts new file mode 100644 index 0000000..05dd18b --- /dev/null +++ b/app/api/trajets/route.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getCurrentUser } from '@/lib/auth'; + +// GET - Liste tous les trajets avec leurs relations +export async function GET(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const searchParams = request.nextUrl.searchParams; + const limit = searchParams.get('limit'); + const startDate = searchParams.get('startDate'); + const endDate = searchParams.get('endDate'); + + const where: any = {}; + + // Filtrer par date si fourni + if (startDate || endDate) { + where.date = {}; + if (startDate) { + where.date.gte = new Date(startDate); + } + if (endDate) { + where.date.lte = new Date(endDate); + } + } + + // Si limit est fourni sans filtre de date, trier par date de création (derniers créés) + // Sinon, trier par date du trajet (pour le calendrier) + const orderBy = limit && !startDate && !endDate + ? { createdAt: 'desc' as const } + : { date: 'asc' as const }; + + const trajets = await prisma.trajet.findMany({ + where, + include: { + adherent: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + email: true, + }, + }, + chauffeur: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + }, + }, + }, + orderBy, + take: limit ? parseInt(limit) : undefined, + }); + + return NextResponse.json(trajets); + } catch (error) { + console.error('Erreur lors de la récupération des trajets:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// POST - Créer un nouveau trajet +export async function POST(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const body = await request.json(); + const { date, adresseDepart, adresseArrivee, commentaire, statut, adherentId, chauffeurId } = body; + + if (!date || !adresseDepart || !adresseArrivee || !adherentId) { + return NextResponse.json( + { error: 'Les champs date, adresse de départ, adresse d\'arrivée et adhérent sont requis' }, + { status: 400 } + ); + } + + const trajet = await prisma.trajet.create({ + data: { + date: new Date(date), + adresseDepart, + adresseArrivee, + commentaire: commentaire || null, + statut: statut || 'Planifié', + adherentId, + chauffeurId: chauffeurId || null, + }, + include: { + adherent: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + email: true, + }, + }, + chauffeur: { + select: { + id: true, + nom: true, + prenom: true, + telephone: true, + }, + }, + }, + }); + + return NextResponse.json(trajet, { status: 201 }); + } catch (error) { + console.error('Erreur lors de la création du trajet:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/dashboard/calendrier/page.tsx b/app/dashboard/calendrier/page.tsx new file mode 100644 index 0000000..b579e5a --- /dev/null +++ b/app/dashboard/calendrier/page.tsx @@ -0,0 +1,27 @@ +import { redirect } from 'next/navigation'; +import { getCurrentUser } from '@/lib/auth'; +import DashboardLayout from '@/components/DashboardLayout'; +import CalendrierPageContent from '@/components/CalendrierPageContent'; + +export default async function CalendrierPage() { + const user = await getCurrentUser(); + + if (!user) { + redirect('/login'); + } + + return ( + +
+

+ Calendrier +

+

+ Gestion des trajets et planning des chauffeurs +

+ + +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index 3b8de5b..168df16 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,6 +2,59 @@ @tailwind components; @tailwind utilities; +@import 'leaflet/dist/leaflet.css'; + +/* Styles personnalisés pour Leaflet */ +.leaflet-container { + font-family: inherit; +} + +.leaflet-popup-content-wrapper { + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +.leaflet-popup-tip { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.leaflet-control-zoom { + border: none !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + border-radius: 8px !important; + overflow: hidden; +} + +.leaflet-control-zoom a { + background-color: white !important; + color: #374151 !important; + border: none !important; + width: 36px !important; + height: 36px !important; + line-height: 36px !important; + font-size: 18px !important; + transition: all 0.2s !important; +} + +.leaflet-control-zoom a:hover { + background-color: #f3f4f6 !important; + color: #6B46C1 !important; +} + +.leaflet-control-zoom-in { + border-bottom: 1px solid #e5e7eb !important; +} + +.custom-marker-depart, +.custom-marker-arrivee { + background: transparent !important; + border: none !important; +} + +.custom-popup .leaflet-popup-content { + margin: 0 !important; +} + :root { --background: #ffffff; --foreground: #171717; diff --git a/components/AddressAutocomplete.tsx b/components/AddressAutocomplete.tsx new file mode 100644 index 0000000..a854065 --- /dev/null +++ b/components/AddressAutocomplete.tsx @@ -0,0 +1,154 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; + +interface AddressSuggestion { + display_name: string; + lat: string; + lon: string; +} + +interface AddressAutocompleteProps { + value: string; + onChange: (address: string) => void; + placeholder?: string; + required?: boolean; +} + +export default function AddressAutocomplete({ + value, + onChange, + placeholder = 'Rechercher une adresse...', + required = false, +}: AddressAutocompleteProps) { + const [suggestions, setSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); + const [loading, setLoading] = useState(false); + const wrapperRef = useRef(null); + const timeoutRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setShowSuggestions(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const searchAddresses = async (query: string) => { + if (query.length < 3) { + setSuggestions([]); + return; + } + + setLoading(true); + try { + // Annuler la requête précédente si elle existe + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + // Attendre un peu avant de faire la requête (debounce) + timeoutRef.current = setTimeout(async () => { + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&addressdetails=1&countrycodes=fr`, + { + headers: { + 'User-Agent': 'MAD Platform', + 'Accept-Language': 'fr-FR,fr;q=0.9', + }, + } + ); + + if (response.ok) { + const data = await response.json(); + setSuggestions(data); + setShowSuggestions(true); + } + } catch (error) { + console.error('Erreur lors de la recherche d\'adresses:', error); + } finally { + setLoading(false); + } + }, 300); + } catch (error) { + console.error('Erreur:', error); + setLoading(false); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + onChange(newValue); + searchAddresses(newValue); + }; + + const handleSelectSuggestion = (suggestion: AddressSuggestion) => { + onChange(suggestion.display_name); + setShowSuggestions(false); + setSuggestions([]); + }; + + return ( +
+
+ { + if (suggestions.length > 0) { + setShowSuggestions(true); + } + }} + placeholder={placeholder} + required={required} + className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> + {loading && ( +
+ + + + +
+ )} +
+ + {showSuggestions && suggestions.length > 0 && ( +
+ {suggestions.map((suggestion, index) => ( + + ))} +
+ )} +
+ ); +} diff --git a/components/CalendrierPageContent.tsx b/components/CalendrierPageContent.tsx new file mode 100644 index 0000000..dc01fa3 --- /dev/null +++ b/components/CalendrierPageContent.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { useState } from 'react'; +import CalendrierTrajets from './CalendrierTrajets'; +import ListeTrajets from './ListeTrajets'; + +export default function CalendrierPageContent() { + const [refreshTrigger, setRefreshTrigger] = useState(0); + + const handleTrajetCreated = () => { + setRefreshTrigger((prev) => prev + 1); + }; + + return ( +
+ {/* Calendrier - Prend 2 colonnes sur grand écran */} +
+ +
+ + {/* Liste des derniers trajets - Prend 1 colonne sur grand écran */} +
+ +
+
+ ); +} diff --git a/components/CalendrierTrajets.tsx b/components/CalendrierTrajets.tsx new file mode 100644 index 0000000..8212d2c --- /dev/null +++ b/components/CalendrierTrajets.tsx @@ -0,0 +1,313 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +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 CalendrierTrajetsProps { + refreshTrigger?: number; +} + +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 month = currentDate.getMonth(); + const year = currentDate.getFullYear(); + + const firstDayOfMonth = new Date(year, month, 1); + const lastDayOfMonth = new Date(year, month + 1, 0); + const daysInMonth = lastDayOfMonth.getDate(); + const startingDayOfWeek = firstDayOfMonth.getDay(); + + // Ajuster pour que lundi soit le premier jour (0 = lundi) + const adjustedStartingDay = startingDayOfWeek === 0 ? 6 : startingDayOfWeek - 1; + + useEffect(() => { + fetchTrajets(); + }, [currentDate, refreshTrigger]); + + const fetchTrajets = async () => { + setLoading(true); + try { + const startDate = new Date(year, month, 1).toISOString(); + const endDate = new Date(year, month + 1, 0, 23, 59, 59).toISOString(); + + const response = await fetch( + `/api/trajets?startDate=${startDate}&endDate=${endDate}` + ); + if (response.ok) { + const data = await response.json(); + setTrajets(data); + } + } catch (error) { + console.error('Erreur lors du chargement des trajets:', error); + } finally { + setLoading(false); + } + }; + + const getTrajetsForDate = (date: Date): Trajet[] => { + const dateStr = date.toISOString().split('T')[0]; + return trajets.filter((trajet) => { + const trajetDate = new Date(trajet.date).toISOString().split('T')[0]; + return trajetDate === dateStr; + }); + }; + + const handleDateClick = (date: Date) => { + setSelectedDate(date); + const trajetsDuJour = getTrajetsForDate(date); + setSelectedTrajets(trajetsDuJour); + }; + + const goToPreviousMonth = () => { + setCurrentDate(new Date(year, month - 1, 1)); + }; + + const goToNextMonth = () => { + setCurrentDate(new Date(year, month + 1, 1)); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const formatDate = (date: Date) => { + 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 days = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']; + const calendarDays = []; + + // Ajouter les jours vides du début + for (let i = 0; i < adjustedStartingDay; i++) { + calendarDays.push(null); + } + + // Ajouter les jours du mois + for (let day = 1; day <= daysInMonth; day++) { + calendarDays.push(new Date(year, month, day)); + } + + return ( +
+ {/* En-tête du calendrier */} +
+

+ {currentDate.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })} +

+
+ + + +
+
+ + {loading ? ( +
Chargement...
+ ) : ( + <> + {/* Grille du calendrier */} +
+ {/* En-têtes des jours */} + {days.map((day) => ( +
+ {day} +
+ ))} + + {/* 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(); + + return ( + + ); + })} +
+ + {/* Détails des trajets du jour sélectionné */} + {selectedDate && selectedTrajets.length > 0 && ( +
+

+ 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} + +
+ ) : ( +
+ + + + + Aucun chauffeur assigné + +
+ )} + {trajet.commentaire && ( +
+ {trajet.commentaire} +
+ )} +
+
+
+ ))} +
+
+ )} + + {selectedDate && selectedTrajets.length === 0 && ( +
+ Aucun trajet prévu pour le {formatDate(selectedDate)} +
+ )} + + )} +
+ ); +} diff --git a/components/ListeTrajets.tsx b/components/ListeTrajets.tsx new file mode 100644 index 0000000..95cf6f2 --- /dev/null +++ b/components/ListeTrajets.tsx @@ -0,0 +1,343 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import TrajetForm from './TrajetForm'; + +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 ListeTrajetsProps { + onTrajetCreated?: () => void; +} + +export default function ListeTrajets({ onTrajetCreated }: ListeTrajetsProps) { + const [trajets, setTrajets] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [showFilters, setShowFilters] = useState(false); + const [filterStatut, setFilterStatut] = useState(''); + const [showTrajetForm, setShowTrajetForm] = useState(false); + + useEffect(() => { + fetchTrajets(); + }, []); + + const fetchTrajets = async () => { + setLoading(true); + try { + const response = await fetch('/api/trajets?limit=10'); + if (response.ok) { + const data = await response.json(); + // L'API retourne déjà les trajets triés par date de création (plus récents en premier) + setTrajets(data); + } + } catch (error) { + console.error('Erreur lors du chargement des trajets:', error); + } finally { + setLoading(false); + } + }; + + const filteredTrajets = trajets.filter((trajet) => { + const matchesSearch = + !search || + trajet.adherent.nom.toLowerCase().includes(search.toLowerCase()) || + trajet.adherent.prenom.toLowerCase().includes(search.toLowerCase()) || + trajet.adresseDepart.toLowerCase().includes(search.toLowerCase()) || + trajet.adresseArrivee.toLowerCase().includes(search.toLowerCase()) || + (trajet.chauffeur && + `${trajet.chauffeur.prenom} ${trajet.chauffeur.nom}` + .toLowerCase() + .includes(search.toLowerCase())); + + const matchesStatut = !filterStatut || trajet.statut === filterStatut; + + return matchesSearch && matchesStatut; + }); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); + }; + + const formatTime = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); + }; + + const getInitials = (nom: string, prenom: string) => { + return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase(); + }; + + return ( +
+ {/* Bloc d'actions */} +
+
+ {/* Barre de recherche */} +
+
+ + + +
+ setSearch(e.target.value)} + className="block w-full pl-10 pr-3 py-2.5 text-sm border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> +
+ + {/* Actions */} +
+ + +
+ + {/* Filtres */} + {showFilters && ( +
+
+ + + + + +
+
+ )} +
+
+ + {/* Liste des trajets */} +
+
+

+ Derniers trajets créés +

+ +
+ + {loading ? ( +
Chargement...
+ ) : filteredTrajets.length === 0 ? ( +
+ {trajets.length === 0 + ? 'Aucun trajet créé récemment' + : 'Aucun trajet ne correspond à votre recherche'} +
+ ) : ( +
+ {filteredTrajets.map((trajet) => ( +
+
+ {/* Avatar adhérent */} +
+ {getInitials(trajet.adherent.nom, trajet.adherent.prenom)} +
+ + {/* Informations principales */} +
+
+
+

+ {trajet.adherent.prenom} {trajet.adherent.nom} +

+
+ + + + + {formatDate(trajet.date)} + + + + + + {formatTime(trajet.date)} + + + {trajet.statut} + +
+
+
+ + {/* Adresses */} +
+
+ Départ:{' '} + {trajet.adresseDepart} +
+
+ Arrivée:{' '} + {trajet.adresseArrivee} +
+
+ + {/* Chauffeur */} +
+ {trajet.chauffeur ? ( +
+
+ {getInitials(trajet.chauffeur.nom, trajet.chauffeur.prenom)} +
+
+
Chauffeur
+
+ {trajet.chauffeur.prenom} {trajet.chauffeur.nom} +
+
+
+ ) : ( +
+ + + + + Aucun chauffeur assigné + +
+ )} +
+ + {/* Commentaire */} + {trajet.commentaire && ( +
+

{trajet.commentaire}

+
+ )} +
+
+
+ ))} +
+ )} +
+ + {/* Modal formulaire trajet */} + {showTrajetForm && ( + setShowTrajetForm(false)} + onSuccess={() => { + fetchTrajets(); + if (onTrajetCreated) { + onTrajetCreated(); + } + }} + /> + )} +
+ ); +} diff --git a/components/TrajetForm.tsx b/components/TrajetForm.tsx new file mode 100644 index 0000000..37e2255 --- /dev/null +++ b/components/TrajetForm.tsx @@ -0,0 +1,501 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import TrajetMap from './TrajetMap'; +import AddressAutocomplete from './AddressAutocomplete'; + +interface Adherent { + id: string; + nom: string; + prenom: string; + adresse: string; + telephone: string; + email: string; +} + +interface Chauffeur { + id: string; + nom: string; + prenom: string; + telephone: string; + email: string; +} + +interface TrajetFormProps { + onClose: () => void; + onSuccess: () => void; +} + +export default function TrajetForm({ onClose, onSuccess }: TrajetFormProps) { + const [loading, setLoading] = useState(false); + const [adherents, setAdherents] = useState([]); + const [chauffeurs, setChauffeurs] = useState([]); + const [searchAdherent, setSearchAdherent] = useState(''); + const [searchChauffeur, setSearchChauffeur] = useState(''); + const [showAdherentDropdown, setShowAdherentDropdown] = useState(false); + const [showChauffeurDropdown, setShowChauffeurDropdown] = useState(false); + const adherentDropdownRef = useRef(null); + const chauffeurDropdownRef = useRef(null); + + const [formData, setFormData] = useState({ + adherentId: '', + adherentNom: '', + adherentPrenom: '', + adherentAdresse: '', + adherentTelephone: '', + chauffeurId: '', + chauffeurNom: '', + chauffeurPrenom: '', + chauffeurTelephone: '', + date: '', + heure: '', + adresseDepart: '', + adresseArrivee: '', + commentaire: '', + }); + + useEffect(() => { + fetchAdherents(); + fetchChauffeurs(); + }, []); + + // Fermer les dropdowns quand on clique en dehors + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + adherentDropdownRef.current && + !adherentDropdownRef.current.contains(event.target as Node) + ) { + setShowAdherentDropdown(false); + } + if ( + chauffeurDropdownRef.current && + !chauffeurDropdownRef.current.contains(event.target as Node) + ) { + setShowChauffeurDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const fetchAdherents = async () => { + try { + const response = await fetch('/api/adherents'); + if (response.ok) { + const data = await response.json(); + setAdherents(data); + } + } catch (error) { + console.error('Erreur lors du chargement des adhérents:', error); + } + }; + + const fetchChauffeurs = async () => { + try { + const response = await fetch('/api/chauffeurs'); + if (response.ok) { + const data = await response.json(); + setChauffeurs(data); + } + } catch (error) { + console.error('Erreur lors du chargement des chauffeurs:', error); + } + }; + + const handleSelectAdherent = (adherent: Adherent) => { + setFormData({ + ...formData, + adherentId: adherent.id, + adherentNom: adherent.nom, + adherentPrenom: adherent.prenom, + adherentAdresse: adherent.adresse, + adherentTelephone: adherent.telephone, + adresseDepart: adherent.adresse, // Remplir automatiquement l'adresse de départ + }); + setSearchAdherent(`${adherent.prenom} ${adherent.nom}`); + setShowAdherentDropdown(false); + }; + + const handleSelectChauffeur = (chauffeur: Chauffeur) => { + setFormData({ + ...formData, + chauffeurId: chauffeur.id, + chauffeurNom: chauffeur.nom, + chauffeurPrenom: chauffeur.prenom, + chauffeurTelephone: chauffeur.telephone, + }); + setSearchChauffeur(`${chauffeur.prenom} ${chauffeur.nom}`); + setShowChauffeurDropdown(false); + }; + + const filteredAdherents = adherents.filter( + (a) => + !searchAdherent || + `${a.prenom} ${a.nom}`.toLowerCase().includes(searchAdherent.toLowerCase()) || + a.email.toLowerCase().includes(searchAdherent.toLowerCase()) || + a.telephone.includes(searchAdherent) + ); + + const filteredChauffeurs = chauffeurs.filter( + (c) => + !searchChauffeur || + `${c.prenom} ${c.nom}`.toLowerCase().includes(searchChauffeur.toLowerCase()) || + c.email.toLowerCase().includes(searchChauffeur.toLowerCase()) || + c.telephone.includes(searchChauffeur) + ); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + // Combiner date et heure + const dateTime = formData.date && formData.heure + ? new Date(`${formData.date}T${formData.heure}`).toISOString() + : formData.date + ? new Date(`${formData.date}T09:00`).toISOString() + : new Date().toISOString(); + + const response = await fetch('/api/trajets', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + date: dateTime, + adresseDepart: formData.adresseDepart, + adresseArrivee: formData.adresseArrivee, + commentaire: formData.commentaire || null, + statut: 'Planifié', + adherentId: formData.adherentId, + chauffeurId: formData.chauffeurId || null, + }), + }); + + if (response.ok) { + onSuccess(); + onClose(); + } else { + const error = await response.json(); + alert(`Erreur: ${error.error || 'Erreur lors de la 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'); + } finally { + setLoading(false); + } + }; + + const getInitials = (nom: string, prenom: string) => { + return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase(); + }; + + return ( +
+
+ {/* Header */} +
+
+
+

Nouveau trajet

+

Créez un nouveau trajet pour un adhérent

+
+ +
+
+ + {/* Content */} +
+ {/* Colonne gauche - Formulaire */} +
+
+ {/* Sélection adhérent */} +
+ +
+ { + setSearchAdherent(e.target.value); + setShowAdherentDropdown(true); + }} + onFocus={() => setShowAdherentDropdown(true)} + className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> + {showAdherentDropdown && filteredAdherents.length > 0 && ( +
+ {filteredAdherents.map((adherent) => ( + + ))} +
+ )} +
+ {formData.adherentId && ( +
+
+
+ {getInitials(formData.adherentNom, formData.adherentPrenom)} +
+
+
+ {formData.adherentPrenom} {formData.adherentNom} +
+
{formData.adherentTelephone}
+
{formData.adherentAdresse}
+
+
+
+ )} +
+ + {/* Sélection chauffeur */} +
+ +
+ { + setSearchChauffeur(e.target.value); + setShowChauffeurDropdown(true); + }} + onFocus={() => setShowChauffeurDropdown(true)} + className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> + {showChauffeurDropdown && filteredChauffeurs.length > 0 && ( +
+ {filteredChauffeurs.map((chauffeur) => ( + + ))} +
+ )} +
+ {formData.chauffeurId && ( +
+
+
+ {getInitials(formData.chauffeurNom, formData.chauffeurPrenom)} +
+
+
+ {formData.chauffeurPrenom} {formData.chauffeurNom} +
+
{formData.chauffeurTelephone}
+
+
+
+ )} +
+ + {/* Date et heure */} +
+
+ + setFormData({ ...formData, date: e.target.value })} + className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> +
+
+ + setFormData({ ...formData, heure: e.target.value })} + className="w-full px-4 py-2.5 border border-gray-300 rounded-lg text-gray-900 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent" + /> +
+
+ + {/* Adresse de départ */} +
+ + setFormData({ ...formData, adresseDepart: address })} + placeholder="Rechercher une adresse de départ..." + required + /> +
+ + {/* Adresse d'arrivée */} +
+ + setFormData({ ...formData, adresseArrivee: address })} + placeholder="Rechercher une adresse d'arrivée..." + required + /> +
+ + {/* Commentaire */} +
+ +