9 Commits

Author SHA1 Message Date
Arnaud Delcasse
039111c36c feat: extra properties dynamiques, filtrage meta_status et alertes retard
Some checks failed
Build and Push Docker Image / build_and_push (push) Failing after 2m37s
2026-02-27 16:40:46 +01:00
Arnaud Delcasse
95365ff8ce feat: gestion manuelle des statuts de réservation véhicule 2026-02-26 17:56:25 +01:00
Arnaud Delcasse
b79cc08b06 feat: add global search across beneficiaries and drivers 2026-02-26 14:54:22 +01:00
Arnaud Delcasse
bb525f174d feat: auto-fill vehicle booking end date (default 90 days, configurable) 2026-02-26 14:32:09 +01:00
Arnaud Delcasse
8d89306a90 evol: mise à jour renderers bénéficiaires et covoiturage 2026-02-25 17:51:53 +01:00
Arnaud Delcasse
549ea35a8c evol: désérialiser adresses disponibilités conducteur solidaire 2026-02-25 17:11:28 +01:00
Arnaud Delcasse
a60466d891 Beneficiaries export as XLSX 2026-02-25 15:38:05 +01:00
Arnaud Delcasse
092d1acfbd evol: filter geography on beneficiaries 2026-02-25 10:15:22 +01:00
Arnaud Delcasse
1b1c4443fc fix: update history count 2026-02-25 10:05:07 +01:00
33 changed files with 1213 additions and 136 deletions

View File

@@ -232,6 +232,25 @@ func ReadConfig() (*viper.Viper, error) {
},
"vehicles": map[string]any{
"enabled": true,
"default_booking_duration_days": 90,
"status_management": "automatic",
"booking_extra_properties": []map[string]any{
{"name": "start_kilometers", "label": "Kilométrage de départ", "type": "number"},
{"name": "enddate", "label": "Date et heure de restitution", "type": "datetime-local", "target": "enddate"},
{"name": "end_kilometers", "label": "Kilométrage de fin", "type": "number"},
{"name": "kilometers_done", "label": "Kilomètres réalisés", "type": "computed", "operation": "subtract", "operands": []string{"end_kilometers", "start_kilometers"}, "unit": "km"},
{"name": "loan_duration", "label": "Durée du prêt", "type": "computed", "operation": "duration", "operands": []string{"booking.startdate", "booking.enddate"}},
{"name": "unavailableto", "label": "Sera à nouveau disponible le", "type": "date", "target": "unavailableto"},
},
"status_options": []map[string]any{
{"name": "requested", "label": "Demandé", "initial": true, "meta_status": "open"},
{"name": "accepted", "label": "Accepté", "meta_status": "active"},
{"name": "en_pret", "label": "En prêt", "meta_status": "active", "requested_properties": []map[string]any{{"name": "start_kilometers", "required": true}, {"name": "enddate"}}},
{"name": "completed", "label": "Terminé", "meta_status": "closed", "requested_properties": []map[string]any{{"name": "end_kilometers", "required": true}, {"name": "unavailableto"}}},
{"name": "refused", "label": "Refusé", "meta_status": "closed"},
{"name": "cancelled", "label": "Annulé", "meta_status": "closed"},
{"name": "not_completed", "label": "Non réalisé", "meta_status": "closed"},
},
},
"vehicles_management": map[string]any{
"enabled": true,
@@ -254,11 +273,11 @@ func ReadConfig() (*viper.Viper, error) {
"geo": map[string]any{
"type": "addok", // Options: "pelias", "addok"
"pelias": map[string]any{
"url": "https://geocode.ridygo.fr",
"url": "http://57.128.110.46:4000/V1",
"autocomplete": "/autocomplete?text=",
},
"addok": map[string]any{
"url": "https://api-adresse.data.gouv.fr",
"url": "https://data.geopf.fr/geocodage/",
"autocomplete": "/search/?q=",
},
},

View File

@@ -31,6 +31,8 @@ import (
solidaritytransformers "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
solidaritytypes "git.coopgo.io/coopgo-platform/solidarity-transport/types"
"github.com/google/uuid"
"github.com/paulmach/orb"
"github.com/paulmach/orb/geojson"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -41,12 +43,38 @@ type BeneficiariesResult struct {
CacheID string
}
func (h *ApplicationHandler) GetBeneficiaries(ctx context.Context, searchFilter string, archivedFilter bool) (*BeneficiariesResult, error) {
func (h *ApplicationHandler) GetBeneficiaries(ctx context.Context, searchFilter string, archivedFilter bool, addressGeoLayer, addressGeoCode string) (*BeneficiariesResult, error) {
accounts, err := h.getBeneficiariesWithFilters(ctx, searchFilter, archivedFilter)
if err != nil {
return nil, err
}
// Apply address geography filtering
if addressGeoLayer != "" && addressGeoCode != "" {
addressPolygons, err := h.loadGeographyPolygon(addressGeoLayer, addressGeoCode)
if err != nil {
log.Warn().Err(err).Msg("failed to load beneficiary address geography filter")
} else {
filtered := []mobilityaccountsstorage.Account{}
for _, account := range accounts {
if addr, ok := account.Data["address"]; ok {
jsonAddr, err := json.Marshal(addr)
if err == nil {
addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
if err == nil && addrGeojson.Geometry != nil {
if point, ok := addrGeojson.Geometry.(orb.Point); ok {
if isPointInGeographies(point, addressPolygons) {
filtered = append(filtered, account)
}
}
}
}
}
}
accounts = filtered
}
}
sort.Sort(sorting.BeneficiariesByName(accounts))
cacheID := uuid.NewString()

View File

@@ -36,6 +36,8 @@ type SearchJourneysResult struct {
Drivers map[string]mobilityaccountsstorage.Account
OrganizedCarpools []*carpoolproto.CarpoolServiceDriverJourney
KnowledgeBaseResults []any
DriverLastTrips map[string]time.Time // Map of driver ID to their last completed trip date
LastTripDays int // Number of days to look back for last trips
}
// SearchJourneyOptions contains per-request options for journey search
@@ -46,6 +48,7 @@ type SearchJourneyOptions struct {
DisableTransit bool
DisableFleetVehicles bool
DisableKnowledgeBase bool
SolidarityTransportNoreturn *bool
}
// SearchJourneys performs the business logic for journey search
@@ -111,6 +114,10 @@ func (h *ApplicationHandler) SearchJourneys(
if solidarityExcludeGroupId != "" {
req.ExcludeGroupId = &solidarityExcludeGroupId
}
// Pass noreturn to filter journeys by type (one-way vs round-trip)
if options.SolidarityTransportNoreturn != nil {
req.Noreturn = *options.SolidarityTransportNoreturn
}
res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, req)
if err != nil {
@@ -291,6 +298,33 @@ func (h *ApplicationHandler) SearchJourneys(
}
}
// Get last trip dates for solidarity transport drivers
driverLastTrips := make(map[string]time.Time)
lastTripDays := h.config.GetInt("modules.journeys.solutions.solidarity_transport.last_trip_days")
if lastTripDays <= 0 {
lastTripDays = 15
}
if len(solidarityTransportResults) > 0 {
// Get all validated bookings from the past N days to find last trips
bookingsRequest := &gen.GetSolidarityTransportBookingsRequest{
StartDate: timestamppb.New(departureDateTime.Add(-time.Duration(lastTripDays) * 24 * time.Hour)),
EndDate: timestamppb.New(departureDateTime.Add(24 * time.Hour)),
Status: "VALIDATED",
}
bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, bookingsRequest)
if err == nil {
for _, booking := range bookingsResp.Bookings {
if booking.Journey != nil {
tripDate := booking.Journey.PassengerPickupDate.AsTime()
// Only consider trips that have already happened
if lastTrip, exists := driverLastTrips[booking.DriverId]; !exists || tripDate.After(lastTrip) {
driverLastTrips[booking.DriverId] = tripDate
}
}
}
}
}
return &SearchJourneysResult{
CarpoolResults: carpoolResults,
TransitResults: transitResults,
@@ -300,6 +334,8 @@ func (h *ApplicationHandler) SearchJourneys(
Drivers: drivers,
OrganizedCarpools: organizedCarpoolResults,
KnowledgeBaseResults: knowledgeBaseResults,
DriverLastTrips: driverLastTrips,
LastTripDays: lastTripDays,
}, nil
}

View File

@@ -0,0 +1,47 @@
package application
import (
"context"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
"github.com/rs/zerolog/log"
)
type GlobalSearchResult struct {
Beneficiaries []mobilityaccountsstorage.Account
SolidarityDrivers []mobilityaccountsstorage.Account
OrganizedCarpoolDrivers []mobilityaccountsstorage.Account
}
func (h *ApplicationHandler) GlobalSearch(ctx context.Context, query string) (*GlobalSearchResult, error) {
result := &GlobalSearchResult{}
if h.config.GetBool("modules.beneficiaries.enabled") {
beneficiaries, err := h.getBeneficiariesWithFilters(ctx, query, false)
if err != nil {
log.Error().Err(err).Msg("global search: error retrieving beneficiaries")
} else {
result.Beneficiaries = beneficiaries
}
}
if h.config.GetBool("modules.solidarity_transport.enabled") {
drivers, err := h.solidarityDrivers(ctx, query, false)
if err != nil {
log.Error().Err(err).Msg("global search: error retrieving solidarity drivers")
} else {
result.SolidarityDrivers = drivers
}
}
if h.config.GetBool("modules.organized_carpool.enabled") {
drivers, err := h.getOrganizedCarpoolDrivers(ctx, query, false)
if err != nil {
log.Error().Err(err).Msg("global search: error retrieving organized carpool drivers")
} else {
result.OrganizedCarpoolDrivers = drivers
}
}
return result, nil
}

View File

@@ -689,7 +689,7 @@ func (h *ApplicationHandler) UpdateSolidarityTransportDriver(ctx context.Context
type SolidarityTransportDriverDataResult struct {
Driver mobilityaccountsstorage.Account
Availabilities []*gen.DriverRegularAvailability
Availabilities []*solidaritytypes.DriverRegularAvailability
Documents []filestorage.FileInfo
Bookings []*solidaritytypes.Booking
BeneficiariesMap map[string]mobilityaccountsstorage.Account
@@ -733,6 +733,12 @@ func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Contex
return nil, err
}
// Convert proto availabilities to types with deserialized geojson address
availabilities := []*solidaritytypes.DriverRegularAvailability{}
for _, protoAvail := range availResp.Results {
availabilities = append(availabilities, solidaritytransformers.DriverRegularAvailabilityProtoToType(protoAvail))
}
// Get documents
documents := h.filestorage.List(filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS + "/" + driverID)
@@ -812,7 +818,7 @@ func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Contex
return &SolidarityTransportDriverDataResult{
Driver: driver,
Availabilities: availResp.Results,
Availabilities: availabilities,
Documents: documents,
Bookings: bookings,
BeneficiariesMap: beneficiariesMap,
@@ -1391,7 +1397,11 @@ func (h *ApplicationHandler) calculateSolidarityTransportPricing(ctx context.Con
}
if err == nil {
history = history + len(solidarity.Bookings)
for _, booking := range solidarity.Bookings {
if booking.Status == "VALIDATED" || booking.Status == "WAITING_CONFIRMATION" {
history++
}
}
}
var passengerGeo pricing.GeographyParams

View File

@@ -3,7 +3,10 @@ package application
import (
"context"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
@@ -28,7 +31,7 @@ type VehiclesManagementOverviewResult struct {
Bookings []fleetsstorage.Booking
}
func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context, groupID string) (*VehiclesManagementOverviewResult, error) {
func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context, groupID, vehicleType, status, dateStart, dateEnd, vType, vStatus string) (*VehiclesManagementOverviewResult, error) {
request := &fleets.GetVehiclesRequest{
Namespaces: []string{"parcoursmob"},
}
@@ -41,14 +44,82 @@ func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context,
bookings := []fleetsstorage.Booking{}
vehiclesMap := map[string]fleetsstorage.Vehicle{}
isManualStatus := h.config.GetString("modules.vehicles.status_management") == "manual"
// Parse date filters
var startdate time.Time
if dateStart != "" {
if parsed, err := time.Parse("2006-01-02", dateStart); err == nil {
startdate = parsed
}
}
var enddate time.Time
if dateEnd != "" {
if parsed, err := time.Parse("2006-01-02", dateEnd); err == nil {
enddate = parsed.Add(24 * time.Hour)
}
}
for _, vehicle := range resp.Vehicles {
if h.filterVehicleByGroup(vehicle, groupID) {
v := vehicle.ToStorageType()
vehicleBookings := []fleetsstorage.Booking{}
for _, b := range v.Bookings {
log.Debug().Any("booking", b).Msg("debug")
if b.Status() != fleetsstorage.StatusOld {
if !b.Deleted {
include := true
// Apply date filters (intersection: booking overlaps with [startdate, enddate])
if !startdate.IsZero() && b.Enddate.Before(startdate) {
include = false
}
if !enddate.IsZero() && b.Startdate.After(enddate) {
include = false
}
// Apply vehicle type filter on bookings
if include && vehicleType != "" && v.Type != vehicleType {
include = false
}
// Apply status filter on bookings
if include && status != "" {
isAdminUnavail, _ := b.Data["administrator_unavailability"].(bool)
if isAdminUnavail {
include = false
} else if isManualStatus {
if strings.HasPrefix(status, "meta:") {
metaParts := strings.Split(strings.TrimPrefix(status, "meta:"), ",")
metaSet := map[string]bool{}
for _, ms := range metaParts {
metaSet[ms] = true
}
allowedStatuses := h.resolveMetaStatuses(metaSet)
if !allowedStatuses[b.ManualStatus] {
include = false
}
} else if b.ManualStatus != status {
include = false
}
} else {
var filterStatusInt int
switch status {
case "FORTHCOMING":
filterStatusInt = 1
case "ONGOING":
filterStatusInt = 0
case "TERMINATED":
filterStatusInt = -1
default:
filterStatusInt = 999
}
if b.Status() != filterStatusInt {
include = false
}
}
}
if include {
bookings = append(bookings, b)
}
}
@@ -57,8 +128,48 @@ func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context,
}
}
v.Bookings = vehicleBookings
vehicles = append(vehicles, v)
// Always add to vehiclesMap for booking lookups
vehiclesMap[v.ID] = v
// Apply vehicle type filter
if vType != "" && v.Type != vType {
continue
}
// Apply vehicle reservation status filter
if vStatus != "" {
hasActive := false
hasUpcoming := false
hasRetired := false
for _, b := range v.Bookings {
isAdminUnavail, _ := b.Data["administrator_unavailability"].(bool)
if isAdminUnavail {
hasRetired = true
} else if b.Status() == 0 {
hasActive = true
} else if b.Status() == 1 {
hasUpcoming = true
}
}
match := false
switch vStatus {
case "DISPONIBLE":
match = !hasActive && !hasUpcoming && !hasRetired
case "RESERVE":
match = hasUpcoming
case "EN_PRET":
match = hasActive
case "RETIRE":
match = hasRetired
}
if !match {
continue
}
}
vehicles = append(vehicles, v)
}
}
@@ -95,7 +206,7 @@ type VehiclesManagementBookingsListResult struct {
CacheID string
}
func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Context, groupID, status, startDate, endDate string) (*VehiclesManagementBookingsListResult, error) {
func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Context, groupID, status, startDate, endDate, vehicleType string) (*VehiclesManagementBookingsListResult, error) {
request := &fleets.GetVehiclesRequest{
Namespaces: []string{"parcoursmob"},
IncludeDeleted: true,
@@ -129,9 +240,29 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte
v := vehicle.ToStorageType()
vehiclesMap[v.ID] = v
for _, b := range v.Bookings {
if v, ok := b.Data["administrator_unavailability"].(bool); !ok || !v {
if isAdminUnavail, ok := b.Data["administrator_unavailability"].(bool); !ok || !isAdminUnavail {
// Apply vehicle type filter
if vehicleType != "" && v.Type != vehicleType {
continue
}
// Apply status filter
if status != "" {
if h.config.GetString("modules.vehicles.status_management") == "manual" {
if strings.HasPrefix(status, "meta:") {
metaParts := strings.Split(strings.TrimPrefix(status, "meta:"), ",")
metaSet := map[string]bool{}
for _, ms := range metaParts {
metaSet[ms] = true
}
allowedStatuses := h.resolveMetaStatuses(metaSet)
if !allowedStatuses[b.ManualStatus] {
continue
}
} else if b.ManualStatus != status {
continue
}
} else {
bookingStatus := b.Status()
statusInt := 0
@@ -160,9 +291,10 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte
continue
}
}
}
// Apply date filter (on startdate)
if !startdate.IsZero() && b.Startdate.Before(startdate) {
// Apply date filter (intersection: booking overlaps with [startdate, enddate])
if !startdate.IsZero() && b.Enddate.Before(startdate) {
continue
}
if !enddate.IsZero() && b.Startdate.After(enddate) {
@@ -331,6 +463,7 @@ type BookingDisplayResult struct {
Documents []filestorage.FileInfo
FileTypesMap map[string]string
Alternatives []any
ComputedExtraProperties map[string]string
}
func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID string) (*BookingDisplayResult, error) {
@@ -382,6 +515,8 @@ func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID st
documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingID)
fileTypesMap := h.config.GetStringMapString("storage.files.file_types")
computedProps := h.computeExtraProperties(booking)
return &BookingDisplayResult{
Booking: booking,
Vehicle: booking.Vehicle,
@@ -390,6 +525,7 @@ func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID st
Documents: documents,
FileTypesMap: fileTypesMap,
Alternatives: alternatives,
ComputedExtraProperties: computedProps,
}, nil
}
@@ -596,3 +732,351 @@ func (h *ApplicationHandler) UpdateVehicle(ctx context.Context, vehicleID, name,
return updateResp.Vehicle.Id, nil
}
// getStatusOptions extracts status options from Viper config, handling both
// Go defaults ([]map[string]any) and YAML-parsed ([]interface{}) types.
func getStatusOptions(raw interface{}) []map[string]any {
if options, ok := raw.([]map[string]any); ok {
return options
}
if rawSlice, ok := raw.([]interface{}); ok {
result := make([]map[string]any, 0, len(rawSlice))
for _, item := range rawSlice {
if opt, ok := item.(map[string]any); ok {
result = append(result, opt)
}
}
return result
}
return nil
}
// getBookingExtraProperties extracts booking extra property definitions from Viper config,
// handling both Go defaults ([]map[string]any) and YAML-parsed ([]interface{}) types.
func getBookingExtraProperties(raw interface{}) []map[string]any {
if props, ok := raw.([]map[string]any); ok {
return props
}
if rawSlice, ok := raw.([]interface{}); ok {
result := make([]map[string]any, 0, len(rawSlice))
for _, item := range rawSlice {
if prop, ok := item.(map[string]any); ok {
result = append(result, prop)
}
}
return result
}
return nil
}
func (h *ApplicationHandler) UpdateBookingStatus(ctx context.Context, bookingID, newStatus, comment, userID string, userClaims map[string]any, currentGroup any, extraProperties map[string]string) error {
group := currentGroup.(storage.Group)
// Validate that newStatus is a valid status option
options := getStatusOptions(h.config.Get("modules.vehicles.status_options"))
validStatus := false
for _, opt := range options {
if name, ok := opt["name"].(string); ok && name == newStatus {
validStatus = true
break
}
}
if !validStatus {
return fmt.Errorf("invalid status: %s", newStatus)
}
booking, err := h.services.GetBooking(bookingID)
if err != nil {
return fmt.Errorf("failed to get booking: %w", err)
}
oldStatus := booking.ManualStatus
booking.ManualStatus = newStatus
booking.StatusHistory = append(booking.StatusHistory, fleetsstorage.StatusHistoryEntry{
FromStatus: oldStatus,
ToStatus: newStatus,
UserID: userID,
UserName: fmt.Sprintf("%s %s", userClaims["first_name"], userClaims["last_name"]),
GroupID: group.ID,
GroupName: fmt.Sprintf("%s", group.Data["name"]),
Date: time.Now(),
Comment: comment,
})
// Process extra properties
if len(extraProperties) > 0 {
propDefs := getBookingExtraProperties(h.config.Get("modules.vehicles.booking_extra_properties"))
// Build a lookup map of property definitions by name
propDefsMap := map[string]map[string]any{}
for _, def := range propDefs {
if name, ok := def["name"].(string); ok {
propDefsMap[name] = def
}
}
// Initialize extra_properties in booking data if needed
storedExtras, _ := booking.Data["extra_properties"].(map[string]any)
if storedExtras == nil {
storedExtras = map[string]any{}
}
for propName, propValue := range extraProperties {
if propValue == "" {
continue
}
def, exists := propDefsMap[propName]
if !exists {
continue
}
if target, ok := def["target"].(string); ok && target != "" {
// Update structural booking field
switch target {
case "unavailableto":
if t, err := time.Parse("2006-01-02", propValue); err == nil {
booking.Unavailableto = t
}
case "unavailablefrom":
if t, err := time.Parse("2006-01-02", propValue); err == nil {
booking.Unavailablefrom = t
}
case "enddate":
paris, _ := time.LoadLocation("Europe/Paris")
if t, err := time.ParseInLocation("2006-01-02T15:04", propValue, paris); err == nil {
booking.Enddate = t
if t.After(booking.Unavailableto) || t.Equal(booking.Unavailableto) {
booking.Unavailableto = t.Add(24 * time.Hour)
}
}
case "startdate":
paris, _ := time.LoadLocation("Europe/Paris")
if t, err := time.ParseInLocation("2006-01-02T15:04", propValue, paris); err == nil {
booking.Startdate = t
if t.Before(booking.Unavailablefrom) {
booking.Unavailablefrom = t
}
}
}
} else {
// Store in extra_properties
storedExtras[propName] = propValue
}
}
booking.Data["extra_properties"] = storedExtras
}
b, err := fleets.BookingFromStorageType(&booking)
if err != nil {
return fmt.Errorf("failed to convert booking: %w", err)
}
_, err = h.services.GRPC.Fleets.UpdateBooking(ctx, &fleets.UpdateBookingRequest{
Booking: b,
})
return err
}
// --- Computed extra properties ---
func (h *ApplicationHandler) computeExtraProperties(booking fleetsstorage.Booking) map[string]string {
defs := getBookingExtraProperties(h.config.Get("modules.vehicles.booking_extra_properties"))
extras, _ := booking.Data["extra_properties"].(map[string]any)
if extras == nil {
extras = map[string]any{}
}
result := map[string]string{}
for _, def := range defs {
defType, _ := def["type"].(string)
if defType != "computed" {
continue
}
name, _ := def["name"].(string)
operation, _ := def["operation"].(string)
unit, _ := def["unit"].(string)
operands := resolveStringSlice(def["operands"])
if name == "" || operation == "" || len(operands) == 0 {
continue
}
val := evaluateOperation(operation, operands, booking, extras)
if val != "" {
if unit != "" {
val = val + " " + unit
}
result[name] = val
}
}
return result
}
// resolveStringSlice handles both Go defaults ([]string) and YAML-parsed ([]interface{}) types.
func resolveStringSlice(raw interface{}) []string {
if s, ok := raw.([]string); ok {
return s
}
if rawSlice, ok := raw.([]interface{}); ok {
result := make([]string, 0, len(rawSlice))
for _, item := range rawSlice {
if str, ok := item.(string); ok {
result = append(result, str)
}
}
return result
}
return nil
}
func evaluateOperation(operation string, operands []string, booking fleetsstorage.Booking, extras map[string]any) string {
switch operation {
case "add":
if len(operands) < 2 {
return ""
}
a, okA := resolveNumericOperand(operands[0], booking, extras)
b, okB := resolveNumericOperand(operands[1], booking, extras)
if !okA || !okB {
return ""
}
return formatNumber(a + b)
case "subtract":
if len(operands) < 2 {
return ""
}
a, okA := resolveNumericOperand(operands[0], booking, extras)
b, okB := resolveNumericOperand(operands[1], booking, extras)
if !okA || !okB {
return ""
}
return formatNumber(a - b)
case "multiply":
if len(operands) < 2 {
return ""
}
a, okA := resolveNumericOperand(operands[0], booking, extras)
b, okB := resolveNumericOperand(operands[1], booking, extras)
if !okA || !okB {
return ""
}
return formatNumber(a * b)
case "divide":
if len(operands) < 2 {
return ""
}
a, okA := resolveNumericOperand(operands[0], booking, extras)
b, okB := resolveNumericOperand(operands[1], booking, extras)
if !okA || !okB || b == 0 {
return ""
}
return formatNumber(a / b)
case "duration":
if len(operands) < 2 {
return ""
}
d1, ok1 := resolveDateOperand(operands[0], booking, extras)
d2, ok2 := resolveDateOperand(operands[1], booking, extras)
if !ok1 || !ok2 {
return ""
}
return formatDuration(d2.Sub(d1))
}
return ""
}
func resolveNumericOperand(name string, booking fleetsstorage.Booking, extras map[string]any) (float64, bool) {
if v, ok := extras[name]; ok {
switch val := v.(type) {
case float64:
return val, true
case string:
if f, err := strconv.ParseFloat(val, 64); err == nil {
return f, true
}
}
}
return 0, false
}
func resolveDateOperand(name string, booking fleetsstorage.Booking, extras map[string]any) (time.Time, bool) {
if strings.HasPrefix(name, "booking.") {
field := strings.TrimPrefix(name, "booking.")
switch field {
case "startdate":
return booking.Startdate, !booking.Startdate.IsZero()
case "enddate":
return booking.Enddate, !booking.Enddate.IsZero()
case "unavailablefrom":
return booking.Unavailablefrom, !booking.Unavailablefrom.IsZero()
case "unavailableto":
return booking.Unavailableto, !booking.Unavailableto.IsZero()
}
}
if v, ok := extras[name]; ok {
if dateStr, ok := v.(string); ok {
for _, layout := range []string{"2006-01-02T15:04", "2006-01-02"} {
if t, err := time.Parse(layout, dateStr); err == nil {
return t, true
}
}
}
}
return time.Time{}, false
}
func formatNumber(v float64) string {
if v == math.Trunc(v) {
return strconv.FormatInt(int64(v), 10)
}
return strconv.FormatFloat(v, 'f', 2, 64)
}
// resolveMetaStatuses returns a set of status names matching the given meta_status values.
// metaValues is a set like {"open": true, "active": true}.
func (h *ApplicationHandler) resolveMetaStatuses(metaValues map[string]bool) map[string]bool {
result := map[string]bool{}
options := getStatusOptions(h.config.Get("modules.vehicles.status_options"))
for _, opt := range options {
ms, _ := opt["meta_status"].(string)
name, _ := opt["name"].(string)
if metaValues[ms] {
result[name] = true
}
}
return result
}
func formatDuration(d time.Duration) string {
if d < 0 {
d = -d
}
totalHours := int(d.Hours())
days := totalHours / 24
hours := totalHours % 24
parts := []string{}
if days > 0 {
s := "jour"
if days > 1 {
s = "jours"
}
parts = append(parts, fmt.Sprintf("%d %s", days, s))
}
if hours > 0 {
s := "heure"
if hours > 1 {
s = "heures"
}
parts = append(parts, fmt.Sprintf("%d %s", hours, s))
}
if len(parts) == 0 {
return "0 heure"
}
return strings.Join(parts, " ")
}

View File

@@ -210,6 +210,29 @@ func (h *ApplicationHandler) BookVehicle(ctx context.Context, vehicleID, benefic
Data: datapb,
}
if h.config.GetString("modules.vehicles.status_management") == "manual" {
options := getStatusOptions(h.config.Get("modules.vehicles.status_options"))
for _, opt := range options {
if initial, ok := opt["initial"].(bool); ok && initial {
if name, ok := opt["name"].(string); ok {
booking.ManualStatus = name
booking.StatusHistory = []*fleets.StatusHistoryEntry{
{
ToStatus: name,
UserId: currentUserID,
UserName: fmt.Sprintf("%s %s", currentUserClaims["first_name"], currentUserClaims["last_name"]),
GroupId: group.ID,
GroupName: fmt.Sprintf("%s", group.Data["name"]),
Date: timestamppb.Now(),
Comment: "Création de la réservation",
},
}
}
break
}
}
}
request := &fleets.CreateBookingRequest{
Booking: booking,
}

3
go.mod
View File

@@ -48,7 +48,7 @@ require (
git.coopgo.io/coopgo-platform/agenda v1.0.0
git.coopgo.io/coopgo-platform/carpool-service v0.0.0-20251008165122-38cb3c5ad9b4
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260
git.coopgo.io/coopgo-platform/fleets v1.1.0
git.coopgo.io/coopgo-platform/fleets v1.1.1-0.20260226165510-6007cffdf152
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386
@@ -164,7 +164,6 @@ require (
go.etcd.io/etcd/api/v3 v3.5.12 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.1.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.21.0 // indirect

8
go.sum
View File

@@ -8,8 +8,8 @@ git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss v0.0.0-20251
git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss v0.0.0-20251008142525-4392f227836a/go.mod h1:c9aJwNtY4PJuqAFYZ9afnx46UAZtWJ3P8ICZM02/DBA=
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260 h1:Li3dotY6raKu9+oxEgICU7nwdomYpjgu19i3mZNiqTc=
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260/go.mod h1:6cvvjv0RLSwBthIQ4TiuZoXFGvQXZ55hNSJchWXAgB4=
git.coopgo.io/coopgo-platform/fleets v1.1.0 h1:pfW/K3fWfap54yNfkLzBXjvOjjoTaEGFEqS/+VkHv7s=
git.coopgo.io/coopgo-platform/fleets v1.1.0/go.mod h1:nuK2mi1M2+DdntinqK/8C4ttW4WWyKCCY/xD1D7XjkE=
git.coopgo.io/coopgo-platform/fleets v1.1.1-0.20260226165510-6007cffdf152 h1:kczmeGHnihYQSopDzbQ23B+P8Fw3GuB6iACW7SuDE+s=
git.coopgo.io/coopgo-platform/fleets v1.1.1-0.20260226165510-6007cffdf152/go.mod h1:nuK2mi1M2+DdntinqK/8C4ttW4WWyKCCY/xD1D7XjkE=
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858 h1:4E0tbT8jj5oxaK66Ny61o7zqPaVc0qRN2cZG9IUR4Es=
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858/go.mod h1:TbR3g1Awa8hpAe6LR1z1EQbv2IBVgN5JQ/FjXfKX4K0=
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c h1:bY7PyrAgYY02f5IpDyf1WVfRqvWzivu31K6aEAYbWCw=
@@ -26,8 +26,6 @@ git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463 h1
git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463/go.mod h1:0fuGuYub5CBy9NB6YMqxawE0HoBaxPb9gmSw1gjfDy0=
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af h1:KxHim1dFcOVbFhRqelec8cJ65QBD2cma6eytW8llgYY=
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af/go.mod h1:mad9D+WICDdpJzB+8H/wEVVbllK2mU6VLVByrppc9x0=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc h1:NLU5DUo5Kt3jkPhV3KkqQMahTHIrGildBvYlHwJ6JmM=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee h1:aoXSugsrZrM8E3WhqOCM+bLgGdxdf7dZAxx/vfbYzWQ=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
@@ -362,8 +360,6 @@ go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.1.0 h1:/ELnVNjmfUKDsoBisXxuJL0noR9CfeUIrP7Yt3R+egg=
go.mongodb.org/mongo-driver/v2 v2.1.0/go.mod h1:AWiLRShSrk5RHQS3AEn3RL19rqOzVq49MCpWQ3x/huI=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=

View File

@@ -30,15 +30,25 @@ func (s BeneficiariesListState) JSONWithLimits(a int, b int) template.JS {
return s.JSON()
}
func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string, archived bool) {
func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string, archived bool, enrichedGeoFilters []map[string]string, selectedAddressGeo string) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.list.files")
geoFiltersEnabled := len(enrichedGeoFilters) > 0
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
state.ViewState = BeneficiariesListState{
state.ViewState = map[string]any{
"list": BeneficiariesListState{
Count: len(accounts),
CacheId: cacheid,
Beneficiaries: accounts,
Archived: archived,
},
"geography_filters_enabled": geoFiltersEnabled,
"geography_filters_list": enrichedGeoFilters,
"archived": archived,
"filters": map[string]any{
"beneficiary_address_geo": selectedAddressGeo,
},
}
renderer.Render("beneficiaries_list", w, r, files, state)
@@ -87,7 +97,7 @@ func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Requ
renderer.Render("beneficiaries_display", w, r, files, state)
}
func (renderer *Renderer) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request, beneficiary any) {
func (renderer *Renderer) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request, beneficiary mobilityaccountsstorage.Account) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.update.files")
profileFields := renderer.GlobalConfig.Get("modules.beneficiaries.profile_optional_fields")
@@ -96,6 +106,7 @@ func (renderer *Renderer) BeneficiaryUpdate(w http.ResponseWriter, r *http.Reque
"beneficiary": beneficiary,
"profile_optional_fields": profileFields,
}
state.DynamicData = beneficiary.Data
renderer.Render("beneficiaries_update", w, r, files, state)
}

View File

@@ -170,6 +170,14 @@ func IsGuaranteedTripMotivation(globalConfig *viper.Viper) func(string) bool {
}
}
// IsPast returns true if the given time is before the current time
func IsPast(d any) bool {
if date, ok := d.(time.Time); ok {
return date.Before(time.Now())
}
return false
}
// GetTemplateFuncMap returns the common template functions for rendering
func GetTemplateFuncMap(group groupsstorage.Group, globalConfig *viper.Viper, fileStorage filestorage.FileStorage) template.FuncMap {
return template.FuncMap{
@@ -191,6 +199,7 @@ func GetTemplateFuncMap(group groupsstorage.Group, globalConfig *viper.Viper, fi
"beneficiaryValidatedProfile": validatedprofile.ValidateProfile(globalConfig.Sub("modules.beneficiaries.validated_profile")),
"solidarityDriverValidatedProfile": validatedprofile.ValidateProfile(globalConfig.Sub("modules.solidarity_transport.drivers.validated_profile")),
"carpoolDriverValidatedProfile": validatedprofile.ValidateProfile(globalConfig.Sub("modules.organized_carpool.drivers.validated_profile")),
"isPast": IsPast,
"isGuaranteedTripMotivation": IsGuaranteedTripMotivation(globalConfig),
"beneficiaryDocuments": func(id string) []filestorage.FileInfo {
return fileStorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + id)

View File

@@ -36,7 +36,7 @@ func (s BeneficiariesCovoiturage) JSONWithLimits(a int, b int) template.JS {
return s.JSON()
}
func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request, carpools []*geojson.FeatureCollection, transitjourneys any, vehicles any, searched bool, departure any, destination any, departuredate string, departuretime string, driverJourneys any, solidarityDrivers any, organizedCarpools any, beneficiaries any, kbData any, passengerid string, savedSearches any, beneficiariesMap any) {
func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request, carpools []*geojson.FeatureCollection, transitjourneys any, vehicles any, searched bool, departure any, destination any, departuredate string, departuretime string, departuredatetime any, driverJourneys any, solidarityDrivers any, organizedCarpools any, beneficiaries any, kbData any, passengerid string, savedSearches any, beneficiariesMap any, driverLastTrips any, lastTripDays int) {
files := renderer.ThemeConfig.GetStringSlice("views.journeys.search.files")
state := NewState(r, renderer.ThemeConfig, journeysMenu)
journeyTabs := renderer.ThemeConfig.Get("journey_tabs")
@@ -44,6 +44,7 @@ func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request,
"searched": searched,
"departuredate": departuredate,
"departuretime": departuretime,
"departuredatetime": departuredatetime,
"departure": departure,
"destination": destination,
"journeys": transitjourneys,
@@ -52,6 +53,8 @@ func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request,
"vehicles": vehicles,
"driver_journeys": driverJourneys,
"solidarity_drivers": solidarityDrivers,
"driver_last_trips": driverLastTrips,
"last_trip_days": lastTripDays,
"querystring": r.URL.RawQuery,
"beneficiaries": beneficiariesMap,
"beneficiaries_list": beneficiaries,

View File

@@ -56,6 +56,7 @@ func (renderer *Renderer) OrganizedCarpoolUpdateDriver(w http.ResponseWriter, r
func (renderer *Renderer) OrganizedCarpoolDriverDisplay(w http.ResponseWriter, r *http.Request, driver mobilityaccountsstorage.Account, trips any, documents any, bookings any, beneficiariesMap any, stats map[string]any, walletBalance float64, tab string) {
files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.driver_display.files")
profileFields := renderer.GlobalConfig.Get("modules.organized_carpool.drivers.profile_optional_fields")
state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
drivers_file_types := renderer.GlobalConfig.GetStringSlice("modules.organized_carpool.drivers.documents_types")
@@ -70,6 +71,7 @@ func (renderer *Renderer) OrganizedCarpoolDriverDisplay(w http.ResponseWriter, r
"stats": stats,
"drivers_file_types": drivers_file_types,
"file_types_map": file_types_map,
"profile_optional_fields": profileFields,
"wallet_balance": walletBalance,
"tab": tab,
}

View File

@@ -110,6 +110,7 @@ type RenderState struct {
Group storage.Group
Roles any
ViewState any // This is a state specific to a given view
DynamicData any // Data to be serialized as JSON in a <script> tag for safe JS consumption
}
func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) RenderState {

20
renderer/search.go Normal file
View File

@@ -0,0 +1,20 @@
package renderer
import (
"net/http"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
func (renderer *Renderer) GlobalSearchResults(w http.ResponseWriter, r *http.Request, query string, beneficiaries []mobilityaccountsstorage.Account, solidarityDrivers []mobilityaccountsstorage.Account, organizedCarpoolDrivers []mobilityaccountsstorage.Account) {
files := renderer.ThemeConfig.GetStringSlice("views.search.results.files")
state := NewState(r, renderer.ThemeConfig, "")
state.ViewState = map[string]any{
"query": query,
"beneficiaries": beneficiaries,
"solidarity_drivers": solidarityDrivers,
"organized_carpool_drivers": organizedCarpoolDrivers,
}
renderer.Render("search results", w, r, files, state)
}

View File

@@ -109,7 +109,7 @@ func (renderer *Renderer) SolidarityTransportDriverJourney(w http.ResponseWriter
renderer.Render("solidarity transport driver creation", w, r, files, state)
}
func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, passengerWalletBalance float64, replacementDrivers any, replacementDriversMap any, replacementPricing any, replacementLocations any) {
func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, passengerWalletBalance float64, replacementDrivers any, replacementDriversMap any, replacementPricing any, replacementLocations any, driverLastTrips any, lastTripDays int) {
files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.booking_display.files")
bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
@@ -124,6 +124,8 @@ func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWrite
"replacement_drivers_map": replacementDriversMap,
"replacement_pricing": replacementPricing,
"replacement_locations": replacementLocations,
"driver_last_trips": driverLastTrips,
"last_trip_days": lastTripDays,
}
renderer.Render("booking display", w, r, files, state)

View File

@@ -10,7 +10,7 @@ import (
const vehiclesmanagementMenu = "vehicles_management"
func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request, vehicles []fleetsstorage.Vehicle, vehicles_map map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking) {
func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request, vehicles []fleetsstorage.Vehicle, vehicles_map map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking, filters map[string]string, vehicleTypes []string, tab string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.overview.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
@@ -18,12 +18,18 @@ func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *h
"bookings": bookings,
"vehicles_map": vehicles_map,
"drivers_map": driversMap,
"tab": tab,
"filters": filters,
"vehicle_types": vehicleTypes,
"hide_date_filters": false,
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
}
renderer.Render("fleet overview", w, r, files, state)
}
func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request, vehiclesMap map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking, cacheid string, filters map[string]string) {
func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request, vehiclesMap map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking, cacheid string, filters map[string]string, vehicleTypes []string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.bookings_list.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
@@ -32,6 +38,9 @@ func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter,
"drivers_map": driversMap,
"cacheid": cacheid,
"filters": filters,
"vehicle_types": vehicleTypes,
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
}
renderer.Render("fleet overview", w, r, files, state)
@@ -78,7 +87,7 @@ func (renderer *Renderer) VehiclesFleetUpdate(w http.ResponseWriter, r *http.Req
renderer.Render("fleet display vehicle", w, r, files, state)
}
func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any, documents []filestorage.FileInfo, file_types_map map[string]string, alternative_vehicles []any) {
func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any, documents []filestorage.FileInfo, file_types_map map[string]string, alternative_vehicles []any, computed_extra_properties map[string]string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.booking_display.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
@@ -89,6 +98,10 @@ func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter,
"documents": documents,
"file_types_map": file_types_map,
"alternative_vehicles": alternative_vehicles,
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
"booking_extra_properties": renderer.GlobalConfig.Get("modules.vehicles.booking_extra_properties"),
"computed_extra_properties": computed_extra_properties,
}
renderer.Render("vehicles search", w, r, files, state)

View File

@@ -26,10 +26,13 @@ func selectDocumentsDefaults(beneficiarydocuments []filestorage.FileInfo, mandat
func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []fleetsstorage.Vehicle, beneficiary any, startdate any, enddate any, mandatory_documents []string, file_types_map map[string]string, beneficiarydocuments []filestorage.FileInfo, selected_type string, automatic bool, vehicles_types []string, admingroups map[string]any) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles.search.files")
state := NewState(r, renderer.ThemeConfig, vehiclesMenu)
defaultBookingDurationDays := renderer.GlobalConfig.GetInt("modules.vehicles.default_booking_duration_days")
viewstate := map[string]any{
"beneficiaries": beneficiaries,
"searched": searched,
"vehicles_types": vehicles_types,
"default_booking_duration_days": defaultBookingDurationDays,
}
if searched {
@@ -63,6 +66,8 @@ func (renderer *Renderer) VehicleBookingDisplay(w http.ResponseWriter, r *http.R
"group": group,
"documents": documents,
"file_types_map": file_types_map,
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
}
renderer.Render("vehicles search", w, r, files, state)
@@ -75,6 +80,8 @@ func (renderer *Renderer) VehicleBookingsList(w http.ResponseWriter, r *http.Req
"bookings": bookings,
"vehicles_map": vehiclesMap,
"groups_map": groupsMap,
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
}
renderer.Render("vehicles search", w, r, files, state)

View File

@@ -0,0 +1,99 @@
package xlsx
import (
"fmt"
"net/http"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/gender"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
"github.com/rs/zerolog/log"
)
type BeneficiaryGeoInfo struct {
Commune string
EPCI string
Departement string
Region string
}
func (r *XLSXRenderer) Beneficiaries(w http.ResponseWriter, accounts []mobilityaccountsstorage.Account, geoInfoMap map[string]BeneficiaryGeoInfo) {
spreadsheet := r.NewSpreadsheet("Bénéficiaires")
// Build headers dynamically based on configuration
beneficiaryOptionalFields := r.Config.Get("modules.beneficiaries.profile_optional_fields")
beneficiaryFields := []string{"last_name", "first_name", "email", "phone_number", "birthdate", "gender", "file_number"}
headers := []string{"ID", "Nom", "Prénom", "Email", "Téléphone", "Date de naissance", "Genre", "Numéro de dossier"}
if beneficiaryOptionalFieldsList, ok := beneficiaryOptionalFields.([]interface{}); ok {
for _, field := range beneficiaryOptionalFieldsList {
if fieldMap, ok := field.(map[string]interface{}); ok {
if name, ok := fieldMap["name"].(string); ok {
beneficiaryFields = append(beneficiaryFields, name)
label := name
if labelVal, ok := fieldMap["label"].(string); ok {
label = labelVal
}
headers = append(headers, label)
}
}
}
}
headers = append(headers, "Adresse", "Commune", "EPCI", "Département", "Région", "Archivé")
spreadsheet.SetHeaders(headers)
for _, account := range accounts {
row := []interface{}{}
row = append(row, account.ID)
for _, field := range beneficiaryFields {
value := getAccountFieldValue(account.Data, field)
if field == "gender" && value != "" {
value = gender.ISO5218ToString(value)
}
row = append(row, value)
}
// Address
address := ""
if addr, ok := account.Data["address"]; ok {
if addrMap, ok := addr.(map[string]interface{}); ok {
if props, ok := addrMap["properties"]; ok {
if propsMap, ok := props.(map[string]interface{}); ok {
if label, ok := propsMap["label"].(string); ok {
address = label
}
}
}
}
}
row = append(row, address)
// Geographic info (Commune, EPCI, Département, Région)
geoInfo := geoInfoMap[account.ID]
row = append(row, geoInfo.Commune)
row = append(row, geoInfo.EPCI)
row = append(row, geoInfo.Departement)
row = append(row, geoInfo.Region)
// Archived status
archived := "Non"
if archivedVal, ok := account.Data["archived"].(bool); ok && archivedVal {
archived = "Oui"
}
row = append(row, archived)
spreadsheet.AddRow(row)
}
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"export-beneficiaires.xlsx\""))
if err := spreadsheet.GetFile().Write(w); err != nil {
log.Error().Err(err).Msg("Error generating Excel file")
http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
return
}
}

View File

@@ -10,6 +10,31 @@ import (
"github.com/rs/zerolog/log"
)
// resolveStatusLabel returns the display label for a manual status name
func resolveStatusLabel(statusOptions interface{}, manualStatus string) string {
switch opts := statusOptions.(type) {
case []map[string]any:
for _, opt := range opts {
if name, _ := opt["name"].(string); name == manualStatus {
if label, ok := opt["label"].(string); ok {
return label
}
}
}
case []interface{}:
for _, opt := range opts {
if optMap, ok := opt.(map[string]interface{}); ok {
if name, _ := optMap["name"].(string); name == manualStatus {
if label, ok := optMap["label"].(string); ok {
return label
}
}
}
}
}
return manualStatus
}
func (r *XLSXRenderer) VehicleBookings(w http.ResponseWriter, bookings []fleetsstorage.Booking, vehiclesMap map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account) {
// Create Excel spreadsheet
spreadsheet := r.NewSpreadsheet("Réservations véhicules")
@@ -55,6 +80,10 @@ func (r *XLSXRenderer) VehicleBookings(w http.ResponseWriter, bookings []fleetss
spreadsheet.SetHeaders(headers)
// Read status management config
isManualStatus := r.Config.GetString("modules.vehicles.status_management") == "manual"
statusOptions := r.Config.Get("modules.vehicles.status_options")
// Add data rows
for _, booking := range bookings {
vehicle := vehiclesMap[booking.Vehicleid]
@@ -69,6 +98,8 @@ func (r *XLSXRenderer) VehicleBookings(w http.ResponseWriter, bookings []fleetss
status := ""
if booking.Deleted {
status = "Annulé"
} else if isManualStatus {
status = resolveStatusLabel(statusOptions, booking.ManualStatus)
} else {
switch booking.Status() {
case 1:
@@ -216,6 +247,10 @@ func (r *XLSXRenderer) VehicleBookingsAdmin(w http.ResponseWriter, bookings []fl
spreadsheet.SetHeaders(headers)
// Read status management config
isManualStatusAdmin := r.Config.GetString("modules.vehicles.status_management") == "manual"
statusOptionsAdmin := r.Config.Get("modules.vehicles.status_options")
// Add data rows
for _, booking := range bookings {
// Get vehicle from map
@@ -243,6 +278,8 @@ func (r *XLSXRenderer) VehicleBookingsAdmin(w http.ResponseWriter, bookings []fl
status := ""
if booking.Deleted {
status = "Annulé"
} else if isManualStatusAdmin {
status = resolveStatusLabel(statusOptionsAdmin, booking.ManualStatus)
} else {
switch booking.Status() {
case 1:

View File

@@ -0,0 +1,9 @@
package web
import (
"github.com/gorilla/mux"
)
func (ws *WebServer) setupSearchRoutes(appRouter *mux.Router) {
appRouter.HandleFunc("/search", ws.appHandler.GlobalSearchHTTPHandler())
}

View File

@@ -17,6 +17,7 @@ func (ws *WebServer) setupVehiclesManagementRoutes(appRouter *mux.Router) {
// Bookings
vehiclesManagement.HandleFunc("/bookings/", ws.appHandler.VehiclesManagementBookingsListHTTPHandler())
vehiclesManagement.HandleFunc("/bookings/{bookingid}", ws.appHandler.VehicleManagementBookingDisplayHTTPHandler())
vehiclesManagement.HandleFunc("/bookings/{bookingid}/status", ws.appHandler.VehicleManagementUpdateBookingStatusHTTPHandler()).Methods("POST")
vehiclesManagement.HandleFunc("/bookings/{bookingid}/change-vehicle", ws.appHandler.VehicleManagementBookingChangeVehicleHTTPHandler())
vehiclesManagement.HandleFunc("/bookings/{bookingid}/delete", ws.appHandler.DeleteBookingHTTPHandler())
vehiclesManagement.HandleFunc("/bookings/{bookingid}/unbooking", ws.appHandler.UnbookingVehicleHTTPHandler())

View File

@@ -5,7 +5,9 @@ import (
"fmt"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
@@ -83,14 +85,64 @@ func (h *Handler) BeneficiariesListHTTPHandler() http.HandlerFunc {
archivedFilter = true
}
result, err := h.applicationHandler.GetBeneficiaries(r.Context(), searchFilter, archivedFilter)
// Extract beneficiary address geography filter
beneficiaryAddressGeo := r.URL.Query().Get("beneficiary_address_geo")
addressGeoLayer, addressGeoCode := "", ""
if beneficiaryAddressGeo != "" {
parts := strings.SplitN(beneficiaryAddressGeo, ":", 2)
if len(parts) == 2 {
addressGeoLayer, addressGeoCode = parts[0], parts[1]
}
}
result, err := h.applicationHandler.GetBeneficiaries(r.Context(), searchFilter, archivedFilter, addressGeoLayer, addressGeoCode)
if err != nil {
log.Error().Err(err).Msg("error retrieving beneficiaries")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
h.renderer.BeneficiariesList(w, r, result.Accounts, result.CacheID, archivedFilter)
// Enrich geography filters with names from geography service
var enrichedGeoFilters []map[string]string
if h.cfg.GetBool("geography.filters.enabled") {
geoFilters := h.cfg.Get("geography.filters.geographies")
if geoList, ok := geoFilters.([]any); ok {
for _, geoItem := range geoList {
if geoMap, ok := geoItem.(map[string]any); ok {
layer := ""
code := ""
if l, ok := geoMap["layer"].(string); ok {
layer = l
}
if c, ok := geoMap["code"].(string); ok {
code = c
}
enrichedGeo := map[string]string{
"layer": layer,
"code": code,
"name": code,
}
if layer != "" && code != "" {
if geoFeature, err := h.services.Geography.Find(layer, code); err == nil {
if name := geoFeature.Properties.MustString("nom"); name != "" {
enrichedGeo["name"] = name
}
}
}
enrichedGeoFilters = append(enrichedGeoFilters, enrichedGeo)
}
}
}
sort.Slice(enrichedGeoFilters, func(i, j int) bool {
return enrichedGeoFilters[i]["name"] < enrichedGeoFilters[j]["name"]
})
}
h.renderer.BeneficiariesList(w, r, result.Accounts, result.CacheID, archivedFilter, enrichedGeoFilters, beneficiaryAddressGeo)
}
}

View File

@@ -159,6 +159,7 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
destinationGeo,
departureDate,
departureTime,
departureDateTime,
result.DriverJourneys,
result.Drivers,
result.OrganizedCarpools,
@@ -167,6 +168,8 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
passengerID,
savedSearches,
beneficiariesMap,
result.DriverLastTrips,
result.LastTripDays,
)
}
}

View File

@@ -0,0 +1,26 @@
package application
import (
"net/http"
"github.com/rs/zerolog/log"
)
func (h *Handler) GlobalSearchHTTPHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Redirect(w, r, "/app/", http.StatusFound)
return
}
result, err := h.applicationHandler.GlobalSearch(r.Context(), query)
if err != nil {
log.Error().Err(err).Msg("error performing global search")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
h.renderer.GlobalSearchResults(w, r, query, result.Beneficiaries, result.SolidarityDrivers, result.OrganizedCarpoolDrivers)
}
}

View File

@@ -9,6 +9,7 @@ import (
"strings"
"time"
coreapplication "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
@@ -614,12 +615,17 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
var replacementDriversMap map[string]mobilityaccountsstorage.Account
var replacementPricing map[string]map[string]interface{}
var replacementLocations map[string]string
var driverLastTrips map[string]time.Time
var lastTripDays int
if result.Booking.Status == "CANCELLED" {
// Initialize maps to avoid nil pointer in template
replacementDriversMap = make(map[string]mobilityaccountsstorage.Account)
replacementPricing = make(map[string]map[string]interface{})
replacementLocations = make(map[string]string)
driverLastTrips = make(map[string]time.Time)
// Preserve the original booking's noreturn status when searching for replacement drivers
noreturn := result.Booking.Journey.Noreturn
searchResult, err := h.applicationHandler.SearchJourneys(
r.Context(),
result.Booking.Journey.PassengerPickupDate,
@@ -628,11 +634,15 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
result.Booking.PassengerId,
result.Booking.DriverId, // Exclude the original driver
result.Booking.GroupId, // Exclude drivers with bookings in this group
nil, // options - use defaults
&coreapplication.SearchJourneyOptions{
SolidarityTransportNoreturn: &noreturn,
},
)
if err == nil {
replacementDrivers = searchResult.DriverJourneys
replacementDriversMap = searchResult.Drivers
driverLastTrips = searchResult.DriverLastTrips
lastTripDays = searchResult.LastTripDays
// Calculate pricing for each replacement driver journey
for _, dj := range searchResult.DriverJourneys {
@@ -668,7 +678,7 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
}
}
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance, replacementDrivers, replacementDriversMap, replacementPricing, replacementLocations)
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance, replacementDrivers, replacementDriversMap, replacementPricing, replacementLocations, driverLastTrips, lastTripDays)
}
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
@@ -22,14 +23,37 @@ func (h *Handler) VehiclesManagementOverviewHTTPHandler() http.HandlerFunc {
groupID = group.ID
}
result, err := h.applicationHandler.GetVehiclesManagementOverview(r.Context(), groupID)
// Extract tab and filter parameters from query
tab := r.URL.Query().Get("tab")
vehicleType := r.URL.Query().Get("vehicle_type")
status := r.URL.Query().Get("status")
if _, hasStatus := r.URL.Query()["status"]; !hasStatus {
status = "meta:open,active"
}
dateStart := r.URL.Query().Get("date_start")
dateEnd := r.URL.Query().Get("date_end")
vType := r.URL.Query().Get("v_type")
vStatus := r.URL.Query().Get("v_status")
result, err := h.applicationHandler.GetVehiclesManagementOverview(r.Context(), groupID, vehicleType, status, dateStart, dateEnd, vType, vStatus)
if err != nil {
log.Error().Err(err).Msg("error retrieving vehicles management overview")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
h.renderer.VehiclesManagementOverview(w, r, result.Vehicles, result.VehiclesMap, result.DriversMap, result.Bookings)
filters := map[string]string{
"vehicle_type": vehicleType,
"status": status,
"date_start": dateStart,
"date_end": dateEnd,
"v_type": vType,
"v_status": vStatus,
}
vehicleTypes, _ := h.applicationHandler.GetVehicleTypes(r.Context())
h.renderer.VehiclesManagementOverview(w, r, result.Vehicles, result.VehiclesMap, result.DriversMap, result.Bookings, filters, vehicleTypes, tab)
}
}
@@ -46,6 +70,7 @@ func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc {
status := r.URL.Query().Get("status")
dateStart := r.URL.Query().Get("date_start")
dateEnd := r.URL.Query().Get("date_end")
vehicleType := r.URL.Query().Get("vehicle_type")
// Default to last month if no dates specified
if dateStart == "" {
@@ -55,7 +80,7 @@ func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc {
dateEnd = time.Now().Format("2006-01-02")
}
result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), groupID, status, dateStart, dateEnd)
result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), groupID, status, dateStart, dateEnd, vehicleType)
if err != nil {
log.Error().Err(err).Msg("error retrieving vehicles management bookings list")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -67,9 +92,12 @@ func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc {
"status": status,
"date_start": dateStart,
"date_end": dateEnd,
"vehicle_type": vehicleType,
}
h.renderer.VehiclesManagementBookingsList(w, r, result.VehiclesMap, result.DriversMap, result.Bookings, result.CacheID, filters)
vehicleTypes, _ := h.applicationHandler.GetVehicleTypes(r.Context())
h.renderer.VehiclesManagementBookingsList(w, r, result.VehiclesMap, result.DriversMap, result.Bookings, result.CacheID, filters, vehicleTypes)
}
}
@@ -223,7 +251,7 @@ func (h *Handler) VehicleManagementBookingDisplayHTTPHandler() http.HandlerFunc
return
}
h.renderer.VehicleManagementBookingDisplay(w, r, result.Booking, result.Vehicle, result.Beneficiary, result.Group, result.Documents, result.FileTypesMap, result.Alternatives)
h.renderer.VehicleManagementBookingDisplay(w, r, result.Booking, result.Vehicle, result.Beneficiary, result.Group, result.Documents, result.FileTypesMap, result.Alternatives, result.ComputedExtraProperties)
}
}
@@ -355,6 +383,38 @@ func (h *Handler) UnbookingVehicleHTTPHandler() http.HandlerFunc {
}
}
func (h *Handler) VehicleManagementUpdateBookingStatusHTTPHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bookingID := vars["bookingid"]
r.ParseForm()
newStatus := r.FormValue("new_status")
comment := r.FormValue("comment")
// Collect extra properties from prop_* form fields
extraProperties := map[string]string{}
for key, values := range r.Form {
if strings.HasPrefix(key, "prop_") && len(values) > 0 {
extraProperties[strings.TrimPrefix(key, "prop_")] = values[0]
}
}
currentUserToken := r.Context().Value(identification.IdtokenKey).(*oidc.IDToken)
currentUserClaims := r.Context().Value(identification.ClaimsKey).(map[string]any)
currentGroup := r.Context().Value(identification.GroupKey)
err := h.applicationHandler.UpdateBookingStatus(r.Context(), bookingID, newStatus, comment, currentUserToken.Subject, currentUserClaims, currentGroup, extraProperties)
if err != nil {
log.Error().Err(err).Msg("error updating booking status")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/bookings/%s", bookingID), http.StatusFound)
}
}
func (h *Handler) VehiclesFleetUpdateHTTPHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

View File

@@ -8,6 +8,7 @@ func (ws *WebServer) setupApplicationRoutes(r *mux.Router) {
application := r.PathPrefix("/app").Subrouter()
// Setup all application route groups
ws.setupSearchRoutes(application)
ws.setupDashboardRoutes(application)
setupMiscRoutes(application, ws.applicationHandler)
ws.setupDirectoryRoutes(application)

View File

@@ -0,0 +1,63 @@
package exports
import (
"encoding/json"
"net/http"
"strings"
xlsxrenderer "git.coopgo.io/coopgo-apps/parcoursmob/renderer/xlsx"
"github.com/paulmach/orb/geojson"
"github.com/rs/zerolog/log"
)
func (h *Handler) Beneficiaries(w http.ResponseWriter, r *http.Request) {
archivedFilter := r.URL.Query().Get("archived") == "true"
beneficiaryAddressGeo := r.URL.Query().Get("beneficiary_address_geo")
addressGeoLayer, addressGeoCode := "", ""
if beneficiaryAddressGeo != "" {
parts := strings.SplitN(beneficiaryAddressGeo, ":", 2)
if len(parts) == 2 {
addressGeoLayer, addressGeoCode = parts[0], parts[1]
}
}
result, err := h.applicationHandler.GetBeneficiaries(r.Context(), "", archivedFilter, addressGeoLayer, addressGeoCode)
if err != nil {
log.Error().Err(err).Msg("Failed to get beneficiaries")
w.WriteHeader(http.StatusInternalServerError)
return
}
// Resolve geographic layers (EPCI, Département, Région) for each beneficiary
geoInfoMap := map[string]xlsxrenderer.BeneficiaryGeoInfo{}
for _, account := range result.Accounts {
if addr, ok := account.Data["address"]; ok {
jsonAddr, err := json.Marshal(addr)
if err == nil {
addrFeature, err := geojson.UnmarshalFeature(jsonAddr)
if err == nil && addrFeature.Geometry != nil {
geo, err := h.services.Geography.GeoSearch(addrFeature)
if err == nil {
info := xlsxrenderer.BeneficiaryGeoInfo{}
if commune, ok := geo["communes"]; ok {
info.Commune = commune.Properties.MustString("nom")
}
if epci, ok := geo["epci"]; ok {
info.EPCI = epci.Properties.MustString("nom")
}
if dept, ok := geo["departements"]; ok {
info.Departement = dept.Properties.MustString("nom")
}
if region, ok := geo["regions"]; ok {
info.Region = region.Properties.MustString("nom")
}
geoInfoMap[account.ID] = info
}
}
}
}
}
h.renderer.XLSX.Beneficiaries(w, result.Accounts, geoInfoMap)
}

View File

@@ -53,7 +53,9 @@ func (h *Handler) FleetBookingsInGroup(w http.ResponseWriter, r *http.Request) {
dateStart := r.URL.Query().Get("date_start")
dateEnd := r.URL.Query().Get("date_end")
result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), group.ID, status, dateStart, dateEnd)
vehicleType := r.URL.Query().Get("vehicle_type")
result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), group.ID, status, dateStart, dateEnd, vehicleType)
if err != nil {
log.Error().Err(err).Msg("Failed to get vehicle bookings for export")
w.WriteHeader(http.StatusInternalServerError)

View File

@@ -6,6 +6,7 @@ import (
"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"github.com/spf13/viper"
)
@@ -14,14 +15,16 @@ type Handler struct {
applicationHandler *application.ApplicationHandler
idp *identification.IdentificationProvider
renderer *renderer.Renderer
services *services.ServicesHandler
}
func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer) *Handler {
func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer, services *services.ServicesHandler) *Handler {
return &Handler{
config: cfg,
applicationHandler: applicationHandler,
idp: idp,
renderer: renderer,
services: services,
}
}

View File

@@ -14,6 +14,7 @@ func (ws *WebServer) setupExportsRoutes(r *mux.Router) {
export.HandleFunc("/solidarity-transport/drivers.xlsx", ws.exportsHandler.SolidarityTransportDrivers)
export.HandleFunc("/organized-carpool/bookings.xlsx", ws.exportsHandler.OrganizedCarpoolBookings)
export.HandleFunc("/organized-carpool/drivers.xlsx", ws.exportsHandler.OrganizedCarpoolDrivers)
export.HandleFunc("/beneficiaries/beneficiaries.xlsx", ws.exportsHandler.Beneficiaries)
export.Use(ws.idp.Middleware)
export.Use(ws.idp.GroupsMiddleware)
}

View File

@@ -58,7 +58,7 @@ func Run(cfg *viper.Viper, services *services.ServicesHandler, renderer *rendere
// Initialize web handler subpackages
appHandler: webapplication.NewHandler(cfg, renderer, applicationHandler, idp, services),
authHandler: webauth.NewHandler(cfg, applicationHandler, idp, renderer),
exportsHandler: webexports.NewHandler(cfg, applicationHandler, idp, renderer),
exportsHandler: webexports.NewHandler(cfg, applicationHandler, idp, renderer, services),
extHandler: webexternal.NewHandler(cfg, applicationHandler, filestorage),
protectedAPIHandler: webprotectedapi.NewHandler(cfg, applicationHandler),
webAPIHandler: webapi.NewHandler(cfg, idp, applicationHandler, cacheHandler),