feat: gestion manuelle des statuts de réservation véhicule

This commit is contained in:
Arnaud Delcasse
2026-02-26 17:56:25 +01:00
parent b79cc08b06
commit 95365ff8ce
9 changed files with 182 additions and 51 deletions

View File

@@ -233,6 +233,14 @@ func ReadConfig() (*viper.Viper, error) {
"vehicles": map[string]any{
"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,

View File

@@ -132,6 +132,11 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte
if v, ok := b.Data["administrator_unavailability"].(bool); !ok || !v {
// Apply status filter
if status != "" {
if h.config.GetString("modules.vehicles.status_management") == "manual" {
if b.ManualStatus != status {
continue
}
} else {
bookingStatus := b.Status()
statusInt := 0
@@ -160,6 +165,7 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte
continue
}
}
}
// Apply date filter (on startdate)
if !startdate.IsZero() && b.Startdate.Before(startdate) {
@@ -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
}

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

@@ -18,6 +18,8 @@ func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *h
"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)
@@ -32,6 +34,8 @@ func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter,
"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)

View File

@@ -66,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)
@@ -78,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

@@ -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

@@ -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)