diff --git a/config.go b/config.go index 407b75c..2f47fcc 100755 --- a/config.go +++ b/config.go @@ -233,10 +233,20 @@ func ReadConfig() (*viper.Viper, error) { "vehicles": map[string]any{ "enabled": true, "default_booking_duration_days": 90, - "status_management": "automatic", + "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"}, diff --git a/core/application/vehicles-management.go b/core/application/vehicles-management.go index 878c702..7e2850f 100755 --- a/core/application/vehicles-management.go +++ b/core/application/vehicles-management.go @@ -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 { + 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,11 +240,26 @@ 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 b.ManualStatus != status { + 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 { @@ -167,8 +293,8 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte } } - // 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) { @@ -330,13 +456,14 @@ func (h *ApplicationHandler) UpdateBooking(ctx context.Context, bookingID string } type BookingDisplayResult struct { - Booking fleetsstorage.Booking - Vehicle fleetsstorage.Vehicle - Beneficiary mobilityaccountsstorage.Account - Group storage.Group - Documents []filestorage.FileInfo - FileTypesMap map[string]string - Alternatives []any + Booking fleetsstorage.Booking + Vehicle fleetsstorage.Vehicle + Beneficiary mobilityaccountsstorage.Account + Group storage.Group + Documents []filestorage.FileInfo + FileTypesMap map[string]string + Alternatives []any + ComputedExtraProperties map[string]string } func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID string) (*BookingDisplayResult, error) { @@ -388,14 +515,17 @@ 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, - Beneficiary: beneficiary, - Group: groupresp.Group.ToStorageType(), - Documents: documents, - FileTypesMap: fileTypesMap, - Alternatives: alternatives, + Booking: booking, + Vehicle: booking.Vehicle, + Beneficiary: beneficiary, + Group: groupresp.Group.ToStorageType(), + Documents: documents, + FileTypesMap: fileTypesMap, + Alternatives: alternatives, + ComputedExtraProperties: computedProps, }, nil } @@ -620,7 +750,25 @@ func getStatusOptions(raw interface{}) []map[string]any { return nil } -func (h *ApplicationHandler) UpdateBookingStatus(ctx context.Context, bookingID, newStatus, comment, userID string, userClaims map[string]any, currentGroup any) error { +// 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 @@ -655,6 +803,70 @@ func (h *ApplicationHandler) UpdateBookingStatus(ctx context.Context, bookingID, 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) @@ -666,3 +878,205 @@ func (h *ApplicationHandler) UpdateBookingStatus(ctx context.Context, bookingID, 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, " ") +} + diff --git a/renderer/func-maps.go b/renderer/func-maps.go index 9a4902f..3006b66 100755 --- a/renderer/func-maps.go +++ b/renderer/func-maps.go @@ -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) diff --git a/renderer/vehicle-management.go b/renderer/vehicle-management.go index e477865..ef1471a 100755 --- a/renderer/vehicle-management.go +++ b/renderer/vehicle-management.go @@ -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,6 +18,10 @@ 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"), } @@ -25,7 +29,7 @@ func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *h 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{ @@ -34,6 +38,7 @@ 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"), } @@ -82,19 +87,21 @@ 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{ - "booking": booking, - "vehicle": vehicle, - "beneficiary": beneficiary, - "group": group, - "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": booking, + "vehicle": vehicle, + "beneficiary": beneficiary, + "group": group, + "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) diff --git a/renderer/xlsx/vehicle-bookings.go b/renderer/xlsx/vehicle-bookings.go index 8263d84..7090c36 100644 --- a/renderer/xlsx/vehicle-bookings.go +++ b/renderer/xlsx/vehicle-bookings.go @@ -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: diff --git a/servers/web/application/vehicles_management.go b/servers/web/application/vehicles_management.go index 1de4776..24149cc 100644 --- a/servers/web/application/vehicles_management.go +++ b/servers/web/application/vehicles_management.go @@ -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) @@ -64,12 +89,15 @@ func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc { // Prepare filters map for template filters := map[string]string{ - "status": status, - "date_start": dateStart, - "date_end": dateEnd, + "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) } } @@ -364,11 +392,19 @@ func (h *Handler) VehicleManagementUpdateBookingStatusHTTPHandler() http.Handler 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) + 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) diff --git a/servers/web/exports/fleets.go b/servers/web/exports/fleets.go index 9355740..d168ded 100644 --- a/servers/web/exports/fleets.go +++ b/servers/web/exports/fleets.go @@ -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)