Compact search improvements + drivers map in the dashboard
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user