package application import ( "context" "crypto/rand" "encoding/base64" "fmt" "io" "net/http" "sort" "sync" "time" "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification" "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting" agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi" agendastorage "git.coopgo.io/coopgo-platform/agenda/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" groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage" accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi" 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" ) type AdministrationDataResult struct { Accounts []mobilityaccountsstorage.Account Beneficiaries []mobilityaccountsstorage.Account Groups []groupstorage.Group Bookings []fleetsstorage.Booking Events []agendastorage.Event } type AdminVehiclesStatsResult struct { Vehicles []fleetsstorage.Vehicle Bookings []fleetsstorage.Booking Groups map[string]any } type AdminBookingsStatsResult struct { Vehicles map[string]fleetsstorage.Vehicle Bookings []fleetsstorage.Booking Groups map[string]any BeneficiariesMap map[string]any } type AdminBeneficiariesStatsResult struct { Beneficiaries []mobilityaccountsstorage.Account CacheID string } type AdminEventsStatsResult struct { Events []agendastorage.Event Groups map[string]any } // GetAdministrationData retrieves all data needed for the administration dashboard func (h *ApplicationHandler) GetAdministrationData(ctx context.Context) (*AdministrationDataResult, error) { var ( wg sync.WaitGroup accounts, beneficiaries []mobilityaccountsstorage.Account bookings []fleetsstorage.Booking accountsErr, beneficiariesErr, bookingsErr, groupsResponseErr, eventsResponseErr, groupsBatchErr error groups = []groupstorage.Group{} responses = []agendastorage.Event{} groupsResponse *groupsmanagement.GetGroupsResponse eventsResponse *agenda.GetEventsResponse groupids = []string{} ) // Retrieve accounts in a goroutine wg.Add(1) go func() { defer wg.Done() accounts, accountsErr = h.services.GetAccounts() }() // Retrieve beneficiaries in a goroutine wg.Add(1) go func() { defer wg.Done() beneficiaries, beneficiariesErr = h.services.GetBeneficiaries() }() // Retrieve bookings in a goroutine wg.Add(1) go func() { defer wg.Done() bookings, bookingsErr = h.services.GetBookings() }() // Retrieve groups in a goroutine wg.Add(1) go func() { defer wg.Done() request := &groupsmanagement.GetGroupsRequest{ Namespaces: []string{"parcoursmob_organizations"}, } groupsResponse, groupsResponseErr = h.services.GRPC.GroupsManagement.GetGroups(ctx, request) if groupsResponseErr == nil { for _, group := range groupsResponse.Groups { g := group.ToStorageType() groups = append(groups, g) } sort.Sort(sorting.GroupsByName(groups)) } }() // Retrieve Events in a goroutine wg.Add(1) go func() { defer wg.Done() eventsResponse, eventsResponseErr = h.services.GRPC.Agenda.GetEvents(ctx, &agenda.GetEventsRequest{ Namespaces: []string{"parcoursmob_dispositifs"}, }) if eventsResponseErr == nil { for _, e := range eventsResponse.Events { groupids = append(groupids, e.Owners...) responses = append(responses, e.ToStorageType()) } sort.Sort(sorting.EventsByStartdate(responses)) } }() wg.Wait() // Check for errors if accountsErr != nil || beneficiariesErr != nil || bookingsErr != nil || groupsResponseErr != nil || eventsResponseErr != nil { log.Error(). Any("accounts error", accountsErr). Any("beneficiaries error", beneficiariesErr). Any("bookings error", bookingsErr). Any("groups response error", groupsResponseErr). Any("events response error", eventsResponseErr). Any("groups batch error", groupsBatchErr). Msg("Error in retrieving administration data") return nil, fmt.Errorf("error retrieving administration data") } return &AdministrationDataResult{ Accounts: accounts, Beneficiaries: beneficiaries, Groups: groups, Bookings: bookings, Events: responses, }, nil } // CreateAdministrationGroup creates a new administration group func (h *ApplicationHandler) CreateAdministrationGroup(ctx context.Context, name string, modules map[string]any) (string, error) { groupid := uuid.NewString() dataMap := map[string]any{ "name": name, "modules": modules, } data, err := structpb.NewValue(dataMap) if err != nil { log.Error().Err(err).Msg("Cannot create PB struct from data map") return "", fmt.Errorf("failed to create group data: %w", err) } request_organization := &groupsmanagement.AddGroupRequest{ Group: &groupsmanagement.Group{ Id: groupid, Namespace: "parcoursmob_organizations", Data: data.GetStructValue(), }, } request_role := &groupsmanagement.AddGroupRequest{ Group: &groupsmanagement.Group{ Id: groupid + ":admin", Namespace: "parcoursmob_roles", }, } // Create organization group _, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request_organization) if err != nil { log.Error().Err(err).Msg("Issue in Groups management service - AddGroup") return "", fmt.Errorf("failed to create organization group: %w", err) } // Create admin role for the organization _, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request_role) if err != nil { log.Error().Err(err).Msg("Issue in Groups management service - AddGroup") return "", fmt.Errorf("failed to create admin role: %w", err) } return groupid, nil } type AdministrationGroupDataResult struct { Group groupstorage.Group Members []mobilityaccountsstorage.Account Admins []mobilityaccountsstorage.Account } // GetAdministrationGroupData retrieves data for a specific administration group func (h *ApplicationHandler) GetAdministrationGroupData(ctx context.Context, groupID string) (*AdministrationGroupDataResult, error) { request := &groupsmanagement.GetGroupRequest{ Id: groupID, } resp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, request) if err != nil { log.Error().Err(err).Msg("Issue in Groups management service - GetGroup") return nil, fmt.Errorf("failed to get group: %w", err) } groupmembers, admins, err := h.groupmembers(groupID) if err != nil { log.Error().Err(err).Msg("issue retrieving group members") return nil, fmt.Errorf("failed to get group members: %w", err) } return &AdministrationGroupDataResult{ Group: resp.Group.ToStorageType(), Members: groupmembers, Admins: admins, }, nil } // InviteAdministrationGroupAdmin invites a user as admin to an administration group func (h *ApplicationHandler) InviteAdministrationGroupAdmin(ctx context.Context, groupID, username string) error { // Get group info groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{ Id: groupID, }) if err != nil { return fmt.Errorf("failed to get group: %w", err) } group := groupResp.Group.ToStorageType() accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(ctx, &mobilityaccounts.GetAccountUsernameRequest{ Username: username, Namespace: "parcoursmob", }) if err == nil { // Account already exists: adding the existing account to group as admin account := accountresp.Account.ToStorageType() if account.Data["groups"] == nil { account.Data["groups"] = []any{} } account.Data["groups"] = append(account.Data["groups"].([]any), groupID+":admin") as, err := mobilityaccounts.AccountFromStorageType(&account) if err != nil { return fmt.Errorf("failed to convert account: %w", err) } _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &mobilityaccounts.UpdateDataRequest{ Account: as, }) if err != nil { return fmt.Errorf("failed to update account: %w", err) } data := map[string]any{ "group": group.Data["name"], "baseUrl": h.config.GetString("base_url"), } if err := h.emailing.Send("onboarding.existing_administrator", username, data); err != nil { log.Warn().Err(err).Msg("failed to send existing admin email") } } else { // Create onboarding for new admin onboarding := map[string]any{ "username": username, "group": groupID, "admin": true, } b := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, b); err != nil { return fmt.Errorf("failed to generate random key: %w", err) } key := base64.RawURLEncoding.EncodeToString(b) h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour) data := map[string]any{ "group": group.Data["name"], "key": key, "baseUrl": h.config.GetString("base_url"), } if err := h.emailing.Send("onboarding.new_administrator", username, data); err != nil { return fmt.Errorf("failed to send new admin email: %w", err) } } return nil } // InviteAdministrationGroupMember invites a user as member to an administration group func (h *ApplicationHandler) InviteAdministrationGroupMember(ctx context.Context, groupID, username string) error { // Get group info groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{ Id: groupID, }) if err != nil { return fmt.Errorf("failed to get group: %w", err) } group := groupResp.Group.ToStorageType() accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(ctx, &mobilityaccounts.GetAccountUsernameRequest{ Username: username, Namespace: "parcoursmob", }) if err == nil { // Account already exists: adding the existing account to group account := accountresp.Account.ToStorageType() if account.Data["groups"] == nil { account.Data["groups"] = []any{} } account.Data["groups"] = append(account.Data["groups"].([]any), groupID) as, err := mobilityaccounts.AccountFromStorageType(&account) if err != nil { return fmt.Errorf("failed to convert account: %w", err) } _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &mobilityaccounts.UpdateDataRequest{ Account: as, }) if err != nil { return fmt.Errorf("failed to update account: %w", err) } data := map[string]any{ "group": group.Data["name"], "baseUrl": h.config.GetString("base_url"), } if err := h.emailing.Send("onboarding.existing_member", username, data); err != nil { log.Warn().Err(err).Msg("failed to send existing member email") } } else { // Create onboarding for new member onboarding := map[string]any{ "username": username, "group": groupID, "admin": false, } b := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, b); err != nil { return fmt.Errorf("failed to generate random key: %w", err) } key := base64.RawURLEncoding.EncodeToString(b) h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour) data := map[string]any{ "group": group.Data["name"], "key": key, "baseUrl": h.config.GetString("base_url"), } if err := h.emailing.Send("onboarding.new_member", username, data); err != nil { return fmt.Errorf("failed to send new member email: %w", err) } } return nil } func filteVehicle(r *http.Request, v *fleets.Vehicle) bool { g := r.Context().Value(identification.GroupKey) if g == nil { return false } group := g.(storage.Group) for _, n := range v.Administrators { if n == group.ID { return true } } return false } func (h *ApplicationHandler) GetVehiclesStats() (*AdminVehiclesStatsResult, error) { bookings := []fleetsstorage.Booking{} administrators := []string{} request := &fleets.GetVehiclesRequest{ Namespaces: []string{"parcoursmob"}, } resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request) if err != nil { return nil, err } vehicles := []fleetsstorage.Vehicle{} for _, vehicle := range resp.Vehicles { v := vehicle.ToStorageType() adminfound := false for _, a := range administrators { if len(v.Administrators) > 0 && a == v.Administrators[0] { adminfound = true break } } if !adminfound && len(v.Administrators) > 0 { administrators = append(administrators, v.Administrators[0]) } vehicleBookings := []fleetsstorage.Booking{} for _, b := range v.Bookings { if b.Unavailableto.After(time.Now()) { vehicleBookings = append(vehicleBookings, b) } } v.Bookings = vehicleBookings vehicles = append(vehicles, v) } groups := map[string]any{} if len(administrators) > 0 { admingroups, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{ Groupids: administrators, }) if err != nil { return nil, err } for _, g := range admingroups.Groups { groups[g.Id] = g.ToStorageType() } } sort.Sort(sorting.VehiclesByLicencePlate(vehicles)) sort.Sort(sorting.BookingsByStartdate(bookings)) return &AdminVehiclesStatsResult{ Vehicles: vehicles, Bookings: bookings, Groups: groups, }, nil } func (h *ApplicationHandler) GetBookingsStats(status, startDate, endDate string) (*AdminBookingsStatsResult, error) { vehicles := map[string]fleetsstorage.Vehicle{} bookings := []fleetsstorage.Booking{} // 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 } } request := &fleets.GetVehiclesRequest{ Namespaces: []string{"parcoursmob"}, IncludeDeleted: true, } resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request) if err != nil { return nil, err } beneficiaries_ids := []string{} for _, vehicle := range resp.Vehicles { v := vehicle.ToStorageType() for _, b := range v.Bookings { // 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) beneficiaries_ids = append(beneficiaries_ids, b.Driver) } vehicles[v.ID] = v } groups := map[string]any{} admingroups, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), &groupsmanagement.GetGroupsRequest{ Namespaces: []string{"parcoursmob_organizations"}, }) if err != nil { return nil, err } for _, g := range admingroups.Groups { groups[g.Id] = g.ToStorageType() } beneficiaries, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), &accounts.GetAccountsBatchRequest{ Accountids: beneficiaries_ids, }) if err != nil { return nil, err } beneficiaries_map := map[string]any{} for _, ben := range beneficiaries.Accounts { beneficiaries_map[ben.Id] = ben.ToStorageType() } return &AdminBookingsStatsResult{ Vehicles: vehicles, Bookings: bookings, Groups: groups, BeneficiariesMap: beneficiaries_map, }, nil } func (h *ApplicationHandler) GetBeneficiariesStats() (*AdminBeneficiariesStatsResult, error) { beneficiaries, err := h.services.GetBeneficiaries() if err != nil { return nil, err } cacheid := uuid.New().String() h.cache.Put(cacheid, beneficiaries) return &AdminBeneficiariesStatsResult{ Beneficiaries: beneficiaries, CacheID: cacheid, }, nil } func (h *ApplicationHandler) GetEventsStats() (*AdminEventsStatsResult, error) { resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{ Namespaces: []string{"parcoursmob_dispositifs"}, }) if err != nil { return nil, err } responses := []agendastorage.Event{} groupids := []string{} for _, event := range resp.Events { responses = append(responses, event.ToStorageType()) groupids = append(groupids, event.Owners...) } groupsResponse, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{ Groupids: groupids, }) if err != nil { return nil, err } groups := map[string]any{} for _, g := range groupsResponse.Groups { groups[g.Id] = g.ToStorageType() } return &AdminEventsStatsResult{ Events: responses, Groups: groups, }, nil } func (h *ApplicationHandler) members() ([]*accounts.Account, error) { resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), &accounts.GetAccountsRequest{ Namespaces: []string{"parcoursmob"}, }) if err != nil { return nil, err } return resp.Accounts, nil } func (h *ApplicationHandler) groupmembers(groupid string) (groupmembers []mobilityaccountsstorage.Account, admins []mobilityaccountsstorage.Account, err error) { members, err := h.members() if err != nil { if err != nil { log.Error().Err(err).Msg("Cannot get members") return nil, nil, err } } groupmembers = []mobilityaccountsstorage.Account{} admins = []mobilityaccountsstorage.Account{} for _, m := range members { mm := m.ToStorageType() for _, g := range mm.Data["groups"].([]any) { if g.(string) == groupid { groupmembers = append(groupmembers, mm) } if g.(string) == groupid+":admin" { admins = append(admins, mm) } } } return groupmembers, admins, err }