From 95365ff8ce45f6deb7fff71b6df88c6e0d2bc3f6 Mon Sep 17 00:00:00 2001 From: Arnaud Delcasse Date: Thu, 26 Feb 2026 17:56:25 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20gestion=20manuelle=20des=20statuts=20de?= =?UTF-8?q?=20r=C3=A9servation=20v=C3=A9hicule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.go | 12 +- core/application/vehicles-management.go | 116 ++++++++++++++---- core/application/vehicles.go | 23 ++++ go.mod | 3 +- go.sum | 8 +- renderer/vehicle-management.go | 24 ++-- renderer/vehicles.go | 22 ++-- servers/web/app_vehicles_management_routes.go | 1 + .../web/application/vehicles_management.go | 24 ++++ 9 files changed, 182 insertions(+), 51 deletions(-) diff --git a/config.go b/config.go index 2cf30eb..407b75c 100755 --- a/config.go +++ b/config.go @@ -231,8 +231,16 @@ func ReadConfig() (*viper.Viper, error) { }, }, "vehicles": map[string]any{ - "enabled": true, - "default_booking_duration_days": 90, + "enabled": true, + "default_booking_duration_days": 90, + "status_management": "automatic", + "status_options": []map[string]any{ + {"name": "requested", "label": "Demandé", "initial": true, "meta_status": "open"}, + {"name": "accepted", "label": "Accepté", "meta_status": "active"}, + {"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, diff --git a/core/application/vehicles-management.go b/core/application/vehicles-management.go index 5dd32b3..878c702 100755 --- a/core/application/vehicles-management.go +++ b/core/application/vehicles-management.go @@ -132,32 +132,38 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte if v, ok := b.Data["administrator_unavailability"].(bool); !ok || !v { // Apply status filter if status != "" { - bookingStatus := b.Status() - statusInt := 0 - - if b.Deleted { - statusInt = -2 // Use -2 for cancelled to distinguish from terminated + if h.config.GetString("modules.vehicles.status_management") == "manual" { + if b.ManualStatus != status { + continue + } } else { - statusInt = bookingStatus - } + bookingStatus := b.Status() + statusInt := 0 - // Map status string to int - var filterStatusInt int - switch status { - case "FORTHCOMING": - filterStatusInt = 1 - case "ONGOING": - filterStatusInt = 0 - case "TERMINATED": - filterStatusInt = -1 - case "CANCELLED": - filterStatusInt = -2 - default: - filterStatusInt = 999 // Invalid status, won't match anything - } + if b.Deleted { + statusInt = -2 // Use -2 for cancelled to distinguish from terminated + } else { + statusInt = bookingStatus + } - if statusInt != filterStatusInt { - continue + // Map status string to int + var filterStatusInt int + switch status { + case "FORTHCOMING": + filterStatusInt = 1 + case "ONGOING": + filterStatusInt = 0 + case "TERMINATED": + filterStatusInt = -1 + case "CANCELLED": + filterStatusInt = -2 + default: + filterStatusInt = 999 // Invalid status, won't match anything + } + + if statusInt != filterStatusInt { + continue + } } } @@ -596,3 +602,67 @@ 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 +} + +func (h *ApplicationHandler) UpdateBookingStatus(ctx context.Context, bookingID, newStatus, comment, userID string, userClaims map[string]any, currentGroup any) 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, + }) + + 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 +} + diff --git a/core/application/vehicles.go b/core/application/vehicles.go index 6ad30c2..ecb44f9 100755 --- a/core/application/vehicles.go +++ b/core/application/vehicles.go @@ -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, } diff --git a/go.mod b/go.mod index 457c1ab..fc996f4 100755 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index d38c0ce..943a8c0 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/renderer/vehicle-management.go b/renderer/vehicle-management.go index 25e62f1..e477865 100755 --- a/renderer/vehicle-management.go +++ b/renderer/vehicle-management.go @@ -14,10 +14,12 @@ func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *h files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.overview.files") state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu) state.ViewState = map[string]any{ - "vehicles": vehicles, - "bookings": bookings, - "vehicles_map": vehicles_map, - "drivers_map": driversMap, + "vehicles": vehicles, + "bookings": bookings, + "vehicles_map": vehicles_map, + "drivers_map": driversMap, + "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) @@ -27,11 +29,13 @@ func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter, files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.bookings_list.files") state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu) state.ViewState = map[string]any{ - "bookings": bookings, - "vehicles_map": vehiclesMap, - "drivers_map": driversMap, - "cacheid": cacheid, - "filters": filters, + "bookings": bookings, + "vehicles_map": vehiclesMap, + "drivers_map": driversMap, + "cacheid": cacheid, + "filters": filters, + "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) @@ -89,6 +93,8 @@ 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"), } renderer.Render("vehicles search", w, r, files, state) diff --git a/renderer/vehicles.go b/renderer/vehicles.go index 77b5666..4522f26 100755 --- a/renderer/vehicles.go +++ b/renderer/vehicles.go @@ -60,12 +60,14 @@ func (renderer *Renderer) VehicleBookingDisplay(w http.ResponseWriter, r *http.R files := renderer.ThemeConfig.GetStringSlice("views.vehicles.booking_display.files") state := NewState(r, renderer.ThemeConfig, vehiclesMenu) state.ViewState = map[string]any{ - "booking": booking, - "vehicle": vehicle, - "beneficiary": beneficiary, - "group": group, - "documents": documents, - "file_types_map": file_types_map, + "booking": booking, + "vehicle": vehicle, + "beneficiary": beneficiary, + "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,9 +77,11 @@ func (renderer *Renderer) VehicleBookingsList(w http.ResponseWriter, r *http.Req files := renderer.ThemeConfig.GetStringSlice("views.vehicles.bookings_list.files") state := NewState(r, renderer.ThemeConfig, vehiclesMenu) state.ViewState = map[string]any{ - "bookings": bookings, - "vehicles_map": vehiclesMap, - "groups_map": groupsMap, + "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) diff --git a/servers/web/app_vehicles_management_routes.go b/servers/web/app_vehicles_management_routes.go index 29e6132..80ae0ec 100644 --- a/servers/web/app_vehicles_management_routes.go +++ b/servers/web/app_vehicles_management_routes.go @@ -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()) diff --git a/servers/web/application/vehicles_management.go b/servers/web/application/vehicles_management.go index f57aef0..1de4776 100644 --- a/servers/web/application/vehicles_management.go +++ b/servers/web/application/vehicles_management.go @@ -355,6 +355,30 @@ 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") + + 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) + 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)