Compact search improvements + drivers map in the dashboard

This commit is contained in:
Arnaud Delcasse
2025-10-13 12:39:31 +02:00
parent 5d546c0efc
commit c7d263fded
10 changed files with 606 additions and 40 deletions

View File

@@ -120,6 +120,10 @@
class="p-2 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm border-gray-300 rounded-none">
</div>
{{if ne .ViewState.passengerid ""}}
<input type="hidden" name="passengerid" value="{{.ViewState.passengerid}}">
{{end}}
<button type="submit"
class="px-4 py-2 border border-transparent text-sm font-medium rounded-r-lg text-white bg-co-blue hover:bg-co-blue-dark focus:outline-none">
Rechercher
@@ -151,6 +155,10 @@
<input type="checkbox" x-model="filters.kb" class="rounded border-gray-300 text-co-blue focus:ring-co-blue">
<span class="ml-2 text-gray-600">Solutions locales</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="filters.vehicles" class="rounded border-gray-300 text-co-blue focus:ring-co-blue">
<span class="ml-2 text-gray-600">Véhicules</span>
</label>
</div>
<!-- Bouton Enregistrer -->
<a href="/app/journeys/save?departure={{json .ViewState.departure}}&destination={{json .ViewState.destination}}&departuredate={{.ViewState.departuredate}}&departuretime={{.ViewState.departuretime}}{{if ne .ViewState.passengerid ""}}&passengerid={{.ViewState.passengerid}}{{end}}"
@@ -163,7 +171,7 @@
</div>
<!-- Main Content: Results List (Left) + Map (Right) -->
<div class="flex overflow-hidden" style="height: calc(100% - 11rem);">
<div class="flex overflow-hidden" style="height: calc(100% - 8rem);">
<!-- Results List (Left Side) -->
<div class="w-96 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
{{if .ViewState.searched}}
@@ -306,6 +314,26 @@
</div>
</div>
{{end}}
<!-- Vehicles Results -->
{{if .ViewState.vehicles}}
<div x-show="filters.vehicles" @click="selectSolution('vehicles', 0)"
:class="selectedType === 'vehicles' ? 'bg-orange-50 border-l-4 border-co-orange' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-gray-900">Véhicules disponibles</span>
<span class="text-xs font-medium bg-orange-50 text-co-orange px-2 py-1 rounded-full">
{{len .ViewState.vehicles}} véhicule{{if gt (len .ViewState.vehicles) 1}}s{{end}}
</span>
</div>
<div class="flex items-center gap-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-50 text-co-orange">
{{$.IconSet.Icon "tabler-icons:car" "h-3 w-3"}}
<span class="ml-1">Véhicules partagés</span>
</span>
</div>
</div>
{{end}}
</div>
{{else}}
<div class="p-8 text-center text-gray-500">
@@ -638,7 +666,7 @@
<!-- Action Button -->
<div class="pt-4 border-t border-gray-200">
<a :href="'/app/organized-carpool/drivers/' + organizedCarpools[selectedOrganizedCarpoolIndex].driverId + '/journeys/' + organizedCarpools[selectedOrganizedCarpoolIndex].id + '{{if ne .ViewState.passengerid ""}}?passengerid={{.ViewState.passengerid}}{{end}}'"
<a :href="'/app/organized-carpool/drivers/' + organizedCarpools[selectedOrganizedCarpoolIndex].driverId + '/journeys/' + organizedCarpools[selectedOrganizedCarpoolIndex].id + (passengerId ? '?passengerid=' + passengerId : '')"
class="block w-full text-center bg-co-blue text-white px-4 py-2 rounded-2xl hover:bg-blue-700 transition-colors">
Organiser le covoiturage solidaire
</a>
@@ -771,6 +799,122 @@
</div>
</template>
<!-- Vehicles List (Middle Column) -->
<template x-if="selectedType === 'vehicles'">
<div x-transition
class="w-80 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<div class="p-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-sm font-semibold text-gray-900">Véhicules disponibles</h3>
</div>
<div class="divide-y divide-gray-200">
<template x-for="(vehicle, idx) in vehicles" :key="idx">
<div @click="selectVehicle(idx)"
:class="selectedVehicleIndex === idx ? 'bg-orange-50 border-l-4 border-co-orange' : 'hover:bg-gray-50'"
class="p-4 cursor-pointer transition-colors">
<div class="mb-2">
<div class="text-sm font-semibold text-gray-900" x-text="vehicle.name"></div>
<div class="text-xs text-gray-500 mt-1">
<span x-text="vehicle.type"></span>
<span x-show="vehicle.type === 'Voiture' && vehicle.automatic"> (boite auto)</span>
</div>
</div>
<div class="space-y-1 text-xs text-gray-600">
<div x-show="vehicle.licencePlate" class="flex items-center gap-2">
<span>Numéro:</span>
<span class="font-medium" x-text="vehicle.licencePlate"></span>
</div>
<div x-show="vehicle.address" class="flex items-start gap-2">
<span class="flex-shrink-0">Localisation:</span>
<span class="font-medium" x-text="vehicle.address"></span>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Vehicle Details (Third Column) -->
<template x-if="selectedType === 'vehicles' && selectedVehicleIndex !== null">
<div x-transition
class="w-96 flex-shrink-0 bg-white border-r border-gray-200 overflow-y-auto">
<template x-if="selectedVehicleIndex !== null && vehicles[selectedVehicleIndex]">
<div>
<!-- Header -->
<div class="p-4 border-b border-gray-200 bg-co-orange">
<div class="text-white">
<div class="text-xl font-bold mb-2" x-text="vehicles[selectedVehicleIndex].name"></div>
<div class="text-sm" x-text="vehicles[selectedVehicleIndex].type"></div>
</div>
</div>
<!-- Vehicle Details -->
<div class="p-4 space-y-4">
<!-- Vehicle Info -->
<div class="bg-orange-50 rounded-lg p-3 border border-orange-200">
<div class="text-xs font-semibold text-gray-700 mb-2">Informations du véhicule</div>
<div class="space-y-2 text-sm">
<div x-show="vehicles[selectedVehicleIndex].type" class="flex items-center justify-between">
<span class="text-gray-600">Type:</span>
<span class="font-medium text-gray-900">
<span x-text="vehicles[selectedVehicleIndex].type"></span>
<span x-show="vehicles[selectedVehicleIndex].type === 'Voiture' && vehicles[selectedVehicleIndex].automatic" class="text-gray-500"> (boite auto)</span>
</span>
</div>
<div x-show="vehicles[selectedVehicleIndex].licencePlate" class="flex items-center justify-between">
<span class="text-gray-600">Numéro:</span>
<span class="font-medium text-gray-900" x-text="vehicles[selectedVehicleIndex].licencePlate"></span>
</div>
</div>
</div>
<!-- Location -->
<div x-show="vehicles[selectedVehicleIndex].address" class="bg-orange-50 rounded-lg p-3 border border-orange-200">
<div class="text-xs font-semibold text-gray-700 mb-2">Localisation</div>
<div class="text-sm text-gray-900" x-text="vehicles[selectedVehicleIndex].address"></div>
</div>
<!-- Description -->
<div x-show="vehicles[selectedVehicleIndex].description" class="space-y-2">
<div class="text-xs font-semibold text-gray-700">Description</div>
<div class="text-sm text-gray-600" x-text="vehicles[selectedVehicleIndex].description"></div>
</div>
<!-- Optional Fields -->
<template x-if="vehicleOptionalFields && vehicleOptionalFields.length > 0">
<div class="bg-orange-50 rounded-lg p-3 border border-orange-200">
<div class="text-xs font-semibold text-gray-700 mb-2">Autres propriétés</div>
<div class="space-y-2 text-sm">
<template x-for="field in vehicleOptionalFields" :key="field.name">
<div x-show="vehicles[selectedVehicleIndex].data[field.name]" class="flex items-center justify-between">
<span class="text-gray-600" x-text="field.label"></span>
<span class="font-medium text-gray-900">
<template x-if="field.type === 'select'">
<span x-text="getSelectLabel(field, vehicles[selectedVehicleIndex].data[field.name])"></span>
</template>
<template x-if="field.type !== 'select'">
<span x-text="vehicles[selectedVehicleIndex].data[field.name]"></span>
</template>
</span>
</div>
</template>
</div>
</div>
</template>
<!-- Booking Button -->
<div class="pt-4">
<a :href="getVehicleBookingUrl()"
class="block w-full bg-co-orange hover:bg-co-orange-dark text-white text-center font-medium py-2 px-4 rounded-2xl transition-colors">
Réserver un véhicule
</a>
</div>
</div>
</div>
</template>
</div>
</template>
<!-- Map (Right Side) -->
<div class="flex-1 bg-gray-100 relative">
<!-- Driver Detail Panel -->
@@ -815,7 +959,7 @@
</div>
</div>
<div class="mt-4">
<a :href="'/app/solidarity-transport/drivers/' + solidarityJourneys[selectedDriverIndex].driverId + '/journeys/' + solidarityJourneys[selectedDriverIndex].id"
<a :href="'/app/solidarity-transport/drivers/' + solidarityJourneys[selectedDriverIndex].driverId + '/journeys/' + solidarityJourneys[selectedDriverIndex].id + (passengerId ? '?passengerid=' + passengerId : '')"
class="block w-full text-center px-4 py-2 bg-co-blue text-white rounded-lg hover:bg-co-blue-dark transition-colors">
Organiser le transport solidaire
</a>
@@ -971,11 +1115,31 @@ function compactJourneySearch() {
{{end}}
];
const vehicleOptionalFields = {{if .ViewState.vehicle_optional_fields}}{{json .ViewState.vehicle_optional_fields}}{{else}}[]{{end}};
const vehicles = [
{{range $index, $vehicle := .ViewState.vehicles}}
{
id: '{{$vehicle.ID}}',
name: {{if $vehicle.Data.name}}'{{$vehicle.Data.name}}'{{else}}'Véhicule'{{end}},
type: {{if $vehicle.Type}}'{{$vehicle.Type}}'{{else}}null{{end}},
automatic: {{if $vehicle.Data.automatic}}true{{else}}false{{end}},
licencePlate: {{if $vehicle.Data.licence_plate}}'{{$vehicle.Data.licence_plate}}'{{else}}null{{end}},
description: {{if $vehicle.Data.description}}'{{$vehicle.Data.description}}'{{else}}null{{end}},
address: {{if and $vehicle.Data.address $vehicle.Data.address.properties}}'{{$vehicle.Data.address.properties.label}}'{{else}}null{{end}},
location: {{if and $vehicle.Data.address $vehicle.Data.address.geometry $vehicle.Data.address.geometry.coordinates}}[{{index $vehicle.Data.address.geometry.coordinates 0}}, {{index $vehicle.Data.address.geometry.coordinates 1}}]{{else}}null{{end}},
distance: null,
data: {{json $vehicle.Data}}
},
{{end}}
];
return {
selectedType: null,
selectedIndex: null,
selectedDriverIndex: null,
selectedOrganizedCarpoolIndex: null,
selectedVehicleIndex: null,
map: null,
startMarker: null,
endMarker: null,
@@ -985,13 +1149,18 @@ function compactJourneySearch() {
organizedCarpools: organizedCarpools,
carpools: carpools,
kbSolutions: kbSolutions,
vehicles: vehicles,
vehicleOptionalFields: vehicleOptionalFields,
passengerId: '{{.ViewState.passengerid}}',
departureDate: '{{.ViewState.departuredate}}',
filters: {
transit: true,
solidarity: true,
organizedCarpool: true,
carpool: true,
kb: true
kb: true,
vehicles: true
},
init() {
@@ -1063,6 +1232,7 @@ function compactJourneySearch() {
this.selectedIndex = null;
this.selectedDriverIndex = null;
this.selectedOrganizedCarpoolIndex = null;
this.selectedVehicleIndex = null;
return;
}
@@ -1076,6 +1246,9 @@ function compactJourneySearch() {
if (type !== 'organized_carpool') {
this.selectedOrganizedCarpoolIndex = null;
}
if (type !== 'vehicles') {
this.selectedVehicleIndex = null;
}
// Update selection
this.selectedType = type;
@@ -1096,6 +1269,9 @@ function compactJourneySearch() {
this.displayCarpoolRoute(index);
} else if (type === 'kb') {
this.displayKBSolution(index);
} else if (type === 'vehicles') {
this.selectedVehicleIndex = null;
this.displayVehiclesMarkers();
}
}, 500);
},
@@ -1126,6 +1302,19 @@ function compactJourneySearch() {
}, 500);
},
selectVehicle(vehicleIndex) {
// Clear routes before selecting new vehicle
this.clearRoutes();
// Update vehicle selection
this.selectedVehicleIndex = vehicleIndex;
// Use setTimeout to ensure clearing completes before displaying new route
setTimeout(() => {
this.displayVehicleMarker(vehicleIndex);
}, 500);
},
clearRoutes() {
// Remove route markers
this.routeMarkers.forEach(marker => marker.remove());
@@ -1162,7 +1351,7 @@ function compactJourneySearch() {
const allCoords = [];
journey.legs.forEach((leg, legIndex) => {
const lineColor = leg.mode === 'WALK' ? '#9ca3af' : '#' + leg.color;
const lineColor = leg.mode === 'WALK' ? '#9ca3af' : (leg.color && leg.color !== '' ? '#' + leg.color : '#243887');
const lineWidth = leg.mode === 'WALK' ? 2 : 4;
const lineDasharray = leg.mode === 'WALK' ? [2, 2] : undefined;
@@ -1758,6 +1947,111 @@ function compactJourneySearch() {
padding: 50
});
}
},
displayVehiclesMarkers() {
if (!this.map || !this.map.loaded()) return;
const bounds = new maplibregl.LngLatBounds();
let hasMarkers = false;
// Display all vehicles as markers
this.vehicles.forEach((vehicle, idx) => {
if (!vehicle.location) return;
const el = document.createElement('div');
el.className = 'w-10 h-10 rounded-co bg-co-orange border-2 border-white shadow-md flex items-center justify-center cursor-pointer hover:scale-110 transition-transform';
el.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"></path>
<circle cx="7" cy="17" r="2"></circle>
<circle cx="17" cy="17" r="2"></circle>
</svg>
`;
const marker = new maplibregl.Marker({element: el})
.setLngLat(vehicle.location)
.setPopup(new maplibregl.Popup({offset: 25, className: 'rounded-lg'})
.setHTML(`
<div class="p-3">
<div class="font-semibold text-sm text-gray-900">${vehicle.name}</div>
${vehicle.type ? `<p class="text-xs text-gray-600 mt-1">${vehicle.type}</p>` : ''}
${vehicle.address ? `<p class="text-xs text-gray-500 mt-1">${vehicle.address}</p>` : ''}
</div>
`))
.addTo(this.map);
this.routeMarkers.push(marker);
bounds.extend(vehicle.location);
hasMarkers = true;
});
// Fit map to show all vehicle markers
if (hasMarkers) {
this.map.fitBounds(bounds, {
padding: 100,
maxZoom: 14
});
}
},
displayVehicleMarker(vehicleIndex) {
if (!this.map || !this.map.loaded() || !this.vehicles[vehicleIndex]) return;
const vehicle = this.vehicles[vehicleIndex];
if (!vehicle.location) return;
// Create a larger marker for the selected vehicle
const el = document.createElement('div');
el.className = 'w-12 h-12 rounded-co bg-co-orange border-2 border-white shadow-lg flex items-center justify-center';
el.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"></path>
<circle cx="7" cy="17" r="2"></circle>
<circle cx="17" cy="17" r="2"></circle>
</svg>
`;
const marker = new maplibregl.Marker({element: el})
.setLngLat(vehicle.location)
.addTo(this.map);
this.routeMarkers.push(marker);
// Center map on the vehicle
this.map.flyTo({
center: vehicle.location,
zoom: 15,
duration: 1000
});
},
getVehicleBookingUrl() {
let url = '/app/vehicles/?';
if (this.passengerId) {
url += 'beneficiaryid=' + this.passengerId;
}
// Use the departure date from search as start date
if (this.departureDate) {
url += '&startdate=' + this.departureDate;
// Calculate end date as 7 days after start date
const startDate = new Date(this.departureDate);
startDate.setDate(startDate.getDate() + 7);
const endDate = startDate.toISOString().split('T')[0];
url += '&enddate=' + endDate;
}
return url;
},
getSelectLabel(field, value) {
if (!field.options || !value) return value;
const option = field.options.find(opt => opt.value === value);
return option ? option.label : value;
}
};
}