package application import ( "context" "fmt" "sort" "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 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{} 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 { bookings = append(bookings, b) } } if b.Unavailableto.After(time.Now()) { vehicleBookings = append(vehicleBookings, b) } } v.Bookings = vehicleBookings vehicles = append(vehicles, v) vehiclesMap[v.ID] = v } } driversMap, _ := h.services.GetBeneficiariesMap() 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 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 v, ok := b.Data["administrator_unavailability"].(bool); !ok || !v { // Apply status filter if status != "" { bookingStatus := b.Status() statusInt := 0 if b.Deleted { statusInt = -2 // Use -2 for cancelled to distinguish from terminated } 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 (on startdate) if !startdate.IsZero() && b.Startdate.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() 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() 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, startdate, enddate, 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 != "" { newstartdate, _ := time.Parse("2006-01-02", startdate) newbooking.Startdate = timestamppb.New(newstartdate) if newstartdate.Before(newbooking.Unavailablefrom.AsTime()) { newbooking.Unavailablefrom = timestamppb.New(newstartdate) } } if enddate != "" { newenddate, _ := time.Parse("2006-01-02", enddate) newbooking.Enddate = timestamppb.New(newenddate) if newenddate.After(newbooking.Unavailableto.AsTime()) || newenddate.Equal(newbooking.Unavailableto.AsTime()) { newbooking.Unavailableto = timestamppb.New(newenddate.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 } 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") return &BookingDisplayResult{ Booking: booking, Vehicle: booking.Vehicle, Beneficiary: beneficiary, Group: groupresp.Group.ToStorageType(), Documents: documents, FileTypesMap: fileTypesMap, Alternatives: alternatives, }, 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 }