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

@ -78,6 +78,7 @@ views:
- web/layouts/dashboard/_partials/agenda-widget.html
- web/layouts/dashboard/_partials/beneficiaries-widget.html
- web/layouts/dashboard/_partials/bookings-widget.html
- web/layouts/dashboard/_partials/drivers-map-widget.html
- web/layouts/dashboard/dashboard.html
beneficiaries:
list:

View File

@ -1,6 +1,6 @@
{{define "beneficiary_journeys"}}
<div class="px-4 py-6 sm:px-6">
<form action="/app/journeys/" method="GET">
<form action="{{if eq $.ViewState.search_view "compact"}}/app/journeys/search{{else}}/app/journeys/{{end}}" method="GET">
{{ $departureField := "departure" }}
{{ $departureLabel := "Départ" }}

View File

@ -0,0 +1,174 @@
{{define "drivers_map_widget"}}
<div class="bg-white overflow-hidden shadow rounded-2xl">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Carte des conducteurs
</h3>
</div>
<div id="drivers-map" class="w-full rounded-lg overflow-hidden border border-gray-200" style="height: 500px;"></div>
<div class="mt-4 flex items-center justify-center gap-6 text-sm">
{{if moduleAvailable "solidarity_transport"}}
<div class="flex items-center">
<div class="w-6 h-6 rounded-co bg-co-blue border border-white shadow-md mr-2"></div>
<span class="text-gray-700">Transport solidaire ({{len .solidarity_drivers}})</span>
</div>
{{end}}
{{if moduleAvailable "organized_carpool"}}
<div class="flex items-center">
<div class="w-6 h-6 rounded-co bg-co-green border border-white shadow-md mr-2"></div>
<span class="text-gray-700">Covoiturage solidaire ({{len .organized_carpool_drivers}})</span>
</div>
{{end}}
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Check if map libraries are available
if (typeof maplibregl === 'undefined' || typeof pmtiles === 'undefined') {
console.warn('Map libraries not available');
return;
}
// Initialize PMTiles protocol
let protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
// Initialize map centered on France with protomaps style
const map = new maplibregl.Map({
container: 'drivers-map',
style: '/public/maps/protomaps-light/style.json',
center: [2.3522, 48.8566], // Paris
zoom: 5
});
map.addControl(new maplibregl.NavigationControl(), 'top-right');
const bounds = new maplibregl.LngLatBounds();
let hasMarkers = false;
map.on('load', function() {
// Add solidarity transport drivers
{{if moduleAvailable "solidarity_transport"}}
const solidarityDrivers = [
{{range .solidarity_drivers}}
{{if .Data.address}}
{
name: '{{.Data.first_name}} {{.Data.last_name}}',
coordinates: {{if and .Data.address.geometry .Data.address.geometry.coordinates}}[{{index .Data.address.geometry.coordinates 0}}, {{index .Data.address.geometry.coordinates 1}}]{{else}}null{{end}},
address: {{if .Data.address.properties}}'{{.Data.address.properties.label}}'{{else}}''{{end}},
type: 'solidarity-transport',
driverId: '{{.ID}}'
},
{{end}}
{{end}}
].filter(d => d.coordinates !== null);
solidarityDrivers.forEach(function(driver) {
// Create custom marker element matching compact search style
const el = document.createElement('div');
el.className = 'w-8 h-8 rounded-co bg-co-blue border border-white shadow-md flex items-center justify-center cursor-pointer';
// User icon matching compact search
el.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const marker = new maplibregl.Marker({element: el})
.setLngLat(driver.coordinates)
.setPopup(new maplibregl.Popup({offset: 25, className: 'rounded-lg'})
.setHTML(`
<div class="p-3">
<a href="/app/solidarity-transport/drivers/${driver.driverId}" class="font-semibold text-sm text-gray-900 hover:text-co-blue">${driver.name}</a>
<p class="text-xs text-gray-600 mt-1">${driver.address}</p>
<p class="text-xs text-co-blue font-medium mt-2">Transport solidaire</p>
</div>
`))
.addTo(map);
bounds.extend(driver.coordinates);
hasMarkers = true;
});
{{end}}
// Add organized carpool drivers
{{if moduleAvailable "organized_carpool"}}
const organizedCarpoolDrivers = [
{{range .organized_carpool_drivers}}
{{if .Data.address}}
{
name: '{{.Data.first_name}} {{.Data.last_name}}',
coordinates: {{if and .Data.address.geometry .Data.address.geometry.coordinates}}[{{index .Data.address.geometry.coordinates 0}}, {{index .Data.address.geometry.coordinates 1}}]{{else}}null{{end}},
address: {{if .Data.address.properties}}'{{.Data.address.properties.label}}'{{else}}''{{end}},
type: 'organized-carpool',
driverId: '{{.ID}}'
},
{{end}}
{{end}}
].filter(d => d.coordinates !== null);
organizedCarpoolDrivers.forEach(function(driver) {
// Create custom marker element matching compact search style
const el = document.createElement('div');
el.className = 'w-8 h-8 rounded-co bg-co-green border border-white shadow-md flex items-center justify-center cursor-pointer';
// User icon matching compact search
el.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
`;
const marker = new maplibregl.Marker({element: el})
.setLngLat(driver.coordinates)
.setPopup(new maplibregl.Popup({offset: 25, className: 'rounded-lg'})
.setHTML(`
<div class="p-3">
<a href="/app/organized-carpool/drivers/${driver.driverId}" class="font-semibold text-sm text-gray-900 hover:text-co-green">${driver.name}</a>
<p class="text-xs text-gray-600 mt-1">${driver.address}</p>
<p class="text-xs text-co-green font-medium mt-2">Covoiturage solidaire</p>
</div>
`))
.addTo(map);
bounds.extend(driver.coordinates);
hasMarkers = true;
});
{{end}}
// Fit map to markers bounds with padding
if (hasMarkers) {
map.fitBounds(bounds, {
padding: 50,
maxZoom: 12
});
}
});
});
</script>
<style>
.maplibregl-popup-content {
padding: 0;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.maplibregl-popup-close-button {
font-size: 20px;
padding: 4px;
color: #6B7280;
}
.maplibregl-popup-close-button:hover {
background-color: #F3F4F6;
color: #111827;
}
</style>
{{end}}

View File

@ -14,40 +14,12 @@
<div
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-3xl truncate">
<div class="flex-1 px-4 py-2 text-sm truncate">
<a href="#" class="text-gray-900 font-medium hover:text-gray-600">Bénéficiaires</a>
<a href="/app/beneficiaries/" class="text-gray-900 font-medium hover:text-gray-600">Bénéficiaires</a>
<p class="text-gray-500">{{.ViewState.beneficiaries.count}} bénéficiaires</p>
</div>
</div>
</li>
<li class="col-span-1 flex shadow-sm rounded-3xl">
<div
class="flex-shrink-0 flex items-center justify-center w-16 bg-co-green text-white text-sm font-medium rounded-l-3xl">
{{.IconSet.Icon "hero:outline/shield-check" "h-6 w-6"}}
</div>
<div
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-3xl truncate">
<div class="flex-1 px-4 py-2 text-sm truncate">
<a href="#" class="text-gray-900 font-medium hover:text-gray-600">Accompagnement</a>
<p class="text-gray-500">0 actions réalisées</p>
</div>
</div>
</li>
<li class="col-span-1 flex shadow-sm rounded-3xl">
<div
class="flex-shrink-0 flex items-center justify-center w-16 bg-co-yellow text-white text-sm font-medium rounded-l-3xl">
{{.IconSet.Icon "hero:outline/office-building" "h-6 w-6"}}
</div>
<div
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-3xl truncate">
<div class="flex-1 px-4 py-2 text-sm truncate">
<a href="#" class="text-gray-900 font-medium hover:text-gray-600">Groupes</a>
<p class="text-gray-500">0 groupes créés</p>
</div>
</div>
</li>
<li class="col-span-1 flex shadow-sm rounded-3xl">
<div
class="flex-shrink-0 flex items-center justify-center w-16 bg-co-red text-white text-sm font-medium rounded-l-3xl">
@ -61,6 +33,44 @@
</div>
</div>
</li>
{{if moduleAvailable "solidarity_transport"}}
<li class="col-span-1 flex shadow-sm rounded-3xl">
<div
class="flex-shrink-0 flex items-center justify-center w-16 bg-co-orange text-white text-sm font-medium rounded-l-3xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<div
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-3xl truncate">
<div class="flex-1 px-4 py-2 text-sm truncate">
<a href="/app/solidarity-transport/?tab=drivers" class="text-gray-900 font-medium hover:text-gray-600">Conducteurs solidaires</a>
<p class="text-gray-500">{{len .ViewState.solidarity_drivers}} conducteurs</p>
</div>
</div>
</li>
{{end}}
{{if moduleAvailable "organized_carpool"}}
<li class="col-span-1 flex shadow-sm rounded-3xl">
<div
class="flex-shrink-0 flex items-center justify-center w-16 bg-co-green text-white text-sm font-medium rounded-l-3xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<div
class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-3xl truncate">
<div class="flex-1 px-4 py-2 text-sm truncate">
<a href="/app/organized-carpool/?tab=drivers" class="text-gray-900 font-medium hover:text-gray-600">Covoitureurs solidaires</a>
<p class="text-gray-500">{{len .ViewState.organized_carpool_drivers}} covoitureurs</p>
</div>
</div>
</li>
{{end}}
</ul>
</div>
@ -79,5 +89,11 @@
{{end}}
</div>
{{if or (moduleAvailable "solidarity_transport") (moduleAvailable "organized_carpool")}}
<div class="py-4">
{{template "drivers_map_widget" .ViewState}}
</div>
{{end}}
</div>
{{end}}

View File

@ -80,7 +80,7 @@
{{end}}
</td>
<td class="relative py-3 px-3 text-right text-sm font-medium whitespace-nowrap">
<a href="/app/journeys/?departure={{json .Departure}}&destination={{json .Destination}}&departuredate={{timeFormat .DateTime "2006-01-02"}}&departuretime={{timeFormat .DateTime "15:04"}}{{if and .Data .Data.passenger_id}}&passengerid={{.Data.passenger_id}}{{end}}" class="text-co-blue hover:text-co-blue-dark mr-4">
<a href="{{if eq $.ViewState.search_view "compact"}}/app/journeys/search{{else}}/app/journeys/{{end}}?departure={{json .Departure}}&destination={{json .Destination}}&departuredate={{timeFormat .DateTime "2006-01-02"}}&departuretime={{timeFormat .DateTime "15:04"}}{{if and .Data .Data.passenger_id}}&passengerid={{.Data.passenger_id}}{{end}}" class="text-co-blue hover:text-co-blue-dark mr-4">
Rechercher
</a>
<a href="/app/journeys/saved-searches/{{.ID}}/delete"

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;
}
};
}

View File

@ -21,7 +21,7 @@
<div class="bg-white shadow sm:rounded-2xl">
<h2 id="timeline-title" class="text-lg font-medium text-gray-900 p-4 sm:px-6">Chercher une solution</h2>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<form method="GET" x-data="journeySearch()">
<form method="GET" action="{{if eq .ViewState.search_view "compact"}}/app/journeys/search{{else}}/app/journeys/{{end}}" x-data="journeySearch()">
<div class="py-4">
<label for="beneficiary" class="block text-sm font-medium text-gray-700">Bénéficiaire (optionnel)</label>

View File

@ -12,7 +12,7 @@
<div class="divide-y divide-gray-200">
<div>
<div class="hidden sm:block">
<div class="border-b border-gray-200 pl-4">
<div class="border-b border-gray-200 pl-4 flex justify-between items-center">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<a href="#" @click="tab = 'carpoolService'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
@ -29,6 +29,24 @@
:class="tab == 'carpoolHistory' ? 'border-co-blue text-co-blue' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
Trajets passés </a>
</nav>
<div class="pr-4" x-show="tab == 'carpoolHistory'">
{{$exportURL := "/exports/organized-carpool/bookings.xlsx"}}
{{$hasParams := false}}
{{if .ViewState.hist_filters.date_start}}{{$exportURL = printf "%s?start_date=%s" $exportURL .ViewState.hist_filters.date_start}}{{$hasParams = true}}{{end}}
{{if .ViewState.hist_filters.date_end}}{{if $hasParams}}{{$exportURL = printf "%s&end_date=%s" $exportURL .ViewState.hist_filters.date_end}}{{else}}{{$exportURL = printf "%s?end_date=%s" $exportURL .ViewState.hist_filters.date_end}}{{$hasParams = true}}{{end}}{{end}}
{{if .ViewState.hist_filters.status}}{{if $hasParams}}{{$exportURL = printf "%s&status=%s" $exportURL .ViewState.hist_filters.status}}{{else}}{{$exportURL = printf "%s?status=%s" $exportURL .ViewState.hist_filters.status}}{{end}}{{end}}
<a href="{{$exportURL}}">
<button type="button"
class="inline-flex items-center justify-center bg-white hover:bg-gray-50 border-gray-300 border px-4 py-2 text-gray-700 flex items-center text-sm rounded-2xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Exporter
</button>
</a>
</div>
</div>
</div>
</div>

View File

@ -12,7 +12,7 @@
<div class="divide-y divide-gray-200">
<div>
<div class="hidden sm:block">
<div class="border-b border-gray-200 pl-4">
<div class="border-b border-gray-200 pl-4 flex justify-between items-center">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<a href="#" @click="tab = 'solidarityService'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
@ -34,6 +34,24 @@
:class="tab == 'solidarityHistory' ? 'border-co-blue text-co-blue' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
Trajets passés </a>
</nav>
<div class="pr-4" x-show="tab == 'solidarityHistory'">
{{$exportURL := "/exports/solidarity-transport/bookings.xlsx"}}
{{$hasParams := false}}
{{if .ViewState.hist_filters.date_start}}{{$exportURL = printf "%s?start_date=%s" $exportURL .ViewState.hist_filters.date_start}}{{$hasParams = true}}{{end}}
{{if .ViewState.hist_filters.date_end}}{{if $hasParams}}{{$exportURL = printf "%s&end_date=%s" $exportURL .ViewState.hist_filters.date_end}}{{else}}{{$exportURL = printf "%s?end_date=%s" $exportURL .ViewState.hist_filters.date_end}}{{$hasParams = true}}{{end}}{{end}}
{{if .ViewState.hist_filters.status}}{{if $hasParams}}{{$exportURL = printf "%s&status=%s" $exportURL .ViewState.hist_filters.status}}{{else}}{{$exportURL = printf "%s?status=%s" $exportURL .ViewState.hist_filters.status}}{{end}}{{end}}
<a href="{{$exportURL}}">
<button type="button"
class="inline-flex items-center justify-center bg-white hover:bg-gray-50 border-gray-300 border px-4 py-2 text-gray-700 flex items-center text-sm rounded-2xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Exporter
</button>
</a>
</div>
</div>
</div>
</div>

View File

@ -11,10 +11,13 @@
--color-red-600: oklch(0.577 0.245 27.325);
--color-red-800: oklch(0.444 0.177 26.899);
--color-red-900: oklch(0.396 0.141 25.723);
--color-orange-50: oklch(0.98 0.016 73.684);
--color-orange-200: oklch(0.901 0.076 70.697);
--color-yellow-50: oklch(0.987 0.026 102.212);
--color-yellow-100: oklch(0.973 0.071 103.193);
--color-yellow-800: oklch(0.476 0.114 61.907);
--color-green-100: oklch(0.962 0.044 156.743);
--color-green-500: oklch(0.723 0.219 149.579);
--color-green-600: oklch(0.627 0.194 149.214);
--color-green-800: oklch(0.448 0.119 151.328);
--color-blue-50: oklch(0.97 0.014 254.604);
@ -30,6 +33,7 @@
--color-purple-50: oklch(0.977 0.014 308.299);
--color-purple-100: oklch(0.946 0.033 307.174);
--color-purple-200: oklch(0.902 0.063 306.703);
--color-purple-500: oklch(0.627 0.265 303.9);
--color-purple-600: oklch(0.558 0.288 302.321);
--color-purple-700: oklch(0.496 0.265 301.924);
--color-purple-800: oklch(0.438 0.218 303.724);
@ -1804,6 +1808,9 @@
.border-indigo-500 {
border-color: var(--color-indigo-500);
}
.border-orange-200 {
border-color: var(--color-orange-200);
}
.border-purple-200 {
border-color: var(--color-purple-200);
}
@ -1840,6 +1847,9 @@
.bg-blue-100 {
background-color: var(--color-blue-100);
}
.bg-blue-500 {
background-color: var(--color-blue-500);
}
.bg-blue-600 {
background-color: var(--color-blue-600);
}
@ -1852,6 +1862,9 @@
.bg-co-lightblue {
background-color: var(--color-co-lightblue);
}
.bg-co-orange {
background-color: var(--color-co-orange);
}
.bg-co-red {
background-color: var(--color-co-red);
}
@ -1882,6 +1895,9 @@
.bg-green-100 {
background-color: var(--color-green-100);
}
.bg-green-500 {
background-color: var(--color-green-500);
}
.bg-green-600 {
background-color: var(--color-green-600);
}
@ -1891,12 +1907,18 @@
.bg-indigo-600 {
background-color: var(--color-indigo-600);
}
.bg-orange-50 {
background-color: var(--color-orange-50);
}
.bg-purple-50 {
background-color: var(--color-purple-50);
}
.bg-purple-100 {
background-color: var(--color-purple-100);
}
.bg-purple-500 {
background-color: var(--color-purple-500);
}
.bg-purple-600 {
background-color: var(--color-purple-600);
}
@ -2132,6 +2154,9 @@
.p-4 {
padding: calc(var(--spacing) * 4);
}
.p-6 {
padding: calc(var(--spacing) * 6);
}
.p-8 {
padding: calc(var(--spacing) * 8);
}
@ -2467,6 +2492,9 @@
.text-black {
color: var(--color-black);
}
.text-blue-600 {
color: var(--color-blue-600);
}
.text-blue-800 {
color: var(--color-blue-800);
}
@ -3113,6 +3141,16 @@
outline-style: none;
}
}
.hover\:scale-110 {
&:hover {
@media (hover: hover) {
--tw-scale-x: 110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
}
.hover\:border-gray-300 {
&:hover {
@media (hover: hover) {
@ -3183,6 +3221,13 @@
}
}
}
.hover\:text-co-green {
&:hover {
@media (hover: hover) {
color: var(--color-co-green);
}
}
}
.hover\:text-gray-500 {
&:hover {
@media (hover: hover) {