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
Some checks failed
Build and Push Docker Image / build_and_push (push) Failing after 2m37s
This commit is contained in:
@@ -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, " ")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user