Some checks failed
Build and Push Docker Image / build_and_push (push) Failing after 2m37s
1083 lines
30 KiB
Go
Executable File
1083 lines
30 KiB
Go
Executable File
package application
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
|
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
|
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
|
|
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
|
|
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
|
|
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
|
|
"git.coopgo.io/coopgo-platform/groups-management/storage"
|
|
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
|
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog/log"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
type VehiclesManagementOverviewResult struct {
|
|
Vehicles []fleetsstorage.Vehicle
|
|
VehiclesMap map[string]fleetsstorage.Vehicle
|
|
DriversMap map[string]mobilityaccountsstorage.Account
|
|
Bookings []fleetsstorage.Booking
|
|
}
|
|
|
|
func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context, groupID, vehicleType, status, dateStart, dateEnd, vType, vStatus string) (*VehiclesManagementOverviewResult, error) {
|
|
request := &fleets.GetVehiclesRequest{
|
|
Namespaces: []string{"parcoursmob"},
|
|
}
|
|
resp, err := h.services.GRPC.Fleets.GetVehicles(ctx, request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get vehicles: %w", err)
|
|
}
|
|
|
|
vehicles := []fleetsstorage.Vehicle{}
|
|
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.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)
|
|
}
|
|
}
|
|
if b.Unavailableto.After(time.Now()) {
|
|
vehicleBookings = append(vehicleBookings, b)
|
|
}
|
|
}
|
|
v.Bookings = vehicleBookings
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
driversMap, _ := h.services.GetBeneficiariesMap(ctx)
|
|
|
|
sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
|
|
sort.Sort(sorting.BookingsByStartdate(bookings))
|
|
|
|
return &VehiclesManagementOverviewResult{
|
|
Vehicles: vehicles,
|
|
VehiclesMap: vehiclesMap,
|
|
DriversMap: driversMap,
|
|
Bookings: bookings,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ApplicationHandler) filterVehicleByGroup(v *fleets.Vehicle, groupID string) bool {
|
|
if groupID == "" {
|
|
return false
|
|
}
|
|
|
|
for _, n := range v.Administrators {
|
|
if n == groupID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type VehiclesManagementBookingsListResult struct {
|
|
VehiclesMap map[string]fleetsstorage.Vehicle
|
|
DriversMap map[string]mobilityaccountsstorage.Account
|
|
Bookings []fleetsstorage.Booking
|
|
CacheID string
|
|
}
|
|
|
|
func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Context, groupID, status, startDate, endDate, vehicleType string) (*VehiclesManagementBookingsListResult, error) {
|
|
request := &fleets.GetVehiclesRequest{
|
|
Namespaces: []string{"parcoursmob"},
|
|
IncludeDeleted: true,
|
|
}
|
|
resp, err := h.services.GRPC.Fleets.GetVehicles(ctx, request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get vehicles: %w", err)
|
|
}
|
|
|
|
bookings := []fleetsstorage.Booking{}
|
|
vehiclesMap := map[string]fleetsstorage.Vehicle{}
|
|
|
|
// Parse start date filter
|
|
var startdate time.Time
|
|
if startDate != "" {
|
|
if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
|
|
startdate = parsed
|
|
}
|
|
}
|
|
|
|
// Parse end date filter
|
|
var enddate time.Time
|
|
if endDate != "" {
|
|
if parsed, err := time.Parse("2006-01-02", endDate); err == nil {
|
|
enddate = parsed.Add(24 * time.Hour) // End of day
|
|
}
|
|
}
|
|
|
|
for _, vehicle := range resp.Vehicles {
|
|
if h.filterVehicleByGroup(vehicle, groupID) {
|
|
v := vehicle.ToStorageType()
|
|
vehiclesMap[v.ID] = v
|
|
for _, b := range v.Bookings {
|
|
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
|
|
|
|
if b.Deleted {
|
|
statusInt = -2 // Use -2 for cancelled to distinguish from terminated
|
|
} else {
|
|
statusInt = bookingStatus
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply date filter (intersection: booking overlaps with [startdate, enddate])
|
|
if !startdate.IsZero() && b.Enddate.Before(startdate) {
|
|
continue
|
|
}
|
|
if !enddate.IsZero() && b.Startdate.After(enddate) {
|
|
continue
|
|
}
|
|
|
|
bookings = append(bookings, b)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Sort(sorting.BookingsByStartdate(bookings))
|
|
|
|
cacheID := uuid.NewString()
|
|
h.cache.PutWithTTL(cacheID, bookings, 1*time.Hour)
|
|
|
|
driversMap, _ := h.services.GetBeneficiariesMap(ctx)
|
|
|
|
return &VehiclesManagementBookingsListResult{
|
|
VehiclesMap: vehiclesMap,
|
|
DriversMap: driversMap,
|
|
Bookings: bookings,
|
|
CacheID: cacheID,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ApplicationHandler) CreateVehicle(ctx context.Context, name, vehicleType, informations, licencePlate, kilometers string, automatic bool, address map[string]any, otherProperties map[string]any) (string, error) {
|
|
g := ctx.Value(identification.GroupKey)
|
|
if g == nil {
|
|
return "", fmt.Errorf("no group found in context")
|
|
}
|
|
group := g.(storage.Group)
|
|
|
|
dataMap := map[string]any{}
|
|
if name != "" {
|
|
dataMap["name"] = name
|
|
}
|
|
if address != nil {
|
|
dataMap["address"] = address
|
|
}
|
|
if informations != "" {
|
|
dataMap["informations"] = informations
|
|
}
|
|
if licencePlate != "" {
|
|
dataMap["licence_plate"] = licencePlate
|
|
}
|
|
dataMap["automatic"] = automatic
|
|
if kilometers != "" {
|
|
dataMap["kilometers"] = kilometers
|
|
}
|
|
// Add other properties
|
|
for key, value := range otherProperties {
|
|
dataMap[key] = value
|
|
}
|
|
|
|
data, err := structpb.NewValue(dataMap)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create data struct: %w", err)
|
|
}
|
|
|
|
vehicle := &fleets.Vehicle{
|
|
Id: uuid.NewString(),
|
|
Namespace: "parcoursmob",
|
|
Type: vehicleType,
|
|
Administrators: []string{group.ID},
|
|
Data: data.GetStructValue(),
|
|
}
|
|
|
|
request := &fleets.AddVehicleRequest{
|
|
Vehicle: vehicle,
|
|
}
|
|
|
|
_, err = h.services.GRPC.Fleets.AddVehicle(ctx, request)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to add vehicle: %w", err)
|
|
}
|
|
|
|
return vehicle.Id, nil
|
|
}
|
|
|
|
func (h *ApplicationHandler) GetVehicleTypes(ctx context.Context) ([]string, error) {
|
|
return h.config.GetStringSlice("modules.fleets.vehicle_types"), nil
|
|
}
|
|
|
|
type VehicleDisplayResult struct {
|
|
Vehicle fleetsstorage.Vehicle
|
|
Beneficiaries map[string]mobilityaccountsstorage.Account
|
|
}
|
|
|
|
func (h *ApplicationHandler) GetVehicleDisplay(ctx context.Context, vehicleID string) (*VehicleDisplayResult, error) {
|
|
request := &fleets.GetVehicleRequest{
|
|
Vehicleid: vehicleID,
|
|
}
|
|
|
|
resp, err := h.services.GRPC.Fleets.GetVehicle(ctx, request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get vehicle: %w", err)
|
|
}
|
|
|
|
beneficiaries, err := h.services.GetBeneficiariesMap(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get beneficiaries: %w", err)
|
|
}
|
|
|
|
vehicle := resp.Vehicle.ToStorageType()
|
|
|
|
// Sort bookings by start date (most recent first)
|
|
sort.Slice(vehicle.Bookings, func(i, j int) bool {
|
|
return vehicle.Bookings[i].Startdate.After(vehicle.Bookings[j].Startdate)
|
|
})
|
|
|
|
return &VehicleDisplayResult{
|
|
Vehicle: vehicle,
|
|
Beneficiaries: beneficiaries,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ApplicationHandler) UpdateBooking(ctx context.Context, bookingID string, startdate, enddate *time.Time, unavailablefrom, unavailableto string) error {
|
|
booking, err := h.services.GetBooking(bookingID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get booking: %w", err)
|
|
}
|
|
|
|
newbooking, _ := fleets.BookingFromStorageType(&booking)
|
|
|
|
if startdate != nil {
|
|
newbooking.Startdate = timestamppb.New(*startdate)
|
|
|
|
if startdate.Before(newbooking.Unavailablefrom.AsTime()) {
|
|
newbooking.Unavailablefrom = timestamppb.New(*startdate)
|
|
}
|
|
}
|
|
|
|
if enddate != nil {
|
|
newbooking.Enddate = timestamppb.New(*enddate)
|
|
|
|
if enddate.After(newbooking.Unavailableto.AsTime()) || enddate.Equal(newbooking.Unavailableto.AsTime()) {
|
|
newbooking.Unavailableto = timestamppb.New(enddate.Add(24 * time.Hour))
|
|
}
|
|
}
|
|
|
|
if unavailablefrom != "" {
|
|
newunavailablefrom, _ := time.Parse("2006-01-02", unavailablefrom)
|
|
newbooking.Unavailablefrom = timestamppb.New(newunavailablefrom)
|
|
}
|
|
|
|
if unavailableto != "" {
|
|
newunavailableto, _ := time.Parse("2006-01-02", unavailableto)
|
|
newbooking.Unavailableto = timestamppb.New(newunavailableto)
|
|
}
|
|
|
|
request := &fleets.UpdateBookingRequest{
|
|
Booking: newbooking,
|
|
}
|
|
|
|
_, err = h.services.GRPC.Fleets.UpdateBooking(ctx, request)
|
|
return err
|
|
}
|
|
|
|
type BookingDisplayResult struct {
|
|
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) {
|
|
booking, err := h.services.GetBooking(bookingID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get booking: %w", err)
|
|
}
|
|
|
|
beneficiary := mobilityaccountsstorage.Account{}
|
|
if booking.Driver != "" {
|
|
beneficiaryrequest := &mobilityaccounts.GetAccountRequest{
|
|
Id: booking.Driver,
|
|
}
|
|
|
|
beneficiaryresp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, beneficiaryrequest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get beneficiary: %w", err)
|
|
}
|
|
beneficiary = beneficiaryresp.Account.ToStorageType()
|
|
}
|
|
|
|
grouprequest := &groupsmanagement.GetGroupRequest{
|
|
Id: booking.Vehicle.Administrators[0],
|
|
}
|
|
|
|
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get group: %w", err)
|
|
}
|
|
|
|
alternativerequest := &fleets.GetVehiclesRequest{
|
|
Namespaces: []string{"parcoursmob"},
|
|
Types: []string{booking.Vehicle.Type},
|
|
Administrators: booking.Vehicle.Administrators,
|
|
AvailabilityFrom: timestamppb.New(booking.Startdate),
|
|
AvailabilityTo: timestamppb.New(booking.Enddate.Add(24 * time.Hour)),
|
|
}
|
|
|
|
alternativeresp, err := h.services.GRPC.Fleets.GetVehicles(ctx, alternativerequest)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("failed to get alternative vehicles")
|
|
}
|
|
|
|
alternatives := []any{}
|
|
for _, a := range alternativeresp.Vehicles {
|
|
alternatives = append(alternatives, a.ToStorageType())
|
|
}
|
|
|
|
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,
|
|
ComputedExtraProperties: computedProps,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ApplicationHandler) ChangeBookingVehicle(ctx context.Context, bookingID, newVehicleID string) error {
|
|
booking, err := h.services.GetBooking(bookingID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get booking: %w", err)
|
|
}
|
|
|
|
booking.Vehicleid = newVehicleID
|
|
b, _ := fleets.BookingFromStorageType(&booking)
|
|
|
|
request := &fleets.UpdateBookingRequest{
|
|
Booking: b,
|
|
}
|
|
|
|
_, err = h.services.GRPC.Fleets.UpdateBooking(ctx, request)
|
|
return err
|
|
}
|
|
|
|
func (h *ApplicationHandler) MakeVehicleUnavailable(ctx context.Context, vehicleID, unavailablefrom, unavailableto, comment, currentUserID string, currentUserClaims map[string]any) error {
|
|
g := ctx.Value(identification.GroupKey)
|
|
if g == nil {
|
|
return fmt.Errorf("no group found in context")
|
|
}
|
|
currentGroup := g.(storage.Group)
|
|
|
|
unavailablefromTime, _ := time.Parse("2006-01-02", unavailablefrom)
|
|
unavailabletoTime, _ := time.Parse("2006-01-02", unavailableto)
|
|
|
|
data := map[string]any{
|
|
"comment": comment,
|
|
"administrator_unavailability": true,
|
|
"booked_by": map[string]any{
|
|
"user": map[string]any{
|
|
"id": currentUserID,
|
|
"display_name": currentUserClaims["display_name"],
|
|
},
|
|
"group": map[string]any{
|
|
"id": currentGroup.ID,
|
|
"name": currentGroup.Data["name"],
|
|
},
|
|
},
|
|
}
|
|
|
|
datapb, err := structpb.NewStruct(data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create data struct: %w", err)
|
|
}
|
|
|
|
booking := &fleets.Booking{
|
|
Id: uuid.NewString(),
|
|
Vehicleid: vehicleID,
|
|
Unavailablefrom: timestamppb.New(unavailablefromTime),
|
|
Unavailableto: timestamppb.New(unavailabletoTime),
|
|
Data: datapb,
|
|
}
|
|
|
|
request := &fleets.CreateBookingRequest{
|
|
Booking: booking,
|
|
}
|
|
|
|
_, err = h.services.GRPC.Fleets.CreateBooking(ctx, request)
|
|
return err
|
|
}
|
|
|
|
func (h *ApplicationHandler) DeleteBooking(ctx context.Context, bookingID string) error {
|
|
request := &fleets.DeleteBookingRequest{
|
|
Id: bookingID,
|
|
}
|
|
|
|
_, err := h.services.GRPC.Fleets.DeleteBooking(ctx, request)
|
|
return err
|
|
}
|
|
|
|
func (h *ApplicationHandler) GetBookingForUnbooking(ctx context.Context, bookingID string) (fleetsstorage.Booking, error) {
|
|
request := &fleets.GetBookingRequest{
|
|
Bookingid: bookingID,
|
|
}
|
|
|
|
resp, err := h.services.GRPC.Fleets.GetBooking(ctx, request)
|
|
if err != nil {
|
|
return fleetsstorage.Booking{}, fmt.Errorf("failed to get booking: %w", err)
|
|
}
|
|
|
|
return resp.Booking.ToStorageType(), nil
|
|
}
|
|
|
|
func (h *ApplicationHandler) UnbookVehicle(ctx context.Context, bookingID, motif, currentUserID string, currentUserClaims map[string]any, currentGroup any) error {
|
|
group := currentGroup.(storage.Group)
|
|
|
|
// Prepare deletion metadata (microservice will add deleted_at automatically)
|
|
deletionMetadata := map[string]any{
|
|
"deleted_by": map[string]any{
|
|
"user": map[string]any{
|
|
"id": currentUserID,
|
|
"display_name": currentUserClaims["first_name"].(string) + " " + currentUserClaims["last_name"].(string),
|
|
"email": currentUserClaims["email"],
|
|
},
|
|
"group": map[string]any{
|
|
"id": group.ID,
|
|
"name": group.Data["name"],
|
|
},
|
|
},
|
|
"reason": motif,
|
|
}
|
|
|
|
deletionMetadataPb, err := structpb.NewStruct(deletionMetadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create deletion metadata: %w", err)
|
|
}
|
|
|
|
// Use the microservice's delete endpoint with metadata
|
|
deleteRequest := &fleets.DeleteBookingRequest{
|
|
Id: bookingID,
|
|
DeletionMetadata: deletionMetadataPb,
|
|
}
|
|
|
|
_, err = h.services.GRPC.Fleets.DeleteBooking(ctx, deleteRequest)
|
|
return err
|
|
}
|
|
|
|
type VehicleForUpdateResult struct {
|
|
Vehicle fleetsstorage.Vehicle
|
|
VehicleTypes []string
|
|
}
|
|
|
|
func (h *ApplicationHandler) GetVehicleForUpdate(ctx context.Context, vehicleID string) (*VehicleForUpdateResult, error) {
|
|
request := &fleets.GetVehicleRequest{
|
|
Vehicleid: vehicleID,
|
|
}
|
|
|
|
resp, err := h.services.GRPC.Fleets.GetVehicle(ctx, request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get vehicle: %w", err)
|
|
}
|
|
|
|
vehicleTypes := h.config.GetStringSlice("modules.fleets.vehicle_types")
|
|
|
|
return &VehicleForUpdateResult{
|
|
Vehicle: resp.Vehicle.ToStorageType(),
|
|
VehicleTypes: vehicleTypes,
|
|
}, nil
|
|
}
|
|
|
|
func (h *ApplicationHandler) UpdateVehicle(ctx context.Context, vehicleID, name, vehicleType, informations, licencePlate, kilometers string, automatic bool, address map[string]any, otherProperties map[string]any) (string, error) {
|
|
getRequest := &fleets.GetVehicleRequest{
|
|
Vehicleid: vehicleID,
|
|
}
|
|
|
|
resp, err := h.services.GRPC.Fleets.GetVehicle(ctx, getRequest)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get vehicle: %w", err)
|
|
}
|
|
|
|
// Start with existing data to preserve all fields
|
|
dataMap := resp.Vehicle.Data.AsMap()
|
|
if dataMap == nil {
|
|
dataMap = map[string]any{}
|
|
}
|
|
|
|
// Update with new values
|
|
if name != "" {
|
|
dataMap["name"] = name
|
|
}
|
|
if address != nil {
|
|
dataMap["address"] = address
|
|
}
|
|
if informations != "" {
|
|
dataMap["informations"] = informations
|
|
}
|
|
if licencePlate != "" {
|
|
dataMap["licence_plate"] = licencePlate
|
|
}
|
|
if kilometers != "" {
|
|
dataMap["kilometers"] = kilometers
|
|
}
|
|
dataMap["automatic"] = automatic
|
|
// Add other properties
|
|
for key, value := range otherProperties {
|
|
dataMap[key] = value
|
|
}
|
|
|
|
data, err := structpb.NewValue(dataMap)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create data struct: %w", err)
|
|
}
|
|
|
|
updateRequest := &fleets.UpdateVehicleRequest{
|
|
Vehicle: &fleets.Vehicle{
|
|
Id: vehicleID,
|
|
Namespace: resp.Vehicle.Namespace,
|
|
Type: vehicleType,
|
|
Administrators: resp.Vehicle.Administrators,
|
|
Data: data.GetStructValue(),
|
|
},
|
|
}
|
|
|
|
updateResp, err := h.services.GRPC.Fleets.UpdateVehicle(ctx, updateRequest)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to update vehicle: %w", err)
|
|
}
|
|
|
|
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, " ")
|
|
}
|
|
|