diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
new file mode 100644
index 0000000..deb266f
--- /dev/null
+++ b/.gitea/workflows/build.yml
@@ -0,0 +1,81 @@
+name: Build and Push Docker Image
+
+on:
+ push:
+ tags:
+ - '*'
+ branches:
+ - main
+ - dev
+
+jobs:
+ build_and_push:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Install Docker
+ run: |
+ apt-get update
+ apt-get install -y docker.io
+
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Set Kubernetes Context
+ uses: azure/k8s-set-context@v4
+ with:
+ method: kubeconfig
+ kubeconfig: ${{secrets.buildx_kubeconfig}}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ with:
+ driver: kubernetes
+ driver-opts: |
+ namespace=gitea
+
+ - name: Login to Docker Registry
+ uses: docker/login-action@v3
+ with:
+ registry: git.coopgo.io
+ username: ${{ secrets.REGISTRY_USER }}
+ password: ${{ secrets.REGISTRY_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker image
+ id: metadata
+ uses: docker/metadata-action@v3
+ with:
+ images: git.coopgo.io/${{gitea.repository}}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=tag
+ type=ref,event=pr
+ flavor: |
+ latest=auto
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: |
+ ${{ steps.metadata.outputs.tags }}
+ build-args: |
+ ACCESS_TOKEN_USR=${{gitea.actor}}
+ ACCESS_TOKEN_PWD=${{gitea.token}}
+
+ # BUILD WITH KANIKO
+ # - name: Kaniko build and push
+ # uses: aevea/action-kaniko@master
+ # with:
+ # build_file: Dockerfile
+ # registry: git.coopgo.io
+ # username: ${{secrets.registry_user}}
+ # password: ${{secrets.registry_token}}
+ # image: ${{gitea.repository}}
+ # tag: ${{gitea.ref_name}}
+ # cache: true
+ # cache_registry: git.coopgo.io/${{gitea.repository}}/cache
+ # extra-args: |
+ # ACCESS_TOKEN_USR=${{gitea.actor}}
+ # ACCESS_TOKEN_PWD=${{gitea.token}}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index fa74cfc..de54a0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
/config.yaml
themes/*
.vscode
-__debug_bin
\ No newline at end of file
+__debug_bin
+parcoursmob
+.idea
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..57e33ff
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/parcoursmob.iml b/.idea/parcoursmob.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/parcoursmob.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..b0e7199
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
old mode 100644
new mode 100755
index 635c5f9..71b5486
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,19 +9,22 @@ WORKDIR /
# Create a netrc file using the credentials specified using --build-arg
RUN printf "machine git.coopgo.io\n\
- login ${ACCESS_TOKEN_USR}\n\
- password ${ACCESS_TOKEN_PWD}\n\
- \n"\
- >> ~/.netrc
+ login ${ACCESS_TOKEN_USR}\n\
+ password ${ACCESS_TOKEN_PWD}\n\
+ \n"\
+ >> ~/.netrc
RUN chmod 600 ~/.netrc
COPY . .
RUN go mod download && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /server
-#RUN rm -r themes/*
+RUN rm -r themes
+RUN mkdir themes
+
RUN git clone --depth 1 https://git.coopgo.io/coopgo-apps/parcoursmob-default-theme themes/default
RUN git clone -b spie06 --depth 1 https://git.coopgo.io/coopgo-apps/parcoursmob-default-theme themes/spie06
+RUN git clone -b solidarity-transport-dev --depth 1 https://git.coopgo.io/coopgo-apps/parcoursmob-default-theme themes/solidarity-transport
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index 6980828..ddb2dc0
--- a/README.md
+++ b/README.md
@@ -10,5 +10,3 @@ This new version of PARCOURSMOB brings :
- A configurable and themeable approach of rendering web pages : the default theme is located in the folder [themes/default/](themes/default/)
- A modular architecture based on groups and access rights, using [COOPGO Groups Management](https://git.coopgo.io/coopgo-groups-management)
- A distributed cache system through [etcd](https://etcd.io/) to handle distributed state management like pagination in a cloud native way
-
-
diff --git a/config.go b/config.go
old mode 100644
new mode 100755
index ac4eddb..baeeaf7
--- a/config.go
+++ b/config.go
@@ -14,7 +14,20 @@ func ReadConfig() (*viper.Viper, error) {
"public_dir": "template/default/public",
},
"server": map[string]any{
- "listen": "0.0.0.0:9000",
+ "listen": "0.0.0.0:9000", // DEPRECATED
+ "web": map[string]any{
+ "enabled": true,
+ "listen": "0.0.0.0:8080",
+ },
+ "mcp": map[string]any{
+ "enabled": true,
+ "listen": "0.0.0.0:8081",
+ },
+ "publicweb": map[string]any{
+ "enabled": false,
+ "listen": "0.0.0.0:8082",
+ "root_dir": "public_themes/default",
+ },
},
"identification": map[string]any{
"sessions": map[string]any{
@@ -22,9 +35,258 @@ func ReadConfig() (*viper.Viper, error) {
"session_key": "SESSION_KEY",
},
},
+ "storage": map[string]any{
+ "files": map[string]any{
+ "file_types": map[string]string{
+ "driving_licence": "Permis de conduire",
+ "work_contract": "Contrat de travail",
+ "identity_proof": "Pièce d'identité",
+ "membership_form": "Bulletin d'adhésion",
+ "other": "Autre",
+ },
+ },
+ },
+ "modules": map[string]any{
+ "dashboard": map[string]any{
+ "enabled": true,
+ },
+ "members": map[string]any{
+ "enabled": true,
+ "profile_optional_fields": []map[string]any{},
+ },
+ "beneficiaries": map[string]any{
+ "enabled": true,
+ "validated_profile": map[string]any{
+ "enabled": false,
+ "required": map[string]any{
+ "fields": []string{},
+ "documents": []string{},
+ },
+ },
+ "profile_optional_fields": []map[string]any{
+ {
+ "name": "gender",
+ "label": "Genre",
+ "type": "select",
+ "options": []map[string]string{
+ {"value": "0", "label": "Inconnu"},
+ {"value": "1", "label": "Masculin"},
+ {"value": "2", "label": "Féminin"},
+ {"value": "9", "label": "Sans objet"},
+ },
+ },
+ {
+ "name": "social_situation",
+ "label": "Situation sociale",
+ "type": "select",
+ "options": []map[string]string{
+ {"value": "", "label": "Inconnu"},
+ {"value": "BRSA", "label": "BRSA"},
+ {"value": "Demandeur d'emploi", "label": "Demandeur d'emploi"},
+ {"value": "Chantier d'insertion", "label": "Chantier d'insertion"},
+ {"value": "Jeune -25", "label": "Jeune -25"},
+ },
+ },
+ {
+ "name": "registration_reason",
+ "label": "Motif d'inscription",
+ "type": "select",
+ "options": []map[string]string{
+ {"value": "", "label": "Inconnu"},
+ {"value": "Pas de permis", "label": "Pas de permis"},
+ {"value": "Pas de véhicule", "label": "Pas de véhicule"},
+ {"value": "Perte d'autonomie", "label": "Perte d'autonomie"},
+ {"value": "Autre", "label": "Autre"},
+ },
+ },
+ {
+ "name": "status",
+ "label": "Statut (prioritaire / non prioritaire)",
+ "type": "select",
+ "options": []map[string]string{
+ {"value": "Non prioritaire", "label": "Non prioritaire"},
+ {"value": "Prioritaire", "label": "Prioritaire"},
+ },
+ },
+ {
+ "name": "last_subscription_date",
+ "label": "Date de dernière adhésion",
+ "type": "date",
+ },
+ {
+ "name": "previous_solidarity_transports_count",
+ "label": "Nombre de transports solidaires précédents",
+ "type": "number",
+ },
+ {
+ "name": "comment",
+ "label": "Commentaire",
+ "type": "textarea",
+ },
+ },
+ },
+ "journeys": map[string]any{
+ "enabled": true,
+ "search_view": "tabs",
+ "solutions": map[string]any{
+ "solidarity_transport": map[string]any{
+ "enabled": true,
+ },
+ "organized_carpool": map[string]any{
+ "enabled": true,
+ },
+ "carpool_operators": map[string]any{
+ "enabled": true,
+ },
+ "transit": map[string]any{
+ "enabled": true,
+ },
+ "fleet_vehicles": map[string]any{
+ "enabled": true,
+ },
+ "knowledge_base": map[string]any{
+ "enabled": true,
+ },
+ },
+ },
+ "solidarity_transport": map[string]any{
+ "enabled": true,
+ "pagination": map[string]any{
+ "trips_items_per_page": 10,
+ "drivers_items_per_page": 10,
+ },
+ "drivers": map[string]any{
+ "documents_types": []string{"membership_form", "driving_licence", "identity_proof", "other"},
+ "validated_profile": map[string]any{
+ "enabled": false,
+ "required": map[string]any{
+ "fields": []string{},
+ "documents": []string{},
+ },
+ },
+ "profile_optional_fields": []map[string]any{
+ {
+ "name": "gender",
+ "label": "Genre",
+ "type": "select",
+ "options": []map[string]string{
+ {"value": "0", "label": "Inconnu"},
+ {"value": "1", "label": "Masculin"},
+ {"value": "2", "label": "Féminin"},
+ {"value": "9", "label": "Sans objet"},
+ },
+ },
+ {
+ "name": "last_subscription_date",
+ "label": "Date de dernière adhésion",
+ "type": "date",
+ },
+ {
+ "name": "comment",
+ "label": "Commentaire",
+ "type": "textarea",
+ },
+ },
+ },
+ "booking_motivations": []map[string]string{
+ {"value": "Administratif", "label": "Administratif (trajet garanti)"},
+ {"value": "Commerce", "label": "Commerce"},
+ {"value": "Courses", "label": "Courses"},
+ {"value": "Insertion", "label": "Insertion (trajet garanti)"},
+ {"value": "Loisirs", "label": "Loisirs"},
+ {"value": "Visite à un proche", "label": "Visite à un proche"},
+ {"value": "Santé", "label": "Santé (trajet garanti)"},
+ {"value": "", "label": "Autre "},
+ },
+ "guaranteed_trip_motivations": []string{"Santé", "Insertion", "Administratif"},
+ },
+ "organized_carpool": map[string]any{
+ "enabled": true,
+ "pagination": map[string]any{
+ "trips_items_per_page": 10,
+ "drivers_items_per_page": 10,
+ },
+ "drivers": map[string]any{
+ "documents_types": []string{"membership_form", "driving_licence", "identity_proof", "other"},
+ "validated_profile": map[string]any{
+ "enabled": true,
+ "required": map[string]any{
+ "fields": []string{},
+ "documents": []string{"driving_licence"},
+ },
+ },
+ },
+ "booking_motivations": []map[string]string{
+ {"value": "Administratif", "label": "Administratif"},
+ {"value": "Commerce", "label": "Commerce"},
+ {"value": "Courses", "label": "Courses"},
+ {"value": "Insertion", "label": "Insertion"},
+ {"value": "Loisirs", "label": "Loisirs"},
+ {"value": "Travail", "label": "Travail"},
+ {"value": "Formation", "label": "Formation"},
+ {"value": "Visite à un proche", "label": "Visite à un proche"},
+ {"value": "Santé", "label": "Santé"},
+ {"value": "", "label": "Autre "},
+ },
+ },
+ "vehicles": map[string]any{
+ "enabled": true,
+ },
+ "vehicles_management": map[string]any{
+ "enabled": true,
+ },
+ "fleets": map[string]any{
+ "vehicle_types": []string{"Voiture", "Voiture sans permis", "Scooter", "Trotinette", "Vélo électrique"},
+ "vehicle_optional_fields": []map[string]any{},
+ },
+ "agenda": map[string]any{
+ "enabled": true,
+ },
+ "directory": map[string]any{
+ "enabled": true,
+ },
+ "support": map[string]any{
+ "enabled": true,
+ "email": "support@mobicoop.fr",
+ },
+ },
"geo": map[string]any{
+ "type": "addok", // Options: "pelias", "addok"
"pelias": map[string]any{
- "url": "https://geocode.ridygo.fr",
+ "url": "https://geocode.ridygo.fr",
+ "autocomplete": "/autocomplete?text=",
+ },
+ "addok": map[string]any{
+ "url": "https://api-adresse.data.gouv.fr",
+ "autocomplete": "/search/?q=",
+ },
+ },
+ "geography": map[string]any{
+ "storage": map[string]any{
+ "index": map[string]any{
+ "type": "memory_rtree",
+ "bleve": map[string]any{
+ "file": "index.bleve",
+ },
+ },
+ },
+ "services": map[string]any{
+ "grpc": map[string]any{
+ "enable": true,
+ "port": 8080,
+ },
+ },
+ "data": map[string]any{
+ "layers": map[string]string{
+ "regions": "https://etalab-datasets.geo.data.gouv.fr/contours-administratifs/latest/geojson/regions-50m.geojson",
+ "departements": "https://etalab-datasets.geo.data.gouv.fr/contours-administratifs/latest/geojson/departements-50m.geojson",
+ "epci": "https://etalab-datasets.geo.data.gouv.fr/contours-administratifs/latest/geojson/epci-50m.geojson",
+ "communes": "https://etalab-datasets.geo.data.gouv.fr/contours-administratifs/latest/geojson/communes-50m.geojson",
+ },
+ },
+ "filters": map[string]any{
+ "enabled": false,
+ "geographies": []map[string]string{},
},
},
}
diff --git a/core/application/administration.go b/core/application/administration.go
new file mode 100755
index 0000000..0e7871d
--- /dev/null
+++ b/core/application/administration.go
@@ -0,0 +1,658 @@
+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
+}
diff --git a/core/application/agenda.go b/core/application/agenda.go
new file mode 100755
index 0000000..53e1c3f
--- /dev/null
+++ b/core/application/agenda.go
@@ -0,0 +1,602 @@
+package application
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "sort"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
+ "git.coopgo.io/coopgo-apps/parcoursmob/services"
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
+ agendastorage "git.coopgo.io/coopgo-platform/agenda/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"
+ ics "github.com/arran4/golang-ical"
+ "github.com/google/uuid"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+
+type AgendaEventsResult struct {
+ Events []agendastorage.Event
+ Groups map[string]any
+}
+
+func (h *ApplicationHandler) GetAgendaEvents(ctx context.Context, minDate, maxDate *time.Time) (*AgendaEventsResult, error) {
+ request := &agenda.GetEventsRequest{
+ Namespaces: []string{"parcoursmob_dispositifs"},
+ }
+
+ if minDate != nil {
+ request.Mindate = timestamppb.New(*minDate)
+ }
+ if maxDate != nil {
+ request.Maxdate = timestamppb.New(*maxDate)
+ }
+
+ resp, err := h.services.GRPC.Agenda.GetEvents(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ responses := []agendastorage.Event{}
+ groupids := []string{}
+ for _, e := range resp.Events {
+ groupids = append(groupids, e.Owners...)
+ responses = append(responses, e.ToStorageType())
+ }
+
+ sort.Sort(sorting.EventsByStartdate(responses))
+
+ groups := map[string]any{}
+ if len(groupids) > 0 {
+ groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, &groupsmanagement.GetGroupsBatchRequest{
+ Groupids: groupids,
+ })
+ if err == nil {
+ for _, g := range groupsresp.Groups {
+ groups[g.Id] = g.ToStorageType()
+ }
+ }
+ }
+
+ return &AgendaEventsResult{
+ Events: responses,
+ Groups: groups,
+ }, nil
+}
+
+
+
+func (h *ApplicationHandler) CreateAgendaEvent(ctx context.Context, name, eventType, description string, address any, allday bool, startdate, enddate *time.Time, starttime, endtime string, maxSubscribers int, file io.Reader, filename string, fileSize int64, documentType, documentName string) (string, error) {
+ // Get current group
+ g := ctx.Value(identification.GroupKey)
+ if g == nil {
+ return "", fmt.Errorf("no group found in context")
+ }
+
+ group := g.(storage.Group)
+
+ data, _ := structpb.NewStruct(map[string]any{
+ "address": address,
+ })
+
+ request := &agenda.CreateEventRequest{
+ Event: &agenda.Event{
+ Namespace: "parcoursmob_dispositifs",
+ Owners: []string{group.ID},
+ Type: eventType,
+ Name: name,
+ Description: description,
+ Startdate: timestamppb.New(*startdate),
+ Enddate: timestamppb.New(*enddate),
+ Starttime: starttime,
+ Endtime: endtime,
+ Allday: allday,
+ MaxSubscribers: int64(maxSubscribers),
+ Data: data,
+ Deleted: false,
+ },
+ }
+
+ resp, err := h.services.GRPC.Agenda.CreateEvent(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ // Handle file upload if provided
+ if file != nil && filename != "" {
+ fileid := uuid.NewString()
+
+ metadata := map[string]string{
+ "file_type": documentType,
+ "file_name": documentName,
+ }
+
+ if err := h.filestorage.Put(file, filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s_%s", resp.Event.Id, fileid, filename), fileSize, metadata); err != nil {
+ return "", err
+ }
+ }
+
+ return resp.Event.Id, nil
+}
+
+
+type AgendaEventResult struct {
+ Event agendastorage.Event
+ Group storage.Group
+ Documents []filestorage.FileInfo
+ Subscribers map[string]any
+ Accounts []any
+}
+
+func (h *ApplicationHandler) GetAgendaEvent(ctx context.Context, eventID string) (*AgendaEventResult, error) {
+ request := &agenda.GetEventRequest{
+ Id: eventID,
+ }
+
+ resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ grouprequest := &groupsmanagement.GetGroupRequest{
+ Id: resp.Event.Owners[0],
+ }
+
+ groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest)
+ if err != nil {
+ return nil, err
+ }
+
+ subscribers := map[string]any{}
+ accids := []string{}
+ for _, v := range resp.Event.Subscriptions {
+ accids = append(accids, v.Subscriber)
+ }
+
+ if len(accids) > 0 {
+ subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
+ ctx,
+ &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: accids,
+ },
+ )
+ if err == nil {
+ for _, sub := range subscriberresp.Accounts {
+ subscribers[sub.Id] = sub.ToStorageType()
+ }
+ }
+ }
+
+ g := ctx.Value(identification.GroupKey)
+ if g == nil {
+ return nil, fmt.Errorf("no group found in context")
+ }
+
+ group := g.(storage.Group)
+
+ accountids := []string{}
+ for _, m := range group.Members {
+ if !contains(resp.Event.Subscriptions, m) {
+ accountids = append(accountids, m)
+ }
+ }
+
+ accounts := []any{}
+ if len(accountids) > 0 {
+ accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
+ ctx,
+ &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: accountids,
+ },
+ )
+ if err == nil {
+ for _, acc := range accountresp.Accounts {
+ accounts = append(accounts, acc)
+ }
+ }
+ }
+
+ documents := h.filestorage.List(filestorage.PREFIX_AGENDA + "/" + eventID)
+
+ return &AgendaEventResult{
+ Event: resp.Event.ToStorageType(),
+ Group: groupresp.Group.ToStorageType(),
+ Documents: documents,
+ Subscribers: subscribers,
+ Accounts: accounts,
+ }, nil
+}
+
+
+func (h *ApplicationHandler) SubscribeToAgendaEvent(ctx context.Context, eventID, subscriber string, subscriptionData map[string]any) error {
+ datapb, err := structpb.NewStruct(subscriptionData)
+ if err != nil {
+ return err
+ }
+
+ request := &agenda.SubscribeEventRequest{
+ Eventid: eventID,
+ Subscriber: subscriber,
+ Data: datapb,
+ }
+
+ _, err = h.services.GRPC.Agenda.SubscribeEvent(ctx, request)
+ return err
+}
+
+func (h *ApplicationHandler) UnsubscribeFromAgendaEvent(ctx context.Context, eventID, subscribeID, motif, currentUserID, currentUserDisplayName, currentUserEmail, currentGroupID, currentGroupName string) error {
+ // Get the event first
+ request := &agenda.GetEventRequest{
+ Id: eventID,
+ }
+
+ resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
+ if err != nil {
+ return err
+ }
+
+ // Find subscription data for the subscriber being removed
+ var s_b_id, s_b_name, s_b_email, s_b_group_id, s_b_group_name string
+ for i := range resp.Event.Subscriptions {
+ if resp.Event.Subscriptions[i].Subscriber == subscribeID {
+ s_b_id = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["id"].GetStringValue()
+ s_b_name = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["display_name"].GetStringValue()
+ s_b_email = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["email"].GetStringValue()
+ s_b_group_id = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["id"].GetStringValue()
+ s_b_group_name = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["name"].GetStringValue()
+ break
+ }
+ }
+
+ data := map[string]any{
+ "subscribed_by": map[string]any{
+ "user": map[string]any{
+ "id": s_b_id,
+ "display_name": s_b_name,
+ "email": s_b_email,
+ },
+ "group": map[string]any{
+ "id": s_b_group_id,
+ "name": s_b_group_name,
+ },
+ },
+ "unsubscribed_by": map[string]any{
+ "user": map[string]any{
+ "id": currentUserID,
+ "display_name": currentUserDisplayName,
+ "email": currentUserEmail,
+ },
+ "group": map[string]any{
+ "id": currentGroupID,
+ "name": currentGroupName,
+ },
+ },
+ "motif": motif,
+ }
+
+ datapb, err := structpb.NewStruct(data)
+ if err != nil {
+ return err
+ }
+
+ deleteRequest := &agenda.DeleteSubscriptionRequest{
+ Subscriber: subscribeID,
+ Eventid: eventID,
+ Data: datapb,
+ }
+
+ _, err = h.services.GRPC.Agenda.DeleteSubscription(ctx, deleteRequest)
+ if err != nil {
+ return err
+ }
+
+ // Send email notification
+ emailData := map[string]any{
+ "motif": motif,
+ "user": currentUserDisplayName,
+ "subscriber": fmt.Sprintf("http://localhost:9000/app/beneficiaries/%s", subscribeID),
+ "link": fmt.Sprintf("http://localhost:9000/app/agenda/%s", eventID),
+ }
+
+ if err := h.emailing.Send("delete_subscriber.request", s_b_email, emailData); err != nil {
+ log.Error().Err(err).Msg("Cannot send email")
+ // Don't return error for email failure
+ }
+
+ return nil
+}
+
+type AgendaEventHistoryResult struct {
+ Event agendastorage.Event
+ Group storage.Group
+ Subscribers map[string]any
+ Accounts []any
+}
+
+func (h *ApplicationHandler) GetAgendaEventHistory(ctx context.Context, eventID string) (*AgendaEventHistoryResult, error) {
+ request := &agenda.GetEventRequest{
+ Id: eventID,
+ }
+
+ resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ grouprequest := &groupsmanagement.GetGroupRequest{
+ Id: resp.Event.Owners[0],
+ }
+
+ groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest)
+ if err != nil {
+ return nil, err
+ }
+
+ subscribers := map[string]any{}
+
+ accids := []string{}
+ for _, v := range resp.Event.DeletedSubscription {
+ accids = append(accids, v.Subscriber)
+ }
+
+ if len(accids) > 0 {
+ subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
+ ctx,
+ &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: accids,
+ },
+ )
+
+ if err == nil {
+ for _, sub := range subscriberresp.Accounts {
+ subscribers[sub.Id] = sub.ToStorageType()
+ }
+ }
+ }
+
+ g := ctx.Value(identification.GroupKey)
+ if g == nil {
+ return nil, fmt.Errorf("no group found in context")
+ }
+
+ group := g.(storage.Group)
+
+ accountids := []string{}
+ for _, m := range group.Members {
+ if !contains(resp.Event.DeletedSubscription, m) {
+ accountids = append(accountids, m)
+ }
+ }
+
+ accounts := []any{}
+ if len(accountids) > 0 {
+ accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
+ ctx,
+ &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: accountids,
+ },
+ )
+
+ if err == nil {
+ for _, acc := range accountresp.Accounts {
+ accounts = append(accounts, acc)
+ }
+ }
+ }
+
+ return &AgendaEventHistoryResult{
+ Event: resp.Event.ToStorageType(),
+ Group: groupresp.Group.ToStorageType(),
+ Subscribers: subscribers,
+ Accounts: accounts,
+ }, nil
+}
+
+func (h *ApplicationHandler) AddEventDocument(ctx context.Context, eventID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error {
+ fileid := uuid.NewString()
+
+ metadata := map[string]string{
+ "type": documentType,
+ "name": documentName,
+ }
+
+ if err := h.filestorage.Put(file, filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s_%s", eventID, fileid, filename), fileSize, metadata); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *ApplicationHandler) GetEventDocument(ctx context.Context, eventID, document string) (io.Reader, *filestorage.FileInfo, error) {
+ file, info, err := h.filestorage.Get(filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s", eventID, document))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return file, info, nil
+}
+
+
+
+func contains(s []*agenda.Subscription, e string) bool {
+ for _, a := range s {
+ if a.Subscriber == e {
+ return true
+ }
+ }
+ return false
+}
+
+func (h *ApplicationHandler) UpdateAgendaEvent(ctx context.Context, eventID, name, eventType, description string, address any, allday bool, startdate, enddate *time.Time, starttime, endtime string, maxSubscribers int) (string, error) {
+ // Get current group
+ g := ctx.Value(identification.GroupKey)
+ if g == nil {
+ return "", fmt.Errorf("no group found in context")
+ }
+
+ group := g.(storage.Group)
+
+ // Get existing event first
+ getRequest := &agenda.GetEventRequest{
+ Id: eventID,
+ }
+
+ resp, err := h.services.GRPC.Agenda.GetEvent(ctx, getRequest)
+ if err != nil {
+ return "", err
+ }
+
+ data, _ := structpb.NewStruct(map[string]any{
+ "address": address,
+ })
+
+ request := &agenda.UpdateEventRequest{
+ Event: &agenda.Event{
+ Namespace: "parcoursmob_dispositifs",
+ Id: eventID,
+ Owners: []string{group.ID},
+ Type: eventType,
+ Name: name,
+ Description: description,
+ Startdate: timestamppb.New(*startdate),
+ Enddate: timestamppb.New(*enddate),
+ Starttime: starttime,
+ Endtime: endtime,
+ Allday: allday,
+ MaxSubscribers: int64(maxSubscribers),
+ Data: data,
+ Subscriptions: resp.Event.Subscriptions,
+ },
+ }
+
+ updateResp, err := h.services.GRPC.Agenda.UpdateEvent(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ return updateResp.Event.Id, nil
+}
+
+
+func (h *ApplicationHandler) DeleteAgendaEvent(ctx context.Context, eventID string) error {
+ request := &agenda.GetEventRequest{
+ Id: eventID,
+ }
+
+ resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
+ if err != nil {
+ return err
+ }
+
+ updateRequest := &agenda.UpdateEventRequest{
+ Event: &agenda.Event{
+ Namespace: resp.Event.Namespace,
+ Id: resp.Event.Id,
+ Owners: resp.Event.Owners,
+ Type: resp.Event.Type,
+ Name: resp.Event.Name,
+ Description: resp.Event.Description,
+ Startdate: resp.Event.Startdate,
+ Enddate: resp.Event.Enddate,
+ Starttime: resp.Event.Starttime,
+ Endtime: resp.Event.Endtime,
+ Allday: resp.Event.Allday,
+ MaxSubscribers: int64(resp.Event.MaxSubscribers),
+ Data: resp.Event.Data,
+ Subscriptions: resp.Event.Subscriptions,
+ Deleted: true,
+ },
+ }
+
+ _, err = h.services.GRPC.Agenda.UpdateEvent(ctx, updateRequest)
+ return err
+}
+
+type CalendarResult struct {
+ CalendarData string
+}
+
+func (h *ApplicationHandler) GenerateGlobalCalendar(ctx context.Context) (*CalendarResult, error) {
+ events, err := h.services.GetAgendaEvents()
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda events")
+ return nil, err
+ }
+
+ calendar, err := h.icsCalendar(events)
+ if err != nil {
+ return nil, err
+ }
+
+ return &CalendarResult{
+ CalendarData: calendar.Serialize(),
+ }, nil
+}
+
+func (h *ApplicationHandler) GenerateOrganizationCalendar(ctx context.Context, groupID string) (*CalendarResult, error) {
+ events, err := h.services.GetAgendaEvents()
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda events")
+ return nil, err
+ }
+
+ filteredEvents := []services.AgendaEvent{}
+ for _, e := range events {
+ for _, g := range e.Owners {
+ if g == groupID {
+ filteredEvents = append(filteredEvents, e)
+ break
+ }
+ }
+ }
+
+ calendar, err := h.icsCalendar(filteredEvents)
+ if err != nil {
+ return nil, err
+ }
+
+ return &CalendarResult{
+ CalendarData: calendar.Serialize(),
+ }, nil
+}
+
+func (h *ApplicationHandler) icsCalendar(events []services.AgendaEvent) (*ics.Calendar, error) {
+ calendar := ics.NewCalendarFor(h.config.GetString("service_name"))
+
+ for _, e := range events {
+ vevent := ics.NewEvent(e.ID)
+ vevent.SetSummary(e.Name)
+ vevent.SetDescription(e.Description)
+ if e.Allday {
+ vevent.SetAllDayStartAt(e.Startdate)
+ if e.Enddate.After(e.Startdate) {
+ vevent.SetAllDayEndAt(e.Enddate.Add(24 * time.Hour))
+ }
+ } else {
+ timeloc, err := time.LoadLocation("Europe/Paris")
+ if err != nil {
+ log.Error().Err(err).Msg("Tried to load timezone location Europe/Paris. Error. Missing zones in container ?")
+ return nil, err
+ }
+ vevent.SetStartAt(e.Startdate.In(timeloc))
+ vevent.SetEndAt(e.Enddate.In(timeloc))
+ }
+ calendar.AddVEvent(vevent)
+ }
+
+ return calendar, nil
+}
+
+
+
+
+
diff --git a/handlers/application/application.go b/core/application/application.go
old mode 100644
new mode 100755
similarity index 75%
rename from handlers/application/application.go
rename to core/application/application.go
index 9d85ae4..81fb6b1
--- a/handlers/application/application.go
+++ b/core/application/application.go
@@ -4,41 +4,35 @@ import (
"errors"
"net/http"
- "git.coopgo.io/coopgo-apps/parcoursmob/renderer"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
"git.coopgo.io/coopgo-platform/emailing"
"git.coopgo.io/coopgo-platform/groups-management/storage"
- "github.com/coreos/go-oidc"
+ "github.com/coreos/go-oidc/v3/oidc"
"github.com/spf13/viper"
)
type ApplicationHandler struct {
config *viper.Viper
- Renderer *renderer.Renderer
services *services.ServicesHandler
cache cache.CacheHandler
filestorage cache.FileStorage
emailing *emailing.Mailer
+ idp *identification.IdentificationProvider
}
-func NewApplicationHandler(cfg *viper.Viper, svc *services.ServicesHandler, cache cache.CacheHandler, filestorage cache.FileStorage, emailing *emailing.Mailer) (*ApplicationHandler, error) {
- templates_root := cfg.GetString("templates.root")
- renderer := renderer.NewRenderer(cfg, templates_root)
+func NewApplicationHandler(cfg *viper.Viper, svc *services.ServicesHandler, cache cache.CacheHandler, filestorage cache.FileStorage, emailing *emailing.Mailer, idp *identification.IdentificationProvider) (*ApplicationHandler, error) {
return &ApplicationHandler{
config: cfg,
- Renderer: renderer,
services: svc,
cache: cache,
filestorage: filestorage,
emailing: emailing,
+ idp: idp,
}, nil
}
-func (h *ApplicationHandler) NotFound(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusNotFound)
-}
func (h *ApplicationHandler) templateFile(file string) string {
return h.config.GetString("templates.root") + file
@@ -70,4 +64,4 @@ func (h *ApplicationHandler) currentUser(r *http.Request) (current_user_token *o
current_user_claims = c.(map[string]any)
return current_user_token, current_user_claims, nil
-}
+}
\ No newline at end of file
diff --git a/core/application/auth.go b/core/application/auth.go
new file mode 100644
index 0000000..bccba15
--- /dev/null
+++ b/core/application/auth.go
@@ -0,0 +1,240 @@
+package application
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "io"
+ "time"
+
+ "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
+ mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
+ ma "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/rs/zerolog/log"
+)
+
+type OAuth2CallbackResult struct {
+ RedirectURL string
+ IDToken string
+}
+
+func (h *ApplicationHandler) ProcessOAuth2Callback(code string, redirectSession string) (*OAuth2CallbackResult, error) {
+ oauth2Token, err := h.idp.OAuth2Config.Exchange(context.Background(), code)
+ if err != nil {
+ log.Error().Err(err).Msg("Exchange error")
+ return nil, err
+ }
+
+ // Extract the ID Token from OAuth2 token.
+ rawIDToken, ok := oauth2Token.Extra("id_token").(string)
+ if !ok {
+ log.Error().Msg("Cannot retrieve ID token")
+ return nil, errors.New("cannot retrieve ID token")
+ }
+
+ _, err = h.idp.TokenVerifier.Verify(context.Background(), rawIDToken)
+ if err != nil {
+ log.Error().Err(err).Msg("Not able to verify token")
+ return nil, err
+ }
+
+ redirect := "/app/"
+ if redirectSession != "" {
+ redirect = redirectSession
+ }
+
+ return &OAuth2CallbackResult{
+ RedirectURL: redirect,
+ IDToken: rawIDToken,
+ }, nil
+}
+
+type LostPasswordInitResult struct {
+ Success bool
+}
+
+func (h *ApplicationHandler) InitiateLostPassword(email string) (*LostPasswordInitResult, error) {
+ account, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &mobilityaccounts.GetAccountUsernameRequest{
+ Username: email,
+ Namespace: "parcoursmob",
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ b := make([]byte, 16)
+ if _, err := io.ReadFull(rand.Reader, b); err != nil {
+ return nil, err
+ }
+ key := base64.RawURLEncoding.EncodeToString(b)
+
+ passwordretrieval := map[string]any{
+ "username": email,
+ "account_id": account.Account.Id,
+ "key": key,
+ }
+
+ h.cache.PutWithTTL("retrieve-password/"+key, passwordretrieval, 72*time.Hour)
+
+ if err := h.emailing.Send("auth.retrieve_password", email, passwordretrieval); err != nil {
+ return nil, err
+ }
+
+ return &LostPasswordInitResult{Success: true}, nil
+}
+
+type LostPasswordRecoverResult struct {
+ Success bool
+}
+
+func (h *ApplicationHandler) RecoverLostPassword(key, newPassword string) (*LostPasswordRecoverResult, error) {
+ recover, err := h.cache.Get("retrieve-password/" + key)
+ if err != nil {
+ return nil, err
+ }
+
+ if newPassword == "" {
+ return nil, errors.New("password is empty")
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.ChangePassword(context.TODO(), &mobilityaccounts.ChangePasswordRequest{
+ Id: recover.(map[string]any)["account_id"].(string),
+ Password: newPassword,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ err = h.cache.Delete("retrieve-password/" + key)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to delete password recovery key")
+ }
+
+ return &LostPasswordRecoverResult{Success: true}, nil
+}
+
+func (h *ApplicationHandler) GetPasswordRecoveryData(key string) (map[string]any, error) {
+ recover, err := h.cache.Get("retrieve-password/" + key)
+ if err != nil {
+ return nil, err
+ }
+ return recover.(map[string]any), nil
+}
+
+type OnboardingResult struct {
+ Success bool
+}
+
+func (h *ApplicationHandler) CompleteOnboarding(key, password, firstName, lastName string) (*OnboardingResult, error) {
+ onboarding, err := h.cache.Get("onboarding/" + key)
+ if err != nil {
+ return nil, err
+ }
+
+ onboardingmap := onboarding.(map[string]any)
+
+ if password == "" {
+ return nil, errors.New("password is empty")
+ }
+
+ groups := []string{
+ onboardingmap["group"].(string),
+ }
+
+ if onboardingmap["admin"].(bool) {
+ groups = append(groups, onboardingmap["group"].(string)+":admin")
+ }
+
+ display_name := firstName + " " + lastName
+ account := &ma.Account{
+ Authentication: ma.AccountAuth{
+ Local: ma.LocalAuth{
+ Username: onboardingmap["username"].(string),
+ Password: password,
+ },
+ },
+ Namespace: "parcoursmob",
+ Data: map[string]any{
+ "display_name": display_name,
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": onboardingmap["username"],
+ "groups": groups,
+ },
+ }
+
+ acc, err := mobilityaccounts.AccountFromStorageType(account)
+ if err != nil {
+ return nil, err
+ }
+
+ request := &mobilityaccounts.RegisterRequest{
+ Account: acc,
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.Register(context.TODO(), request)
+ if err != nil {
+ return nil, err
+ }
+
+ err = h.cache.Delete("onboarding/" + key)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to delete onboarding key")
+ }
+
+ return &OnboardingResult{Success: true}, nil
+}
+
+func (h *ApplicationHandler) GetOnboardingData(key string) (map[string]any, error) {
+ onboarding, err := h.cache.Get("onboarding/" + key)
+ if err != nil {
+ return nil, err
+ }
+ return onboarding.(map[string]any), nil
+}
+
+type UserGroupsResult struct {
+ Groups []groupsstorage.Group
+}
+
+func (h *ApplicationHandler) GetUserGroups(idtoken *oidc.IDToken) (*UserGroupsResult, error) {
+ var claims map[string]any
+ err := idtoken.Claims(&claims)
+ if err != nil {
+ return nil, err
+ }
+
+ g := claims["groups"]
+ groups_interface, ok := g.([]any)
+ if !ok {
+ return nil, errors.New("invalid groups format")
+ }
+
+ groups := []string{}
+ for _, v := range groups_interface {
+ groups = append(groups, v.(string))
+ }
+
+ request := &grpcapi.GetGroupsBatchRequest{
+ Groupids: groups,
+ }
+
+ resp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), request)
+ if err != nil {
+ return nil, err
+ }
+
+ var groupsresponse []groupsstorage.Group
+ for _, group := range resp.Groups {
+ if group.Namespace != "parcoursmob_organizations" {
+ continue
+ }
+ g := group.ToStorageType()
+ groupsresponse = append(groupsresponse, g)
+ }
+
+ return &UserGroupsResult{Groups: groupsresponse}, nil
+}
\ No newline at end of file
diff --git a/core/application/beneficiaries.go b/core/application/beneficiaries.go
new file mode 100644
index 0000000..2a81358
--- /dev/null
+++ b/core/application/beneficiaries.go
@@ -0,0 +1,929 @@
+package application
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "image/png"
+ "io"
+ "net/http"
+ "sort"
+ "strings"
+ "time"
+
+ formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ profilepictures "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/profile-pictures"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
+ agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
+ "git.coopgo.io/coopgo-platform/carpool-service/servers/grpc/proto"
+ 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"
+ "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
+ solidaritytransformers "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
+ solidaritytypes "git.coopgo.io/coopgo-platform/solidarity-transport/types"
+ "github.com/google/uuid"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type BeneficiariesResult struct {
+ Accounts []mobilityaccountsstorage.Account
+ CacheID string
+}
+
+func (h *ApplicationHandler) GetBeneficiaries(ctx context.Context, searchFilter string, archivedFilter bool) (*BeneficiariesResult, error) {
+ accounts, err := h.getBeneficiariesWithFilters(ctx, searchFilter, archivedFilter)
+ if err != nil {
+ return nil, err
+ }
+
+ sort.Sort(sorting.BeneficiariesByName(accounts))
+
+ cacheID := uuid.NewString()
+ h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour)
+
+ return &BeneficiariesResult{
+ Accounts: accounts,
+ CacheID: cacheID,
+ }, nil
+}
+
+func (h *ApplicationHandler) CreateBeneficiary(ctx context.Context, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address any, gender string, otherProperties any) (string, error) {
+ // Get current group
+ g := ctx.Value(identification.GroupKey)
+ if g == nil {
+ return "", fmt.Errorf("no group found in context")
+ }
+ group := g.(storage.Group)
+
+ // Create data map for the beneficiary
+ dataMap := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ "phone_number": phoneNumber,
+ "file_number": fileNumber,
+ "gender": gender,
+ }
+
+ // Convert birthdate to string format for structpb compatibility
+ if birthdate != nil {
+ dataMap["birthdate"] = birthdate.Format("2006-01-02")
+ }
+
+ if address != nil {
+ dataMap["address"] = address
+ }
+ if otherProperties != nil {
+ dataMap["other_properties"] = otherProperties
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ request := &mobilityaccounts.RegisterRequest{
+ Account: &mobilityaccounts.Account{
+ Namespace: "parcoursmob_beneficiaries",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ subscribe := &groupsmanagement.SubscribeRequest{
+ Groupid: group.ID,
+ Memberid: resp.Account.Id,
+ }
+
+ _, err = h.services.GRPC.GroupsManagement.Subscribe(ctx, subscribe)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.Account.Id, nil
+}
+
+type BeneficiaryDataResult struct {
+ Account mobilityaccountsstorage.Account
+ Bookings []fleetsstorage.Booking
+ Organizations []any
+ Documents []filestorage.FileInfo
+ EventsList []Event_Beneficiary
+ SolidarityTransportStats map[string]int64
+ SolidarityTransportBookings []*solidaritytypes.Booking
+ SolidarityDriversMap map[string]mobilityaccountsstorage.Account
+ OrganizedCarpoolStats map[string]int64
+ OrganizedCarpoolBookings []*proto.CarpoolServiceBooking
+ OrganizedCarpoolDriversMap map[string]mobilityaccountsstorage.Account
+ WalletBalance float64
+}
+
+func (h *ApplicationHandler) GetBeneficiaryData(ctx context.Context, beneficiaryID string) (*BeneficiaryDataResult, error) {
+ // Get beneficiary account
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Security check: ensure this is actually a beneficiary account
+ if resp.Account.Namespace != "parcoursmob_beneficiaries" {
+ return nil, fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, resp.Account.Namespace)
+ }
+
+ account := resp.Account.ToStorageType()
+
+ // Get documents
+ documents := h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiaryID)
+
+ // Get events subscriptions
+ subscriptionRequest := &agenda.GetSubscriptionByUserRequest{
+ Subscriber: beneficiaryID,
+ }
+
+ subscriptionResp, err := h.services.GRPC.Agenda.GetSubscriptionByUser(ctx, subscriptionRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ events := []agendastorage.Event{}
+ currentTime := time.Now().Truncate(24 * time.Hour)
+
+ for _, e := range subscriptionResp.Subscription {
+ eventRequest := &agenda.GetEventRequest{
+ Id: e.Eventid,
+ }
+ eventResp, err := h.services.GRPC.Agenda.GetEvent(ctx, eventRequest)
+ if err != nil {
+ return nil, err
+ }
+ events = append(events, eventResp.Event.ToStorageType())
+ }
+
+ sort.Sort(sorting.EventsByStartdate(events))
+
+ // Get bookings
+ bookingsRequest := &fleets.GetDriverBookingsRequest{
+ Driver: beneficiaryID,
+ }
+ bookingsResp, err := h.services.GRPC.Fleets.GetDriverBookings(ctx, bookingsRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ bookings := []fleetsstorage.Booking{}
+ for _, b := range bookingsResp.Bookings {
+ bookings = append(bookings, b.ToStorageType())
+ }
+
+ // Build events list
+ var eventsList []Event_Beneficiary
+ var statusEvent int
+
+ for _, e := range events {
+ if e.Startdate.After(currentTime) {
+ statusEvent = 1
+ } else if e.Startdate.Before(currentTime) && e.Enddate.After(currentTime) || e.Enddate.Equal(currentTime) {
+ statusEvent = 2
+ } else {
+ statusEvent = 3
+ }
+
+ event := Event{
+ NameVal: e.Name,
+ DateVal: e.Startdate,
+ DateEndVal: e.Enddate,
+ TypeVal: e.Type,
+ IDVal: e.ID,
+ DbVal: "/app/agenda/",
+ IconSet: "calendar",
+ StatusVal: statusEvent,
+ }
+
+ eventsList = append(eventsList, event)
+ }
+
+ // Add vehicle bookings to events list
+ var statusBooking int
+ for _, b := range bookings {
+ if b.Enddate.After(currentTime) || b.Enddate.Equal(currentTime) {
+ getVehicleRequest := &fleets.GetVehicleRequest{
+ Vehicleid: b.Vehicleid,
+ }
+
+ getVehicleResp, err := h.services.GRPC.Fleets.GetVehicle(ctx, getVehicleRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ if b.Startdate.After(currentTime) {
+ statusBooking = 1
+ } else if b.Startdate.Before(currentTime) && b.Enddate.After(currentTime) || b.Enddate.Equal(currentTime) {
+ statusBooking = 2
+ } else {
+ statusBooking = 3
+ }
+
+ event := Event{
+ NameVal: getVehicleResp.Vehicle.ToStorageType().Data["name"].(string),
+ DateVal: b.Startdate,
+ DateEndVal: b.Enddate,
+ TypeVal: "Réservation de véhicule",
+ IDVal: b.ID,
+ DbVal: "/app/vehicles-management/bookings/",
+ IconSet: "vehicle",
+ StatusVal: statusBooking,
+ }
+
+ eventsList = append(eventsList, event)
+ }
+ }
+
+ // Get solidarity transport bookings (all statuses for display)
+ solidarityResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{
+ Passengerid: beneficiaryID,
+ StartDate: timestamppb.New(time.Now().Add(-365 * 24 * time.Hour)),
+ EndDate: timestamppb.New(time.Now().Add(365 * 24 * time.Hour)),
+ })
+
+ protoBookings := []*gen.SolidarityTransportBooking{}
+ if err == nil {
+ protoBookings = solidarityResp.Bookings
+ } else {
+ log.Error().Err(err).Msg("error retrieving solidarity transport bookings for beneficiary")
+ }
+
+ // Convert proto bookings to types with geojson.Feature
+ solidarityTransportBookings := []*solidaritytypes.Booking{}
+ for _, protoBooking := range protoBookings {
+ booking, err := solidaritytransformers.BookingProtoToType(protoBooking)
+ if err != nil {
+ log.Error().Err(err).Msg("error converting booking proto to type")
+ continue
+ }
+ solidarityTransportBookings = append(solidarityTransportBookings, booking)
+ }
+
+ // Don't filter out replaced bookings from beneficiary profile - show all bookings
+
+ // Collect unique driver IDs
+ driverIDs := []string{}
+ driverIDsMap := make(map[string]bool)
+ for _, booking := range solidarityTransportBookings {
+ if booking.DriverId != "" {
+ if !driverIDsMap[booking.DriverId] {
+ driverIDs = append(driverIDs, booking.DriverId)
+ driverIDsMap[booking.DriverId] = true
+ }
+ }
+ }
+
+ // Get drivers in batch
+ driversMap := make(map[string]mobilityaccountsstorage.Account)
+ if len(driverIDs) > 0 {
+ driversResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: driverIDs,
+ })
+ if err == nil {
+ for _, account := range driversResp.Accounts {
+ a := account.ToStorageType()
+ driversMap[a.ID] = a
+ }
+ }
+ }
+
+ // Calculate stats only for validated bookings
+ solidarityTransportStats := map[string]int64{
+ "count": 0,
+ "km": 0,
+ }
+
+ for _, b := range solidarityTransportBookings {
+ if b.Status == "VALIDATED" {
+ solidarityTransportStats["count"] = solidarityTransportStats["count"] + 1
+ if b.Journey != nil {
+ solidarityTransportStats["km"] = solidarityTransportStats["km"] + b.Journey.PassengerDistance
+ }
+
+ // Add to events list
+ event := Event{
+ NameVal: fmt.Sprintf("%s (%d km)", b.Journey.PassengerDrop.Properties.MustString("label", ""), b.Journey.PassengerDistance),
+ DateVal: b.Journey.PassengerPickupDate,
+ DateEndVal: b.Journey.PassengerPickupDate,
+ TypeVal: "Transport solidaire",
+ IDVal: b.Id,
+ DbVal: "/app/solidarity-transport/bookings/",
+ IconSet: "vehicle",
+ StatusVal: 1,
+ }
+
+ eventsList = append(eventsList, event)
+ }
+ }
+
+ // Get organized carpool bookings
+ carpoolBookingsResp, err := h.services.GRPC.CarpoolService.GetUserBookings(ctx, &proto.GetUserBookingsRequest{
+ UserId: beneficiaryID,
+ })
+
+ organizedCarpoolBookings := []*proto.CarpoolServiceBooking{}
+ if err == nil {
+ organizedCarpoolBookings = carpoolBookingsResp.Bookings
+ } else {
+ log.Error().Err(err).Msg("error retrieving organized carpool bookings for beneficiary")
+ }
+
+ // Collect unique driver IDs from organized carpool bookings
+ carpoolDriverIDs := []string{}
+ carpoolDriverIDsMap := make(map[string]bool)
+ for _, booking := range organizedCarpoolBookings {
+ if booking.Driver != nil && booking.Driver.Id != "" {
+ if !carpoolDriverIDsMap[booking.Driver.Id] {
+ carpoolDriverIDs = append(carpoolDriverIDs, booking.Driver.Id)
+ carpoolDriverIDsMap[booking.Driver.Id] = true
+ }
+ }
+ }
+
+ // Get organized carpool drivers in batch
+ organizedCarpoolDriversMap := make(map[string]mobilityaccountsstorage.Account)
+ if len(carpoolDriverIDs) > 0 {
+ carpoolDriversResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: carpoolDriverIDs,
+ })
+ if err == nil {
+ for _, account := range carpoolDriversResp.Accounts {
+ a := account.ToStorageType()
+ organizedCarpoolDriversMap[a.ID] = a
+ }
+ }
+ }
+
+ // Calculate organized carpool stats (only confirmed bookings)
+ organizedCarpoolStats := map[string]int64{
+ "count": 0,
+ "km": 0,
+ }
+
+ for _, cb := range organizedCarpoolBookings {
+ if cb.Status == proto.CarpoolServiceBookingStatus_CONFIRMED {
+ organizedCarpoolStats["count"]++
+ if cb.Distance != nil {
+ organizedCarpoolStats["km"] += *cb.Distance
+ }
+
+ // Build journey name from drop address and distance for events
+ journeyName := "Covoiturage"
+ if cb.PassengerDropAddress != nil {
+ if cb.Distance != nil {
+ journeyName = fmt.Sprintf("%s (%d km)", *cb.PassengerDropAddress, *cb.Distance)
+ } else {
+ journeyName = *cb.PassengerDropAddress
+ }
+ }
+
+ // Get departure date
+ departureDate := time.Now()
+ if cb.PassengerPickupDate != nil {
+ departureDate = cb.PassengerPickupDate.AsTime()
+ }
+
+ event := Event{
+ NameVal: journeyName,
+ DateVal: departureDate,
+ DateEndVal: departureDate,
+ TypeVal: "Covoiturage solidaire",
+ IDVal: cb.Id,
+ DbVal: "/app/organized-carpool/bookings/",
+ IconSet: "vehicle",
+ StatusVal: 1,
+ }
+
+ eventsList = append(eventsList, event)
+ }
+ }
+
+ sortByDate(eventsList)
+
+ // Get organizations
+ groupsRequest := &groupsmanagement.GetGroupsRequest{
+ Namespaces: []string{"parcoursmob_organizations"},
+ Member: beneficiaryID,
+ }
+
+ groupsResp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, groupsRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ organizations := []any{}
+ for _, o := range groupsResp.Groups {
+ organizations = append(organizations, o.ToStorageType())
+ }
+
+ // Calculate wallet balance
+ walletBalance := h.calculateWalletBalance(account)
+
+ return &BeneficiaryDataResult{
+ Account: account,
+ Bookings: bookings,
+ Organizations: organizations,
+ Documents: documents,
+ EventsList: eventsList,
+ SolidarityTransportStats: solidarityTransportStats,
+ SolidarityTransportBookings: solidarityTransportBookings,
+ SolidarityDriversMap: driversMap,
+ OrganizedCarpoolStats: organizedCarpoolStats,
+ OrganizedCarpoolBookings: organizedCarpoolBookings,
+ OrganizedCarpoolDriversMap: organizedCarpoolDriversMap,
+ WalletBalance: walletBalance,
+ }, nil
+}
+
+type BeneficiaryResult struct {
+ Account mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) GetBeneficiary(ctx context.Context, beneficiaryID string) (*BeneficiaryResult, error) {
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Security check: ensure this is actually a beneficiary account
+ if resp.Account.Namespace != "parcoursmob_beneficiaries" {
+ return nil, fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, resp.Account.Namespace)
+ }
+
+ return &BeneficiaryResult{
+ Account: resp.Account.ToStorageType(),
+ }, nil
+}
+
+func (h *ApplicationHandler) UpdateBeneficiary(ctx context.Context, beneficiaryID, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address any, gender string, otherProperties any) (string, error) {
+ // Security check: verify the account exists and is a beneficiary
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return "", err
+ }
+ if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
+ return "", fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
+ }
+
+ // Create data map for the beneficiary
+ dataMap := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ "phone_number": phoneNumber,
+ "file_number": fileNumber,
+ "gender": gender,
+ }
+
+ // Handle birthdate conversion for protobuf compatibility
+ if birthdate != nil {
+ dataMap["birthdate"] = birthdate.Format(time.RFC3339)
+ }
+
+ if address != nil {
+ dataMap["address"] = address
+ }
+ if otherProperties != nil {
+ dataMap["other_properties"] = otherProperties
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: beneficiaryID,
+ Namespace: "parcoursmob_beneficiaries",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.Account.Id, nil
+}
+
+func (h *ApplicationHandler) ArchiveBeneficiary(ctx context.Context, beneficiaryID string) error {
+ // Security check: verify the account exists and is a beneficiary
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
+ return fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
+ }
+
+ data, err := structpb.NewValue(map[string]any{
+ "archived": true,
+ })
+ if err != nil {
+ return err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: beneficiaryID,
+ Namespace: "parcoursmob_beneficiaries",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ return err
+}
+
+func (h *ApplicationHandler) UnarchiveBeneficiary(ctx context.Context, beneficiaryID string) error {
+ // Security check: verify the account exists and is a beneficiary
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
+ return fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
+ }
+
+ data, err := structpb.NewValue(map[string]any{
+ "archived": false,
+ })
+ if err != nil {
+ return err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: beneficiaryID,
+ Namespace: "parcoursmob_beneficiaries",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ return err
+}
+
+func (h *ApplicationHandler) GetBeneficiaryPicture(ctx context.Context, beneficiaryID string) ([]byte, string, error) {
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, "", err
+ }
+
+ // Security check: ensure this is actually a beneficiary account
+ // if resp.Account.Namespace != "parcoursmob_beneficiaries" {
+ // return nil, "", fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, resp.Account.Namespace)
+ // }
+
+ account := resp.Account.ToStorageType()
+
+ firstName, ok := account.Data["first_name"].(string)
+ if !ok || firstName == "" {
+ firstName = "U"
+ }
+ lastName, ok := account.Data["last_name"].(string)
+ if !ok || lastName == "" {
+ lastName = "U"
+ }
+
+ initials := strings.ToUpper(string(firstName[0]) + string(lastName[0]))
+ picture := profilepictures.DefaultProfilePicture(initials)
+
+ buffer := new(bytes.Buffer)
+ if err := png.Encode(buffer, picture); err != nil {
+ return nil, "", err
+ }
+
+ return buffer.Bytes(), "image/png", nil
+}
+
+func (h *ApplicationHandler) AddBeneficiaryDocument(ctx context.Context, beneficiaryID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error {
+ // Security check: verify the account exists and is a beneficiary
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
+ return fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
+ }
+
+ fileid := uuid.NewString()
+
+ metadata := map[string]string{
+ "type": documentType,
+ "name": documentName,
+ }
+
+ if err := h.filestorage.Put(file, filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s_%s", beneficiaryID, fileid, filename), fileSize, metadata); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *ApplicationHandler) GetBeneficiaryDocument(ctx context.Context, beneficiaryID, document string) (io.Reader, *filestorage.FileInfo, error) {
+ // Security check: verify the account exists and is a beneficiary
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+ if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
+ return nil, nil, fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
+ }
+
+ file, info, err := h.filestorage.Get(filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s", beneficiaryID, document))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return file, info, nil
+}
+
+func (h *ApplicationHandler) DeleteBeneficiaryDocument(ctx context.Context, beneficiaryID, document string) error {
+ return h.DeleteDocument(ctx, BeneficiaryDocumentConfig, beneficiaryID, document)
+}
+
+func (h *ApplicationHandler) getBeneficiariesWithFilters(ctx context.Context, searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) {
+ accounts := []mobilityaccountsstorage.Account{}
+ g := ctx.Value(identification.GroupKey)
+ if g == nil {
+ return accounts, errors.New("no group provided")
+ }
+
+ group := g.(storage.Group)
+
+ request := &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: group.Members,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, request)
+ if err != nil {
+ log.Error().Err(err).Msg("issue in mobilityaccounts call")
+ return accounts, err
+ }
+
+ for _, account := range resp.Accounts {
+ if h.filterAccount(account, searchFilter, archivedFilter) {
+ a := account.ToStorageType()
+ accounts = append(accounts, a)
+ }
+ }
+
+ return accounts, err
+}
+
+func (h *ApplicationHandler) filterAccount(a *mobilityaccounts.Account, searchFilter string, archivedFilter bool) bool {
+ // Search filter
+ if searchFilter != "" {
+ name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
+ if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter)) {
+ return false
+ }
+ }
+
+ // Archived filter
+ if archivedFilter {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return true
+ }
+ return false
+ } else {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return false
+ }
+ }
+
+ return true
+}
+
+type Event_Beneficiary interface {
+ Name() string
+ Date() time.Time
+ DateEnd() time.Time
+ Type() string
+ Db() string
+ ID() string
+ Icons() string
+ Status() int
+}
+
+type Event struct {
+ IDVal string
+ NameVal string
+ DateVal time.Time
+ DateEndVal time.Time
+ TypeVal string
+ DbVal string
+ Deleted bool
+ IconSet string
+ StatusVal int
+}
+
+func (e Event) Name() string {
+ return e.NameVal
+}
+
+func (e Event) Date() time.Time {
+ return e.DateVal
+}
+
+func (e Event) DateEnd() time.Time {
+ return e.DateEndVal
+}
+
+func (e Event) Type() string {
+ return e.TypeVal
+}
+
+func (e Event) ID() string {
+ return e.IDVal
+}
+
+func (e Event) Db() string {
+ return e.DbVal
+}
+
+func (e Event) Icons() string {
+ return e.IconSet
+}
+
+func (e Event) Status() int {
+ return e.StatusVal
+}
+
+func sortByDate(events []Event_Beneficiary) {
+ sort.Slice(events, func(i, j int) bool {
+ return events[i].Date().After(events[j].Date())
+ })
+}
+
+// Utility functions needed by other modules
+func filterAccount(r *http.Request, a *mobilityaccounts.Account) bool {
+ searchFilter, ok := r.URL.Query()["search"]
+
+ if ok && len(searchFilter[0]) > 0 {
+ name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
+ if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter[0])) {
+ return false
+ }
+ }
+
+ archivedFilter, ok := r.URL.Query()["archived"]
+ if ok && archivedFilter[0] == "true" {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return true
+ }
+ return false
+ } else {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]mobilityaccountsstorage.Account, error) {
+ accounts := []mobilityaccountsstorage.Account{}
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ return accounts, errors.New("no group provided")
+ }
+
+ group := g.(storage.Group)
+
+ request := &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: group.Members,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), request)
+ if err != nil {
+ log.Error().Err(err).Msg("issue in mobilityaccounts call")
+ return accounts, err
+ }
+
+ for _, account := range resp.Accounts {
+ if filterAccount(r, account) {
+ a := account.ToStorageType()
+ accounts = append(accounts, a)
+ }
+ }
+
+ return accounts, err
+}
+
+type BeneficiariesForm struct {
+ FirstName string `json:"first_name" validate:"required"`
+ LastName string `json:"last_name" validate:"required"`
+ Email string `json:"email" validate:"required,email"`
+ Birthdate *time.Time `json:"birthdate" validate:"required"`
+ PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
+ FileNumber string `json:"file_number"`
+ Address any `json:"address,omitempty"`
+ Gender string `json:"gender"`
+ OtherProperties any `json:"other_properties,omitempty"`
+}
+
+func parseBeneficiariesForm(r *http.Request) (map[string]any, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+
+ var date *time.Time
+
+ if r.PostFormValue("birthdate") != "" {
+ d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate"))
+ if err != nil {
+ return nil, err
+ }
+ date = &d
+ }
+
+ formData := BeneficiariesForm{
+ FirstName: r.PostFormValue("first_name"),
+ LastName: r.PostFormValue("last_name"),
+ Email: r.PostFormValue("email"),
+ Birthdate: date,
+ PhoneNumber: r.PostFormValue("phone_number"),
+ FileNumber: r.PostFormValue("file_number"),
+ Gender: r.PostFormValue("gender"),
+ }
+
+ if r.PostFormValue("address") != "" {
+ var a any
+ json.Unmarshal([]byte(r.PostFormValue("address")), &a)
+ formData.Address = a
+ }
+
+ if r.PostFormValue("other_properties") != "" {
+ var a any
+ json.Unmarshal([]byte(r.PostFormValue("other_properties")), &a)
+ formData.OtherProperties = a
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return nil, err
+ }
+
+ d, err := json.Marshal(formData)
+ if err != nil {
+ return nil, err
+ }
+
+ var dataMap map[string]any
+ err = json.Unmarshal(d, &dataMap)
+ if err != nil {
+ return nil, err
+ }
+
+ return dataMap, nil
+}
diff --git a/core/application/dashboard.go b/core/application/dashboard.go
new file mode 100755
index 0000000..c25b8c6
--- /dev/null
+++ b/core/application/dashboard.go
@@ -0,0 +1,220 @@
+package application
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "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"
+ fleetstorage "git.coopgo.io/coopgo-platform/fleets/storage"
+ "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/paulmach/orb"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type DashboardResult struct {
+ Accounts []mobilityaccountsstorage.Account
+ Members []mobilityaccountsstorage.Account
+ Events []agendastorage.Event
+ Bookings []fleetstorage.Booking
+ SolidarityDrivers []mobilityaccountsstorage.Account
+ OrganizedCarpoolDrivers []mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) GetDashboardData(ctx context.Context, driverAddressGeoLayer, driverAddressGeoCode string) (*DashboardResult, error) {
+ g := ctx.Value(identification.GroupKey)
+ if g == nil {
+ return nil, fmt.Errorf("no group found in context")
+ }
+ group := g.(storage.Group)
+
+ // Load geography polygons for driver address filtering
+ var driverAddressPolygons []orb.Polygon
+ if driverAddressGeoLayer != "" && driverAddressGeoCode != "" {
+ polygons, err := h.loadGeographyPolygon(driverAddressGeoLayer, driverAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load driver address geography filter")
+ } else {
+ driverAddressPolygons = polygons
+ }
+ }
+
+ // Get accounts (recent beneficiaries)
+ request := &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: group.Members,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ accounts := []mobilityaccountsstorage.Account{}
+
+ for _, account := range resp.Accounts {
+ // Check if not archived
+ if archived, ok := account.Data.AsMap()["archived"].(bool); !ok || !archived {
+ a := account.ToStorageType()
+ accounts = append([]mobilityaccountsstorage.Account{a}, accounts...)
+ }
+ }
+
+ // Fetch remaining data in parallel using goroutines
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+
+ var members []mobilityaccountsstorage.Account
+ var events []agendastorage.Event
+ var bookings []fleetstorage.Booking
+ var solidarityDrivers []mobilityaccountsstorage.Account
+ var organizedCarpoolDrivers []mobilityaccountsstorage.Account
+
+ // Get members
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ m, _, err := h.groupmembers(group.ID)
+ if err == nil {
+ mu.Lock()
+ members = m
+ mu.Unlock()
+ }
+ }()
+
+ // Get events
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ eventsresp, err := h.services.GRPC.Agenda.GetEvents(ctx, &agenda.GetEventsRequest{
+ Namespaces: []string{"parcoursmob_dispositifs"},
+ Mindate: timestamppb.Now(),
+ })
+ if err == nil {
+ mu.Lock()
+ for _, e := range eventsresp.Events {
+ events = append(events, e.ToStorageType())
+ }
+ sort.Sort(sorting.EventsByStartdate(events))
+ mu.Unlock()
+ }
+ }()
+
+ // Get bookings
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ bookingsresp, err := h.services.GRPC.Fleets.GetBookings(ctx, &fleets.GetBookingsRequest{
+ Namespaces: []string{"parcoursmob_dispositifs"},
+ })
+ if err == nil {
+ mu.Lock()
+ for _, b := range bookingsresp.Bookings {
+ if b.Enddate.AsTime().After(time.Now()) {
+ bookings = append(bookings, b.ToStorageType())
+ }
+ }
+ mu.Unlock()
+ }
+ }()
+
+ // Get solidarity transport drivers
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ solidarityRequest := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: []string{"solidarity_drivers"},
+ }
+ solidarityResp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, solidarityRequest)
+ if err == nil {
+ mu.Lock()
+ for _, account := range solidarityResp.Accounts {
+ // Only include non-archived drivers with addresses
+ if archived, ok := account.Data.AsMap()["archived"].(bool); !ok || !archived {
+ if address, ok := account.Data.AsMap()["address"]; ok && address != nil {
+ // Apply geography filter if specified
+ if len(driverAddressPolygons) > 0 {
+ if addr, ok := account.Data.AsMap()["address"].(map[string]interface{}); ok {
+ jsonAddr, err := json.Marshal(addr)
+ if err == nil {
+ addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
+ if err == nil && addrGeojson.Geometry != nil {
+ if point, ok := addrGeojson.Geometry.(orb.Point); ok {
+ if isPointInGeographies(point, driverAddressPolygons) {
+ solidarityDrivers = append(solidarityDrivers, account.ToStorageType())
+ }
+ }
+ }
+ }
+ }
+ } else {
+ solidarityDrivers = append(solidarityDrivers, account.ToStorageType())
+ }
+ }
+ }
+ }
+ mu.Unlock()
+ }
+ }()
+
+ // Get organized carpool drivers
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ carpoolRequest := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: []string{"organized_carpool_drivers"},
+ }
+ carpoolResp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, carpoolRequest)
+ if err == nil {
+ mu.Lock()
+ for _, account := range carpoolResp.Accounts {
+ // Only include non-archived drivers with addresses
+ if archived, ok := account.Data.AsMap()["archived"].(bool); !ok || !archived {
+ if address, ok := account.Data.AsMap()["address"]; ok && address != nil {
+ // Apply geography filter if specified
+ if len(driverAddressPolygons) > 0 {
+ if addr, ok := account.Data.AsMap()["address"].(map[string]interface{}); ok {
+ jsonAddr, err := json.Marshal(addr)
+ if err == nil {
+ addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
+ if err == nil && addrGeojson.Geometry != nil {
+ if point, ok := addrGeojson.Geometry.(orb.Point); ok {
+ if isPointInGeographies(point, driverAddressPolygons) {
+ organizedCarpoolDrivers = append(organizedCarpoolDrivers, account.ToStorageType())
+ }
+ }
+ }
+ }
+ }
+ } else {
+ organizedCarpoolDrivers = append(organizedCarpoolDrivers, account.ToStorageType())
+ }
+ }
+ }
+ }
+ mu.Unlock()
+ }
+ }()
+
+ // Wait for all goroutines to complete
+ wg.Wait()
+
+ return &DashboardResult{
+ Accounts: accounts,
+ Members: members,
+ Events: events,
+ Bookings: bookings,
+ SolidarityDrivers: solidarityDrivers,
+ OrganizedCarpoolDrivers: organizedCarpoolDrivers,
+ }, nil
+}
diff --git a/core/application/directory.go b/core/application/directory.go
new file mode 100755
index 0000000..7f9a5e9
--- /dev/null
+++ b/core/application/directory.go
@@ -0,0 +1,3 @@
+package application
+
+// Directory module - no business logic needed, all functionality moved to WebServer handlers
diff --git a/core/application/documents.go b/core/application/documents.go
new file mode 100644
index 0000000..1bdb2b7
--- /dev/null
+++ b/core/application/documents.go
@@ -0,0 +1,171 @@
+package application
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
+ "github.com/google/uuid"
+)
+
+// DocumentConfig defines entity-specific document configuration
+type DocumentConfig struct {
+ // Storage prefix for this entity type
+ StoragePrefix string
+
+ // Namespace for account validation (empty if no validation needed)
+ AccountNamespace string
+
+ // Whether to validate against MobilityAccounts service
+ RequiresAccountValidation bool
+
+ // Custom validator function (optional)
+ CustomValidator func(ctx context.Context, entityID string) error
+}
+
+// Pre-configured document configs for each entity type
+var (
+ BeneficiaryDocumentConfig = DocumentConfig{
+ StoragePrefix: filestorage.PREFIX_BENEFICIARIES,
+ AccountNamespace: "parcoursmob_beneficiaries",
+ RequiresAccountValidation: true,
+ }
+
+ SolidarityDriverDocumentConfig = DocumentConfig{
+ StoragePrefix: filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS,
+ AccountNamespace: "solidarity_drivers",
+ RequiresAccountValidation: true,
+ }
+
+ OrganizedCarpoolDriverDocumentConfig = DocumentConfig{
+ StoragePrefix: filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS,
+ AccountNamespace: "organized_carpool_drivers",
+ RequiresAccountValidation: true,
+ }
+
+ BookingDocumentConfig = DocumentConfig{
+ StoragePrefix: filestorage.PREFIX_BOOKINGS,
+ RequiresAccountValidation: false,
+ }
+)
+
+// AddDocument adds a document for any entity with validation
+func (h *ApplicationHandler) AddDocument(
+ ctx context.Context,
+ config DocumentConfig,
+ entityID string,
+ file io.Reader,
+ filename string,
+ fileSize int64,
+ documentType string,
+ documentName string,
+) error {
+ // Perform validation if required
+ if config.RequiresAccountValidation {
+ if err := h.validateAccountForDocument(ctx, entityID, config.AccountNamespace); err != nil {
+ return err
+ }
+ }
+
+ // Custom validation if provided
+ if config.CustomValidator != nil {
+ if err := config.CustomValidator(ctx, entityID); err != nil {
+ return err
+ }
+ }
+
+ // Generate unique file ID
+ fileid := uuid.NewString()
+
+ // Prepare metadata
+ metadata := map[string]string{
+ "type": documentType,
+ "name": documentName,
+ }
+
+ // Construct file path
+ filepath := fmt.Sprintf("%s/%s_%s", entityID, fileid, filename)
+
+ // Store file
+ return h.filestorage.Put(file, config.StoragePrefix, filepath, fileSize, metadata)
+}
+
+// GetDocument retrieves a document for any entity with validation
+func (h *ApplicationHandler) GetDocument(
+ ctx context.Context,
+ config DocumentConfig,
+ entityID string,
+ document string,
+) (io.Reader, *filestorage.FileInfo, error) {
+ // Perform validation if required
+ if config.RequiresAccountValidation {
+ if err := h.validateAccountForDocument(ctx, entityID, config.AccountNamespace); err != nil {
+ return nil, nil, err
+ }
+ }
+
+ // Custom validation if provided
+ if config.CustomValidator != nil {
+ if err := config.CustomValidator(ctx, entityID); err != nil {
+ return nil, nil, err
+ }
+ }
+
+ // Retrieve file
+ filepath := fmt.Sprintf("%s/%s", entityID, document)
+ return h.filestorage.Get(config.StoragePrefix, filepath)
+}
+
+// ListDocuments retrieves all documents for an entity
+func (h *ApplicationHandler) ListDocuments(
+ config DocumentConfig,
+ entityID string,
+) []filestorage.FileInfo {
+ prefix := fmt.Sprintf("%s/%s", config.StoragePrefix, entityID)
+ return h.filestorage.List(prefix)
+}
+
+// DeleteDocument deletes a document for any entity with validation
+func (h *ApplicationHandler) DeleteDocument(
+ ctx context.Context,
+ config DocumentConfig,
+ entityID string,
+ document string,
+) error {
+ // Perform validation if required
+ if config.RequiresAccountValidation {
+ if err := h.validateAccountForDocument(ctx, entityID, config.AccountNamespace); err != nil {
+ return err
+ }
+ }
+
+ // Custom validation if provided
+ if config.CustomValidator != nil {
+ if err := config.CustomValidator(ctx, entityID); err != nil {
+ return err
+ }
+ }
+
+ // Delete file
+ filepath := fmt.Sprintf("%s/%s", entityID, document)
+ return h.filestorage.Delete(config.StoragePrefix, filepath)
+}
+
+// validateAccountForDocument validates entity against MobilityAccounts service
+func (h *ApplicationHandler) validateAccountForDocument(ctx context.Context, accountID string, expectedNamespace string) error {
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, &mobilityaccounts.GetAccountRequest{
+ Id: accountID,
+ })
+ if err != nil {
+ return err
+ }
+
+ if resp.Account.Namespace != expectedNamespace {
+ return fmt.Errorf("account %s is not of type %s (namespace: %s)",
+ accountID, expectedNamespace, resp.Account.Namespace)
+ }
+
+ return nil
+}
diff --git a/core/application/exports.go b/core/application/exports.go
new file mode 100644
index 0000000..6473f91
--- /dev/null
+++ b/core/application/exports.go
@@ -0,0 +1,429 @@
+package application
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strconv"
+
+ "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"
+ groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
+ accountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "github.com/xuri/excelize/v2"
+)
+
+type FlatMaps []map[string]any
+
+func (maps FlatMaps) GetHeaders() (res []string) {
+ keys := map[string]bool{}
+ for _, m := range maps {
+ for k, _ := range m {
+ if _, ok := keys[k]; !ok {
+ keys[k] = true
+ res = append(res, k)
+ }
+ }
+ }
+ sort.Strings(res)
+ return
+}
+
+func (maps FlatMaps) GetValues() (res [][]string) {
+ headers := maps.GetHeaders()
+ for _, m := range maps {
+ line := []string{}
+ for _, k := range headers {
+ if v, ok := m[k]; ok && v != nil {
+ line = append(line, fmt.Sprint(v))
+ } else {
+ line = append(line, "")
+ }
+ }
+ res = append(res, line)
+ }
+ return
+}
+
+type ExportCacheResult struct {
+ Headers []string
+ Values [][]string
+}
+
+func (h *ApplicationHandler) ExportCacheAsCSV(cacheID string) (*ExportCacheResult, error) {
+ d, err := h.cache.Get(cacheID)
+ if err != nil {
+ return nil, err
+ }
+
+ var data []any
+ if dataSlice, ok := d.([]any); ok {
+ data = dataSlice
+ } else {
+ // Convert single item to slice
+ jsonData, err := json.Marshal(d)
+ if err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(jsonData, &data); err != nil {
+ return nil, err
+ }
+ }
+
+ flatmaps := FlatMaps{}
+ for _, v := range data {
+ if vMap, ok := v.(map[string]any); ok {
+ fm := map[string]any{}
+ flatten("", vMap, fm)
+ flatmaps = append(flatmaps, fm)
+ }
+ }
+
+ return &ExportCacheResult{
+ Headers: flatmaps.GetHeaders(),
+ Values: flatmaps.GetValues(),
+ }, nil
+}
+
+func flatten(prefix string, src map[string]any, dest map[string]any) {
+ if len(prefix) > 0 {
+ prefix += "."
+ }
+ for k, v := range src {
+ switch child := v.(type) {
+ case map[string]any:
+ flatten(prefix+k, child, dest)
+ case []any:
+ for i := 0; i < len(child); i++ {
+ dest[prefix+k+"."+strconv.Itoa(i)] = child[i]
+ }
+ default:
+ dest[prefix+k] = v
+ }
+ }
+}
+
+type AgendaExportResult struct {
+ ExcelFile *excelize.File
+}
+
+func (h *ApplicationHandler) ExportAllAgendaEvents() (*AgendaExportResult, error) {
+ resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
+ Namespaces: []string{"parcoursmob_dispositifs"},
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ events := []agendastorage.Event{}
+ groupids := []string{}
+ beneficiaries_ids := []string{}
+
+ for _, e := range resp.Events {
+ groupids = append(groupids, e.Owners...)
+ events = append(events, e.ToStorageType())
+
+ for _, subscriptions := range e.Subscriptions {
+ beneficiaries_ids = append(beneficiaries_ids, subscriptions.Subscriber)
+ }
+ }
+
+ sort.Sort(sorting.EventsByStartdate(events))
+
+ groups, beneficiaries_map, err := h.getAgendaMetadata(groupids, beneficiaries_ids)
+ if err != nil {
+ return nil, err
+ }
+
+ file := h.generateAgendaExcel(events, groups, beneficiaries_map)
+ return &AgendaExportResult{ExcelFile: file}, nil
+}
+
+func (h *ApplicationHandler) ExportSingleAgendaEvent(eventID string) (*AgendaExportResult, error) {
+ resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), &agenda.GetEventRequest{
+ Id: eventID,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ groupids := []string{}
+ beneficiaries_ids := []string{}
+ groupids = append(groupids, resp.Event.Owners...)
+
+ for _, subscriptions := range resp.Event.Subscriptions {
+ beneficiaries_ids = append(beneficiaries_ids, subscriptions.Subscriber)
+ }
+
+ groups, beneficiaries_map, err := h.getAgendaMetadata(groupids, beneficiaries_ids)
+ if err != nil {
+ return nil, err
+ }
+
+ events := []agendastorage.Event{resp.Event.ToStorageType()}
+ file := h.generateAgendaExcel(events, groups, beneficiaries_map)
+ return &AgendaExportResult{ExcelFile: file}, nil
+}
+
+func (h *ApplicationHandler) getAgendaMetadata(groupids, beneficiaries_ids []string) (map[string]groupsstorage.Group, map[string]accountsstorage.Account, error) {
+ groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
+ Groupids: groupids,
+ })
+
+ groups := map[string]groupsstorage.Group{}
+ if err == nil {
+ for _, g := range groupsresp.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, nil, err
+ }
+
+ beneficiaries_map := map[string]accountsstorage.Account{}
+ for _, ben := range beneficiaries.Accounts {
+ beneficiaries_map[ben.Id] = ben.ToStorageType()
+ }
+
+ return groups, beneficiaries_map, nil
+}
+
+func (h *ApplicationHandler) generateAgendaExcel(events []agendastorage.Event, groups map[string]groupsstorage.Group, beneficiaries_map map[string]accountsstorage.Account) *excelize.File {
+ f := excelize.NewFile()
+
+ f.SetCellValue("Sheet1", "A1", "Evénement")
+ f.SetCellValue("Sheet1", "B1", "Date de début")
+ f.SetCellValue("Sheet1", "C1", "Date de fin")
+ f.SetCellValue("Sheet1", "D1", "Nom bénéficiaire")
+ f.SetCellValue("Sheet1", "E1", "Prenom bénéficiaire")
+ f.SetCellValue("Sheet1", "F1", "Numéro allocataire / Pole emploi")
+ f.SetCellValue("Sheet1", "G1", "Prescipteur")
+ f.SetCellValue("Sheet1", "H1", "Prescipteur Nom")
+ f.SetCellValue("Sheet1", "I1", "Prescipteur Email")
+ f.SetCellValue("Sheet1", "J1", "Gestionnaire événement")
+
+ i := 2
+ for _, e := range events {
+ if len(e.Owners) == 0 {
+ continue
+ }
+ admin := groups[e.Owners[0]]
+
+ for _, s := range e.Subscriptions {
+ subscribedbygroup := ""
+ subscribedbyuser := ""
+ subscribedbyemail := ""
+ if v, ok := s.Data["subscribed_by"].(map[string]any); ok {
+ if v2, ok := v["group"].(map[string]any); ok {
+ if v3, ok := v2["name"].(string); ok {
+ subscribedbygroup = v3
+ }
+ }
+ if v4, ok := v["user"].(map[string]any); ok {
+ if v5, ok := v4["display_name"].(string); ok {
+ subscribedbyuser = v5
+ }
+ if v6, ok := v4["email"].(string); ok {
+ subscribedbyemail = v6
+ }
+ }
+ }
+
+ beneficiary := beneficiaries_map[s.Subscriber]
+
+ f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i), e.Name)
+ f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i), e.Startdate.Format("2006-01-02"))
+ f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i), e.Enddate.Format("2006-01-02"))
+ f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i), beneficiary.Data["last_name"])
+ f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i), beneficiary.Data["first_name"])
+ f.SetCellValue("Sheet1", fmt.Sprintf("F%d", i), beneficiary.Data["file_number"])
+ f.SetCellValue("Sheet1", fmt.Sprintf("G%d", i), subscribedbygroup)
+ f.SetCellValue("Sheet1", fmt.Sprintf("H%d", i), subscribedbyuser)
+ f.SetCellValue("Sheet1", fmt.Sprintf("I%d", i), subscribedbyemail)
+ f.SetCellValue("Sheet1", fmt.Sprintf("J%d", i), admin.Data["name"])
+ i = i + 1
+ }
+ }
+ return f
+}
+
+type FleetBookingsExportResult struct {
+ ExcelFile *excelize.File
+}
+
+func (h *ApplicationHandler) ExportAllFleetBookings() (*FleetBookingsExportResult, error) {
+ vehicles, bookings, err := h.getFleetData()
+ if err != nil {
+ return nil, err
+ }
+
+ groups, beneficiaries_map, err := h.getFleetMetadata(bookings)
+ if err != nil {
+ return nil, err
+ }
+
+ file := h.generateFleetExcel(bookings, vehicles, groups, beneficiaries_map, "")
+ return &FleetBookingsExportResult{ExcelFile: file}, nil
+}
+
+func (h *ApplicationHandler) ExportFleetBookingsByGroup(groupID string) (*FleetBookingsExportResult, error) {
+ vehicles, bookings, err := h.getFleetData()
+ if err != nil {
+ return nil, err
+ }
+
+ groups, beneficiaries_map, err := h.getFleetMetadata(bookings)
+ if err != nil {
+ return nil, err
+ }
+
+ file := h.generateFleetExcel(bookings, vehicles, groups, beneficiaries_map, groupID)
+ return &FleetBookingsExportResult{ExcelFile: file}, nil
+}
+
+func (h *ApplicationHandler) getFleetData() (map[string]fleetsstorage.Vehicle, []fleetsstorage.Booking, error) {
+ vehicles := map[string]fleetsstorage.Vehicle{}
+ bookings := []fleetsstorage.Booking{}
+
+ request := &fleets.GetVehiclesRequest{
+ Namespaces: []string{"parcoursmob"},
+ }
+ resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ for _, vehicle := range resp.Vehicles {
+ v := vehicle.ToStorageType()
+ for _, b := range v.Bookings {
+ bookings = append(bookings, b)
+ }
+ vehicles[vehicle.Id] = v
+ }
+
+ return vehicles, bookings, nil
+}
+
+func (h *ApplicationHandler) getFleetMetadata(bookings []fleetsstorage.Booking) (map[string]groupsstorage.Group, map[string]accountsstorage.Account, error) {
+ beneficiaries_ids := []string{}
+ for _, b := range bookings {
+ beneficiaries_ids = append(beneficiaries_ids, b.Driver)
+ }
+
+ groups := map[string]groupsstorage.Group{}
+ admingroups, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), &groupsmanagement.GetGroupsRequest{
+ Namespaces: []string{"parcoursmob_organizations"},
+ })
+ if err != nil {
+ return nil, 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, nil, err
+ }
+
+ beneficiaries_map := map[string]accountsstorage.Account{}
+ for _, ben := range beneficiaries.Accounts {
+ beneficiaries_map[ben.Id] = ben.ToStorageType()
+ }
+
+ return groups, beneficiaries_map, nil
+}
+
+func (h *ApplicationHandler) generateFleetExcel(bookings []fleetsstorage.Booking, vehicles map[string]fleetsstorage.Vehicle, groups map[string]groupsstorage.Group, beneficiaries_map map[string]accountsstorage.Account, filterGroupID string) *excelize.File {
+ f := excelize.NewFile()
+
+ f.SetCellValue("Sheet1", "A1", "Numéro")
+ f.SetCellValue("Sheet1", "B1", "Type")
+ f.SetCellValue("Sheet1", "C1", "Gestionnaire")
+ f.SetCellValue("Sheet1", "D1", "Prescripteur")
+ f.SetCellValue("Sheet1", "E1", "Bénéficiaire")
+ f.SetCellValue("Sheet1", "F1", "Numéro allocataire / Pole emploi")
+ f.SetCellValue("Sheet1", "G1", "Début de Mise à disposition")
+ f.SetCellValue("Sheet1", "H1", "Fin de mise à disposition")
+ f.SetCellValue("Sheet1", "I1", "Début indisponibilité")
+ f.SetCellValue("Sheet1", "J1", "Fin indisponibilité")
+ f.SetCellValue("Sheet1", "K1", "Véhicule retiré")
+ f.SetCellValue("Sheet1", "L1", "Commentaire - Retrait véhicule")
+ f.SetCellValue("Sheet1", "M1", "Réservation supprimée")
+ f.SetCellValue("Sheet1", "N1", "Motif de la suppression")
+
+ i := 2
+ for _, b := range bookings {
+ vehicle := vehicles[b.Vehicleid]
+ if len(vehicle.Administrators) == 0 {
+ continue
+ }
+ admin := groups[vehicle.Administrators[0]]
+
+ bookedby := ""
+ if v, ok := b.Data["booked_by"].(map[string]any); ok {
+ if v2, ok := v["user"].(map[string]any); ok {
+ if v3, ok := v2["display_name"].(string); ok {
+ bookedby = v3
+ }
+ }
+ }
+
+ bookedbygroup := ""
+ if v4, ok := b.Data["booked_by"].(map[string]any); ok {
+ if v5, ok := v4["group"].(map[string]any); ok {
+ if v6, ok := v5["id"].(string); ok {
+ bookedbygroup = v6
+ }
+ }
+ }
+
+ // Filter by group if specified
+ if filterGroupID != "" && bookedbygroup != filterGroupID {
+ continue
+ }
+
+ beneficiary := beneficiaries_map[b.Driver]
+ adminunavailability := false
+
+ if av, ok := b.Data["administrator_unavailability"].(bool); ok && av {
+ adminunavailability = true
+ }
+
+ deleted := ""
+ if b.Deleted {
+ deleted = "DELETED"
+ }
+
+ f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i), vehicle.Data["licence_plate"])
+ f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i), vehicle.Type)
+ f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i), admin.Data["name"])
+ f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i), bookedby)
+ f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i), fmt.Sprintf("%v %v", beneficiary.Data["first_name"], beneficiary.Data["last_name"]))
+ f.SetCellValue("Sheet1", fmt.Sprintf("F%d", i), beneficiary.Data["file_number"])
+ f.SetCellValue("Sheet1", fmt.Sprintf("G%d", i), b.Startdate.Format("2006-01-02"))
+ f.SetCellValue("Sheet1", fmt.Sprintf("H%d", i), b.Enddate.Format("2006-01-02"))
+ f.SetCellValue("Sheet1", fmt.Sprintf("I%d", i), b.Unavailablefrom.Format("2006-01-02"))
+ f.SetCellValue("Sheet1", fmt.Sprintf("J%d", i), b.Unavailableto.Format("2006-01-02"))
+ f.SetCellValue("Sheet1", fmt.Sprintf("K%d", i), adminunavailability)
+ f.SetCellValue("Sheet1", fmt.Sprintf("L%d", i), b.Data["comment"])
+ f.SetCellValue("Sheet1", fmt.Sprintf("M%d", i), deleted)
+ f.SetCellValue("Sheet1", fmt.Sprintf("N%d", i), b.Data["motif"])
+ i = i + 1
+ }
+
+ return f
+}
\ No newline at end of file
diff --git a/core/application/group.go b/core/application/group.go
new file mode 100755
index 0000000..1593077
--- /dev/null
+++ b/core/application/group.go
@@ -0,0 +1,133 @@
+package application
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "time"
+
+ groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
+ "git.coopgo.io/coopgo-platform/groups-management/storage"
+ accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
+ "github.com/rs/zerolog/log"
+)
+
+type GroupSettingsResult struct {
+ Group storage.Group
+ GroupMembers []any
+ Admins []any
+}
+
+func (h *ApplicationHandler) GetGroupSettings(ctx context.Context, groupID string) (*GroupSettingsResult, error) {
+ // Get group info
+ groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{
+ Id: groupID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get group: %w", err)
+ }
+
+ group := groupResp.Group.ToStorageType()
+
+ members, err := h.members()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get members: %w", err)
+ }
+
+ admins := []any{}
+ groupMembers := []any{}
+
+ for _, m := range members {
+ mm := m.ToStorageType()
+ if groups, ok := mm.Data["groups"].([]any); ok {
+ for _, g := range groups {
+ if g.(string) == groupID {
+ groupMembers = append(groupMembers, mm)
+ }
+ if g.(string) == groupID+":admin" {
+ admins = append(admins, mm)
+ }
+ }
+ }
+ }
+
+ return &GroupSettingsResult{
+ Group: group,
+ GroupMembers: groupMembers,
+ Admins: admins,
+ }, nil
+}
+
+func (h *ApplicationHandler) InviteMemberToGroup(ctx context.Context, groupID string, 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, &accounts.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 := accounts.AccountFromStorageType(&account)
+ if err != nil {
+ return fmt.Errorf("failed to convert account: %w", err)
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &accounts.UpdateDataRequest{
+ Account: as,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to update account: %w", err)
+ }
+
+ data := map[string]any{
+ "group": group.Data["name"],
+ }
+
+ 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,
+ }
+
+ 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
+}
diff --git a/core/application/group_module.go b/core/application/group_module.go
new file mode 100755
index 0000000..0e22bb4
--- /dev/null
+++ b/core/application/group_module.go
@@ -0,0 +1,219 @@
+package application
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strings"
+ "time"
+
+ groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
+ groupstorage "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"
+)
+
+type GroupsResult struct {
+ Groups []groupstorage.Group
+}
+
+type CreateGroupModuleResult struct {
+ GroupID string
+}
+
+type GroupModuleCreateDataResult struct {
+ GroupTypes []string
+}
+
+type DisplayGroupModuleResult struct {
+ GroupID string
+ Accounts []any
+ CacheID string
+ Searched bool
+ Beneficiary any
+ Group groupstorage.Group
+ AccountsBeneficiaire []mobilityaccountsstorage.Account
+}
+
+var Addres any
+
+type BeneficiariesGroupForm struct {
+ FirstName string `json:"first_name" validate:"required"`
+ LastName string `json:"last_name" validate:"required"`
+ Email string `json:"email" validate:"required,email"`
+ Birthdate *time.Time `json:"birthdate"`
+ PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
+ Address any `json:"address,omitempty"`
+ Gender string `json:"gender"`
+}
+
+type GroupsModuleByName []groupstorage.Group
+
+func (a GroupsModuleByName) Len() int { return len(a) }
+func (a GroupsModuleByName) Less(i, j int) bool {
+ return strings.Compare(a[i].Data["name"].(string), a[j].Data["name"].(string)) < 0
+}
+func (a GroupsModuleByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+func (h *ApplicationHandler) GetGroups(ctx context.Context) (*GroupsResult, error) {
+ request := &groupsmanagement.GetGroupsRequest{
+ Namespaces: []string{"parcoursmob_groups"},
+ }
+
+ resp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get groups: %w", err)
+ }
+
+ var groups = []groupstorage.Group{}
+
+ for _, group := range resp.Groups {
+ g := group.ToStorageType()
+ groups = append(groups, g)
+ }
+
+ sort.Sort(GroupsModuleByName(groups))
+
+ return &GroupsResult{
+ Groups: groups,
+ }, nil
+}
+
+func (h *ApplicationHandler) GetGroupModuleCreateData(ctx context.Context) (*GroupModuleCreateDataResult, error) {
+ groupTypes := h.config.GetStringSlice("modules.groups.group_types")
+ return &GroupModuleCreateDataResult{
+ GroupTypes: groupTypes,
+ }, nil
+}
+
+func (h *ApplicationHandler) CreateGroupModule(ctx context.Context, name, groupType, description, address string) (*CreateGroupModuleResult, error) {
+ if name == "" {
+ return nil, fmt.Errorf("name is required")
+ }
+ if groupType == "" {
+ return nil, fmt.Errorf("type is required")
+ }
+
+ var addressData any
+ if address != "" {
+ if err := json.Unmarshal([]byte(address), &addressData); err != nil {
+ return nil, fmt.Errorf("failed to parse address: %w", err)
+ }
+ Addres = addressData
+ }
+
+ groupID := uuid.NewString()
+
+ dataMap := map[string]any{
+ "name": name,
+ "type": groupType,
+ "description": description,
+ "address": Addres,
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create data structure: %w", err)
+ }
+
+ request := &groupsmanagement.AddGroupRequest{
+ Group: &groupsmanagement.Group{
+ Id: groupID,
+ Namespace: "parcoursmob_groups",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ _, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to add group: %w", err)
+ }
+
+ return &CreateGroupModuleResult{
+ GroupID: groupID,
+ }, nil
+}
+
+func filterAccountBySearch(searchFilter string, a *mobilityaccounts.Account) bool {
+ if searchFilter != "" {
+ name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
+ if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter)) {
+ return false
+ }
+ }
+ return true
+}
+func (h *ApplicationHandler) DisplayGroupModule(ctx context.Context, groupID string, searchFilter string, currentUserGroup groupstorage.Group) (*DisplayGroupModuleResult, error) {
+ request := &groupsmanagement.GetGroupRequest{
+ Id: groupID,
+ }
+
+ resp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get group: %w", err)
+ }
+
+ var accounts = []any{}
+
+ accountsRequest := &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: resp.Group.Members,
+ }
+
+ accountsResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, accountsRequest)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to get accounts batch")
+ } else {
+ for _, account := range accountsResp.Accounts {
+ if filterAccountBySearch(searchFilter, account) {
+ a := account.ToStorageType()
+ accounts = append(accounts, a)
+ }
+ }
+ }
+
+ cacheID := uuid.NewString()
+ h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour)
+
+ // Get beneficiaries in current user's group
+ accountsBeneficiaire, err := h.services.GetBeneficiariesInGroup(currentUserGroup)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get beneficiaries in group: %w", err)
+ }
+
+ return &DisplayGroupModuleResult{
+ GroupID: resp.Group.ToStorageType().ID,
+ Accounts: accounts,
+ CacheID: cacheID,
+ Searched: false,
+ Beneficiary: nil,
+ Group: resp.Group.ToStorageType(),
+ AccountsBeneficiaire: accountsBeneficiaire,
+ }, nil
+}
+
+func (h *ApplicationHandler) SubscribeBeneficiaryToGroup(ctx context.Context, groupID string, beneficiaryID string) error {
+ beneficiaryRequest := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+
+ beneficiaryResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, beneficiaryRequest)
+ if err != nil {
+ return fmt.Errorf("failed to get beneficiary: %w", err)
+ }
+
+ subscribe := &groupsmanagement.SubscribeRequest{
+ Groupid: groupID,
+ Memberid: beneficiaryResp.Account.Id,
+ }
+
+ _, err = h.services.GRPC.GroupsManagement.Subscribe(ctx, subscribe)
+ if err != nil {
+ return fmt.Errorf("failed to subscribe beneficiary to group: %w", err)
+ }
+
+ return nil
+}
diff --git a/core/application/journeys.go b/core/application/journeys.go
new file mode 100755
index 0000000..cc99e7e
--- /dev/null
+++ b/core/application/journeys.go
@@ -0,0 +1,458 @@
+package application
+
+import (
+ "context"
+ "fmt"
+ "slices"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
+ carpoolproto "git.coopgo.io/coopgo-platform/carpool-service/servers/grpc/proto"
+ fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
+ fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "git.coopgo.io/coopgo-platform/multimodal-routing/libs/transit/transitous"
+ savedsearchtypes "git.coopgo.io/coopgo-platform/saved-search/data/types"
+ savedsearchproto "git.coopgo.io/coopgo-platform/saved-search/servers/grpc/proto/gen"
+ savedsearchtransformers "git.coopgo.io/coopgo-platform/saved-search/servers/grpc/transformers"
+ "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
+ "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type SearchJourneysResult struct {
+ CarpoolResults []*geojson.FeatureCollection
+ TransitResults []*transitous.Itinerary
+ VehicleResults []fleetsstorage.Vehicle
+ Searched bool
+ DriverJourneys []*gen.SolidarityTransportDriverJourney
+ Drivers map[string]mobilityaccountsstorage.Account
+ OrganizedCarpools []*carpoolproto.CarpoolServiceDriverJourney
+ KnowledgeBaseResults []any
+}
+
+// SearchJourneyOptions contains per-request options for journey search
+type SearchJourneyOptions struct {
+ DisableSolidarityTransport bool
+ DisableOrganizedCarpool bool
+ DisableCarpoolOperators bool
+ DisableTransit bool
+ DisableFleetVehicles bool
+ DisableKnowledgeBase bool
+}
+
+// SearchJourneys performs the business logic for journey search
+func (h *ApplicationHandler) SearchJourneys(
+ ctx context.Context,
+ departureDateTime time.Time,
+ departureGeo *geojson.Feature,
+ destinationGeo *geojson.Feature,
+ passengerID string,
+ solidarityTransportExcludeDriver string,
+ solidarityExcludeGroupId string,
+ options *SearchJourneyOptions,
+) (*SearchJourneysResult, error) {
+ var (
+ // Results
+ transitResults []*transitous.Itinerary
+ carpoolResults []*geojson.FeatureCollection
+ vehicleResults []fleetsstorage.Vehicle
+ solidarityTransportResults []*gen.SolidarityTransportDriverJourney
+ organizedCarpoolResults []*carpoolproto.CarpoolServiceDriverJourney
+ knowledgeBaseResults []any
+
+ drivers = map[string]mobilityaccountsstorage.Account{}
+ searched = false
+ )
+
+ // Only search if we have complete departure and destination info
+ if departureGeo != nil && destinationGeo != nil && !departureDateTime.IsZero() {
+ searched = true
+
+ // Default options if not provided
+ if options == nil {
+ options = &SearchJourneyOptions{}
+ }
+
+ // Check solution type configurations (global config AND per-request options)
+ solidarityTransportEnabled := h.config.GetBool("modules.journeys.solutions.solidarity_transport.enabled") && !options.DisableSolidarityTransport
+ organizedCarpoolEnabled := h.config.GetBool("modules.journeys.solutions.organized_carpool.enabled") && !options.DisableOrganizedCarpool
+ carpoolOperatorsEnabled := h.config.GetBool("modules.journeys.solutions.carpool_operators.enabled") && !options.DisableCarpoolOperators
+ transitEnabled := h.config.GetBool("modules.journeys.solutions.transit.enabled") && !options.DisableTransit
+ fleetVehiclesEnabled := h.config.GetBool("modules.journeys.solutions.fleet_vehicles.enabled") && !options.DisableFleetVehicles
+ knowledgeBaseEnabled := h.config.GetBool("modules.journeys.solutions.knowledge_base.enabled") && !options.DisableKnowledgeBase
+
+ // SOLIDARITY TRANSPORT
+ var err error
+ drivers, err = h.services.GetAccountsInNamespacesMap([]string{"solidarity_drivers", "organized_carpool_drivers"})
+ if err != nil {
+ drivers = map[string]mobilityaccountsstorage.Account{}
+ }
+
+ protodep, _ := transformers.GeoJsonToProto(departureGeo)
+ protodest, _ := transformers.GeoJsonToProto(destinationGeo)
+
+ // Get driver IDs to exclude based on group_id (drivers who already have bookings in this group)
+ excludedDriverIds := make(map[string]bool)
+ if solidarityExcludeGroupId != "" {
+ bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{
+ StartDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)),
+ EndDate: timestamppb.New(time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)),
+ })
+ if err == nil {
+ for _, booking := range bookingsResp.Bookings {
+ if booking.GroupId == solidarityExcludeGroupId {
+ excludedDriverIds[booking.DriverId] = true
+ }
+ }
+ }
+ }
+
+ if solidarityTransportEnabled {
+ log.Debug().Time("departure time", departureDateTime).Msg("calling driver journeys with ...")
+
+ res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, &gen.GetDriverJourneysRequest{
+ Departure: protodep,
+ Arrival: protodest,
+ DepartureDate: timestamppb.New(departureDateTime),
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("error in grpc call to GetDriverJourneys")
+ } else {
+ solidarityTransportResults = slices.Collect(func(yield func(*gen.SolidarityTransportDriverJourney) bool) {
+ for _, dj := range res.DriverJourneys {
+ if a, ok := drivers[dj.DriverId].Data["archived"]; ok {
+ if archived, ok := a.(bool); ok {
+ if archived {
+ continue
+ }
+ }
+ }
+ if dj.DriverId == solidarityTransportExcludeDriver {
+ continue
+ }
+ // Skip drivers who already have bookings in the same group
+ if excludedDriverIds[dj.DriverId] {
+ continue
+ }
+ if !yield(dj) {
+ return
+ }
+ }
+ })
+ sort.Slice(solidarityTransportResults, func(i, j int) bool {
+ return solidarityTransportResults[i].DriverDistance < solidarityTransportResults[j].DriverDistance
+ })
+ }
+ }
+
+ // Get departure and destination addresses from properties
+ var departureAddress, destinationAddress string
+ if departureGeo.Properties != nil {
+ if label, ok := departureGeo.Properties["label"].(string); ok {
+ departureAddress = label
+ }
+ }
+ if destinationGeo.Properties != nil {
+ if label, ok := destinationGeo.Properties["label"].(string); ok {
+ destinationAddress = label
+ }
+ }
+
+ // ORGANIZED CARPOOL
+ if organizedCarpoolEnabled {
+ radius := float64(5)
+ organizedCarpoolResultsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(ctx, &carpoolproto.DriverJourneysRequest{
+ DepartureLat: departureGeo.Point().Lat(),
+ DepartureLng: departureGeo.Point().Lon(),
+ ArrivalLat: destinationGeo.Point().Lat(),
+ ArrivalLng: destinationGeo.Point().Lon(),
+ DepartureDate: timestamppb.New(departureDateTime),
+ DepartureAddress: &departureAddress,
+ ArrivalAddress: &destinationAddress,
+ DepartureRadius: &radius,
+ ArrivalRadius: &radius,
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving organized carpools")
+ } else {
+ organizedCarpoolResults = organizedCarpoolResultsRes.DriverJourneys
+ sort.Slice(organizedCarpoolResults, func(i, j int) bool {
+ return *organizedCarpoolResults[i].Distance < *organizedCarpoolResults[j].Distance
+ })
+ }
+ }
+
+ var wg sync.WaitGroup
+ // CARPOOL OPERATORS
+ if carpoolOperatorsEnabled {
+ carpools := make(chan *geojson.FeatureCollection)
+ go h.services.InteropCarpool.Search(carpools, *departureGeo, *destinationGeo, departureDateTime)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for c := range carpools {
+ carpoolResults = append(carpoolResults, c)
+ }
+ }()
+ }
+
+ // TRANSIT
+ if transitEnabled {
+ transitch := make(chan *transitous.Itinerary)
+ go func(transitch chan *transitous.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
+ defer close(transitch)
+ response, err := h.services.TransitRouting.PlanWithResponse(ctx, &transitous.PlanParams{
+ FromPlace: fmt.Sprintf("%f,%f", departure.Point().Lat(), departure.Point().Lon()),
+ ToPlace: fmt.Sprintf("%f,%f", destination.Point().Lat(), destination.Point().Lon()),
+ Time: datetime,
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving transit data from Transitous server")
+ return
+ }
+ for _, i := range response.Itineraries {
+ transitch <- &i
+ }
+ }(transitch, departureGeo, destinationGeo, &departureDateTime)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ paris, _ := time.LoadLocation("Europe/Paris")
+ requestedDay := departureDateTime.In(paris).Truncate(24 * time.Hour)
+
+ for itinerary := range transitch {
+ // Only include journeys that start on the requested day (in Paris timezone)
+ if !itinerary.StartTime.IsZero() && !itinerary.EndTime.IsZero() {
+ log.Info().
+ Time("startTime", itinerary.StartTime).
+ Time("endTime", itinerary.EndTime).
+ Str("startTimezone", itinerary.StartTime.Location().String()).
+ Str("endTimezone", itinerary.EndTime.Location().String()).
+ Str("startTimeRFC3339", itinerary.StartTime.Format(time.RFC3339)).
+ Str("endTimeRFC3339", itinerary.EndTime.Format(time.RFC3339)).
+ Msg("Journey search - received transit itinerary from Transitous")
+
+ startInParis := itinerary.StartTime.In(paris)
+ startDay := startInParis.Truncate(24 * time.Hour)
+
+ // Check if journey starts on the requested day
+ if startDay.Equal(requestedDay) {
+ transitResults = append(transitResults, itinerary)
+ } else {
+ log.Info().
+ Str("requestedDay", requestedDay.Format("2006-01-02")).
+ Str("startDay", startDay.Format("2006-01-02")).
+ Msg("Journey search - filtered out transit journey (not on requested day)")
+ }
+ }
+ }
+ }()
+ }
+
+ // VEHICLES
+ if fleetVehiclesEnabled {
+ vehiclech := make(chan fleetsstorage.Vehicle)
+ go h.vehicleRequest(vehiclech, departureDateTime.Add(-24*time.Hour), departureDateTime.Add(168*time.Hour))
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for vehicle := range vehiclech {
+ vehicleResults = append(vehicleResults, vehicle)
+ }
+ slices.SortFunc(vehicleResults, sorting.VehiclesByDistanceFrom(*departureGeo))
+ }()
+ }
+ wg.Wait()
+
+ // KNOWLEDGE BASE
+ if knowledgeBaseEnabled {
+ departureGeoSearch, _ := h.services.Geography.GeoSearch(departureGeo)
+ kbData := h.config.Get("knowledge_base")
+ if kb, ok := kbData.([]any); ok {
+ for _, sol := range kb {
+ if solution, ok := sol.(map[string]any); ok {
+ if g, ok := solution["geography"]; ok {
+ if geography, ok := g.([]any); ok {
+ for _, gg := range geography {
+ if geog, ok := gg.(map[string]any); ok {
+ if layer, ok := geog["layer"].(string); ok {
+ code := geog["code"]
+ geo, err := h.services.Geography.Find(layer, fmt.Sprintf("%v", code))
+ if err == nil {
+ geog["geography"] = geo
+ geog["name"] = geo.Properties.MustString("nom")
+ }
+ if strings.Compare(fmt.Sprintf("%v", code), departureGeoSearch[layer].Properties.MustString("code")) == 0 {
+ knowledgeBaseResults = append(knowledgeBaseResults, solution)
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return &SearchJourneysResult{
+ CarpoolResults: carpoolResults,
+ TransitResults: transitResults,
+ VehicleResults: vehicleResults,
+ Searched: searched,
+ DriverJourneys: solidarityTransportResults,
+ Drivers: drivers,
+ OrganizedCarpools: organizedCarpoolResults,
+ KnowledgeBaseResults: knowledgeBaseResults,
+ }, nil
+}
+
+func (h *ApplicationHandler) vehicleRequest(vehiclech chan fleetsstorage.Vehicle, start time.Time, end time.Time) {
+ defer close(vehiclech)
+ vehiclerequest := &fleets.GetVehiclesRequest{
+ Namespaces: []string{"parcoursmob"},
+ }
+ vehicleresp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), vehiclerequest)
+ if err != nil {
+ log.Error().Err(err).Msg("")
+ return
+ }
+ for _, vehicle := range vehicleresp.Vehicles {
+ v := vehicle.ToStorageType()
+ if v.Free(start, end) {
+ vehiclech <- v
+ }
+ }
+}
+
+// SaveSearch saves a group's search to the saved-search microservice
+func (h *ApplicationHandler) SaveSearch(
+ ctx context.Context,
+ groupID string,
+ departureDateTime time.Time,
+ departureGeo *geojson.Feature,
+ destinationGeo *geojson.Feature,
+ additionalData map[string]interface{},
+) error {
+ // Convert geojson.Feature to proto format
+ var protoDepart, protoDest *savedsearchproto.SavedSearchGeoJsonFeature
+
+ log.Debug().
+ Bool("departure_nil", departureGeo == nil).
+ Bool("destination_nil", destinationGeo == nil).
+ Msg("SaveSearch: checking geo features")
+
+ if departureGeo != nil {
+ departureBytes, err := departureGeo.MarshalJSON()
+ if err != nil {
+ return fmt.Errorf("error marshaling departure: %w", err)
+ }
+ protoDepart = &savedsearchproto.SavedSearchGeoJsonFeature{
+ Serialized: string(departureBytes),
+ }
+ log.Debug().Str("departure_json", string(departureBytes)).Msg("SaveSearch: departure converted")
+ }
+
+ if destinationGeo != nil {
+ destinationBytes, err := destinationGeo.MarshalJSON()
+ if err != nil {
+ return fmt.Errorf("error marshaling destination: %w", err)
+ }
+ protoDest = &savedsearchproto.SavedSearchGeoJsonFeature{
+ Serialized: string(destinationBytes),
+ }
+ log.Debug().Str("destination_json", string(destinationBytes)).Msg("SaveSearch: destination converted")
+ }
+
+ // Convert additional data to protobuf Struct
+ var protoData *structpb.Struct
+ if additionalData != nil && len(additionalData) > 0 {
+ var err error
+ protoData, err = structpb.NewStruct(additionalData)
+ if err != nil {
+ return fmt.Errorf("error converting additional data: %w", err)
+ }
+ }
+
+ // Handle zero time value
+ var protoDateTime *timestamppb.Timestamp
+ if !departureDateTime.IsZero() {
+ protoDateTime = timestamppb.New(departureDateTime)
+ }
+
+ // Call the saved-search service
+ _, err := h.services.GRPC.SavedSearch.CreateSavedSearch(ctx, &savedsearchproto.CreateSavedSearchRequest{
+ OwnerId: groupID,
+ Departure: protoDepart,
+ Destination: protoDest,
+ Datetime: protoDateTime,
+ Data: protoData,
+ })
+ if err != nil {
+ return fmt.Errorf("error calling saved-search service: %w", err)
+ }
+
+ log.Info().Str("group_id", groupID).Msg("search saved successfully")
+ return nil
+}
+
+// GetSavedSearchesByOwner retrieves saved searches for a group
+func (h *ApplicationHandler) GetSavedSearchesByOwner(
+ ctx context.Context,
+ groupID string,
+) ([]*savedsearchtypes.SavedSearch, error) {
+ // Call the saved-search service to get searches by owner
+ response, err := h.services.GRPC.SavedSearch.GetSavedSearchesByOwner(ctx, &savedsearchproto.GetSavedSearchesByOwnerRequest{
+ OwnerId: groupID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("error calling saved-search service: %w", err)
+ }
+
+ // Convert protobuf searches to domain types
+ var searches []*savedsearchtypes.SavedSearch
+ for _, protoSearch := range response.SavedSearches {
+ search, err := savedsearchtransformers.SavedSearchProtoToType(protoSearch)
+ if err != nil {
+ log.Error().Err(err).Str("search_id", protoSearch.Id).Msg("failed to convert saved search")
+ continue
+ }
+ searches = append(searches, search)
+ }
+
+ // Sort searches by datetime (earliest first)
+ sort.Slice(searches, func(i, j int) bool {
+ return searches[i].DateTime.Before(searches[j].DateTime)
+ })
+
+ return searches, nil
+}
+
+// DeleteSavedSearch deletes a saved search by ID for the specified owner
+func (h *ApplicationHandler) DeleteSavedSearch(
+ ctx context.Context,
+ searchID string,
+ ownerID string,
+) error {
+ // Call the saved-search service to delete the search
+ _, err := h.services.GRPC.SavedSearch.DeleteSavedSearch(ctx, &savedsearchproto.DeleteSavedSearchRequest{
+ Id: searchID,
+ OwnerId: ownerID, // For authorization - ensure only the owner can delete
+ })
+ if err != nil {
+ return fmt.Errorf("error calling saved-search service: %w", err)
+ }
+
+ log.Info().Str("search_id", searchID).Str("owner_id", ownerID).Msg("saved search deleted successfully")
+ return nil
+}
diff --git a/core/application/members.go b/core/application/members.go
new file mode 100755
index 0000000..f926282
--- /dev/null
+++ b/core/application/members.go
@@ -0,0 +1,247 @@
+package application
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
+ groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
+ mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "github.com/google/uuid"
+ "google.golang.org/protobuf/types/known/structpb"
+)
+
+type MembersResult struct {
+ Accounts []mobilityaccountsstorage.Account
+ CacheID string
+ GroupsNames []string
+}
+
+func (h *ApplicationHandler) GetMembers(ctx context.Context) (*MembersResult, error) {
+ accounts, err := h.services.GetAccounts()
+ if err != nil {
+ return nil, err
+ }
+
+ var groupsNames []string
+
+ for _, v := range accounts {
+ adminid := v.ID
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: adminid,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ var allIds []string
+ for _, v := range resp.Account.ToStorageType().Data["groups"].([]any) {
+ s := fmt.Sprintf("%v", v)
+ if !(strings.Contains(s, "admin")) {
+ allIds = append(allIds, s)
+ }
+ }
+
+ reques := &groupsmanagement.GetGroupsBatchRequest{
+ Groupids: allIds,
+ }
+
+ res, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, reques)
+ if err != nil {
+ return nil, err
+ }
+
+ g := ""
+ for _, group := range res.Groups {
+ g += fmt.Sprintf("%v", group.ToStorageType().Data["name"]) + " "
+ }
+ groupsNames = append(groupsNames, g)
+ }
+
+ cacheID := uuid.NewString()
+ h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour)
+
+ return &MembersResult{
+ Accounts: accounts,
+ CacheID: cacheID,
+ GroupsNames: groupsNames,
+ }, nil
+}
+
+type MemberDataResult struct {
+ Account mobilityaccountsstorage.Account
+ GroupsNames []string
+}
+
+func (h *ApplicationHandler) GetMemberData(ctx context.Context, memberID string) (*MemberDataResult, error) {
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: memberID,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Security check: ensure this is actually a member account
+ if resp.Account.Namespace != "parcoursmob" {
+ return nil, fmt.Errorf("account %s is not a member (namespace: %s)", memberID, resp.Account.Namespace)
+ }
+
+ var allIds []string
+ for _, v := range resp.Account.ToStorageType().Data["groups"].([]any) {
+ s := fmt.Sprintf("%v", v)
+ if !(strings.Contains(s, "admin")) {
+ allIds = append(allIds, s)
+ }
+ }
+
+ reques := &groupsmanagement.GetGroupsBatchRequest{
+ Groupids: allIds,
+ }
+
+ res, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, reques)
+ if err != nil {
+ return nil, err
+ }
+
+ var groupsNames []string
+ for _, group := range res.Groups {
+ g := fmt.Sprintf("%v", group.ToStorageType().Data["name"])
+ groupsNames = append(groupsNames, g)
+ }
+
+ return &MemberDataResult{
+ Account: resp.Account.ToStorageType(),
+ GroupsNames: groupsNames,
+ }, nil
+}
+
+type MemberResult struct {
+ Account mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) GetMember(ctx context.Context, memberID string) (*MemberResult, error) {
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: memberID,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Security check: ensure this is actually a member account
+ if resp.Account.Namespace != "parcoursmob" {
+ return nil, fmt.Errorf("account %s is not a member (namespace: %s)", memberID, resp.Account.Namespace)
+ }
+
+ return &MemberResult{
+ Account: resp.Account.ToStorageType(),
+ }, nil
+}
+
+func (h *ApplicationHandler) UpdateMember(ctx context.Context, memberID, firstName, lastName, email, phoneNumber, gender string) (string, error) {
+ // Security check: verify the account exists and is a member
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: memberID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return "", err
+ }
+ if getResp.Account.Namespace != "parcoursmob" {
+ return "", fmt.Errorf("account %s is not a member (namespace: %s)", memberID, getResp.Account.Namespace)
+ }
+
+ dataMap := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ "phone_number": phoneNumber,
+ "gender": gender,
+ }
+
+ // Validate the data
+ formData := UserForm{
+ FirstName: firstName,
+ LastName: lastName,
+ Email: email,
+ PhoneNumber: phoneNumber,
+ Gender: gender,
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return "", err
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: memberID,
+ Namespace: "parcoursmob",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.Account.Id, nil
+}
+
+type UserForm struct {
+ FirstName string `json:"first_name" validate:"required"`
+ LastName string `json:"last_name" validate:"required"`
+ Email string `json:"email" validate:"required,email"`
+ PhoneNumber string `json:"phone_number" `
+ Address any `json:"address,omitempty"`
+ Gender string `json:"gender"`
+}
+
+type RegisterUserResult struct {
+ UserID string
+}
+
+func (h *ApplicationHandler) RegisterUser(ctx context.Context, user mobilityaccountsstorage.Account) (*RegisterUserResult, error) {
+ account, err := mobilityaccounts.AccountFromStorageType(&user)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, &mobilityaccounts.RegisterRequest{
+ Account: account,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if g, ok := user.Metadata["import_in_group"]; ok {
+ if group, ok := g.(string); ok {
+ _, err = h.services.GRPC.GroupsManagement.Subscribe(ctx, &groupsmanagement.SubscribeRequest{
+ Groupid: group,
+ Memberid: resp.Account.Id,
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return &RegisterUserResult{
+ UserID: resp.Account.Id,
+ }, nil
+}
+
diff --git a/core/application/organized-carpool.go b/core/application/organized-carpool.go
new file mode 100644
index 0000000..b7ff56b
--- /dev/null
+++ b/core/application/organized-carpool.go
@@ -0,0 +1,1481 @@
+package application
+
+import (
+ "cmp"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "slices"
+ "strconv"
+ "strings"
+ "time"
+
+ formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "git.coopgo.io/coopgo-platform/carpool-service/servers/grpc/proto"
+ groupstorage "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"
+ "git.coopgo.io/coopgo-platform/payments/pricing"
+ "github.com/google/uuid"
+ "github.com/paulmach/orb"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type OrganizedCarpoolDriversForm struct {
+ FirstName string `json:"first_name" validate:"required"`
+ LastName string `json:"last_name" validate:"required"`
+ Email string `json:"email" validate:"required,email"`
+ Birthdate *time.Time `json:"birthdate" validate:"required"`
+ PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
+ FileNumber string `json:"file_number"`
+ Address any `json:"address,omitempty"`
+ AddressDestination any `json:"address_destination,omitempty"`
+ Gender string `json:"gender"`
+}
+
+type OrganizedCarpoolOverviewResult struct {
+ Accounts []mobilityaccountsstorage.Account
+ AccountsMap map[string]mobilityaccountsstorage.Account
+ BeneficiariesMap map[string]mobilityaccountsstorage.Account
+ Bookings []*proto.CarpoolServiceBooking
+ BookingsHistory []*proto.CarpoolServiceBooking
+}
+
+func (h *ApplicationHandler) getOrganizedCarpoolDrivers(ctx context.Context, searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) {
+ request := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: []string{"organized_carpool_drivers"},
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create iterator that filters and transforms accounts
+ filteredAccounts := func(yield func(mobilityaccountsstorage.Account) bool) {
+ for _, account := range resp.Accounts {
+ if h.filterOrganizedCarpoolDriver(account, searchFilter, archivedFilter) {
+ if !yield(account.ToStorageType()) {
+ return
+ }
+ }
+ }
+ }
+
+ return slices.Collect(filteredAccounts), nil
+}
+
+func (h *ApplicationHandler) filterOrganizedCarpoolDriver(a *mobilityaccounts.Account, searchFilter string, archivedFilter bool) bool {
+ // Search filter
+ if searchFilter != "" {
+ name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
+ if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter)) {
+ return false
+ }
+ }
+
+ // Archived filter
+ if archivedFilter {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return true
+ }
+ return false
+ } else {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return false
+ }
+ }
+
+ return true
+}
+
+// filterOrganizedCarpoolBookingsByGeography filters bookings by departure and destination geography
+func filterOrganizedCarpoolBookingsByGeography(bookings []*proto.CarpoolServiceBooking, departurePolygons, destinationPolygons []orb.Polygon) []*proto.CarpoolServiceBooking {
+ if len(departurePolygons) == 0 && len(destinationPolygons) == 0 {
+ return bookings
+ }
+
+ filtered := []*proto.CarpoolServiceBooking{}
+ for _, booking := range bookings {
+ includeBooking := true
+
+ // Check departure filter if provided
+ if len(departurePolygons) > 0 {
+ departureMatch := false
+ pickupPoint := orb.Point{booking.PassengerPickupLng, booking.PassengerPickupLat}
+ departureMatch = isPointInGeographies(pickupPoint, departurePolygons)
+ if !departureMatch {
+ includeBooking = false
+ }
+ }
+
+ // Check destination filter if provided
+ if len(destinationPolygons) > 0 && includeBooking {
+ destinationMatch := false
+ dropPoint := orb.Point{booking.PassengerDropLng, booking.PassengerDropLat}
+ destinationMatch = isPointInGeographies(dropPoint, destinationPolygons)
+ if !destinationMatch {
+ includeBooking = false
+ }
+ }
+
+ if includeBooking {
+ filtered = append(filtered, booking)
+ }
+ }
+
+ return filtered
+}
+
+// filterOrganizedCarpoolBookingsByPassengerAddressGeography filters bookings where passenger address is within geography
+func filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookings []*proto.CarpoolServiceBooking, beneficiariesMap map[string]mobilityaccountsstorage.Account, addressPolygons []orb.Polygon) []*proto.CarpoolServiceBooking {
+ if len(addressPolygons) == 0 {
+ return bookings
+ }
+
+ filtered := []*proto.CarpoolServiceBooking{}
+ for _, booking := range bookings {
+ passenger, ok := beneficiariesMap[booking.Passenger.Id]
+ if !ok {
+ continue
+ }
+
+ // Check if passenger has address - unmarshal as GeoJSON Feature
+ if pa, ok := passenger.Data["address"]; ok {
+ jsonpa, err := json.Marshal(pa)
+ if err == nil {
+ passGeojson, err := geojson.UnmarshalFeature(jsonpa)
+ if err == nil && passGeojson.Geometry != nil {
+ if point, ok := passGeojson.Geometry.(orb.Point); ok {
+ if isPointInGeographies(point, addressPolygons) {
+ filtered = append(filtered, booking)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return filtered
+}
+
+func (h *ApplicationHandler) GetOrganizedCarpoolOverview(ctx context.Context, status, driverID, startDate, endDate, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode, histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeoLayer, histDepartureGeoCode, histDestinationGeoLayer, histDestinationGeoCode, histPassengerAddressGeoLayer, histPassengerAddressGeoCode string, archivedFilter bool, driverAddressGeoLayer, driverAddressGeoCode string) (*OrganizedCarpoolOverviewResult, error) {
+ // Get ALL drivers for the accountsMap (used in bookings display)
+ allDrivers, err := h.getOrganizedCarpoolDrivers(ctx, "", false)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting all organized carpool drivers")
+ allDrivers = []mobilityaccountsstorage.Account{}
+ }
+
+ // Build accountsMap with ALL drivers (for bookings to reference)
+ accountsMap := map[string]mobilityaccountsstorage.Account{}
+ for _, a := range allDrivers {
+ accountsMap[a.ID] = a
+ }
+
+ // Get filtered drivers for the drivers tab display
+ accounts, err := h.getOrganizedCarpoolDrivers(ctx, "", archivedFilter)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting organized carpool drivers")
+ accounts = []mobilityaccountsstorage.Account{}
+ }
+
+ // Apply driver address geography filtering only to the drivers tab list
+ if driverAddressGeoLayer != "" && driverAddressGeoCode != "" {
+ driverAddressPolygons, err := h.loadGeographyPolygon(driverAddressGeoLayer, driverAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load driver address geography filter")
+ } else {
+ filtered := []mobilityaccountsstorage.Account{}
+ for _, account := range accounts {
+ // Check if driver has address - unmarshal as GeoJSON Feature
+ if addr, ok := account.Data["address"]; ok {
+ jsonAddr, err := json.Marshal(addr)
+ if err == nil {
+ addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
+ if err == nil && addrGeojson.Geometry != nil {
+ if point, ok := addrGeojson.Geometry.(orb.Point); ok {
+ if isPointInGeographies(point, driverAddressPolygons) {
+ filtered = append(filtered, account)
+ }
+ }
+ }
+ }
+ }
+ }
+ accounts = filtered
+ }
+ }
+
+ beneficiariesMap, err := h.services.GetBeneficiariesMap()
+ if err != nil {
+ beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
+ }
+
+ // Parse start date or use default
+ var startdate time.Time
+ if startDate != "" {
+ if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
+ startdate = parsed
+ } else {
+ startdate = time.Now()
+ }
+ } else {
+ startdate = time.Now()
+ }
+
+ // Parse end date or use default
+ 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
+ } else {
+ enddate = time.Now().Add(24 * 365 * time.Hour)
+ }
+ } else {
+ enddate = time.Now().Add(24 * 365 * time.Hour)
+ }
+
+ bookingsproto, err := h.services.GRPC.CarpoolService.GetCarpoolBookings(ctx, &proto.GetCarpoolBookingsRequest{
+ MinDate: timestamppb.New(startdate),
+ MaxDate: timestamppb.New(enddate),
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("issue retrieving bookings")
+ }
+
+ bookings := []*proto.CarpoolServiceBooking{}
+ if err == nil {
+ for _, b := range bookingsproto.Bookings {
+ // Apply driver filter if specified
+ if driverID != "" && b.Driver.Id != driverID {
+ continue
+ }
+ // Apply status filter if specified
+ if status != "" && b.Status.String() != status {
+ continue
+ }
+ bookings = append(bookings, b)
+ }
+ }
+
+ // Apply geography filtering for current bookings
+ var departurePolygons, destinationPolygons, passengerAddressPolygons []orb.Polygon
+ if departureGeoLayer != "" && departureGeoCode != "" {
+ departurePolygons, err = h.loadGeographyPolygon(departureGeoLayer, departureGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load departure geography filter")
+ }
+ }
+ if destinationGeoLayer != "" && destinationGeoCode != "" {
+ destinationPolygons, err = h.loadGeographyPolygon(destinationGeoLayer, destinationGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load destination geography filter")
+ }
+ }
+ if passengerAddressGeoLayer != "" && passengerAddressGeoCode != "" {
+ passengerAddressPolygons, err = h.loadGeographyPolygon(passengerAddressGeoLayer, passengerAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load passenger address geography filter")
+ }
+ }
+ bookings = filterOrganizedCarpoolBookingsByGeography(bookings, departurePolygons, destinationPolygons)
+ bookings = filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookings, beneficiariesMap, passengerAddressPolygons)
+
+ // Get bookings history with filters
+ // Parse history start date or use default (1 month ago)
+ var histStartdate time.Time
+ if histStartDate != "" {
+ if parsed, err := time.Parse("2006-01-02", histStartDate); err == nil {
+ histStartdate = parsed
+ } else {
+ histStartdate = time.Now().Add(-30 * 24 * time.Hour)
+ }
+ } else {
+ histStartdate = time.Now().Add(-30 * 24 * time.Hour)
+ }
+
+ // Parse history end date or use default (yesterday)
+ var histEnddate time.Time
+ if histEndDate != "" {
+ if parsed, err := time.Parse("2006-01-02", histEndDate); err == nil {
+ histEnddate = parsed.Add(24 * time.Hour) // End of day
+ } else {
+ histEnddate = time.Now().Add(-24 * time.Hour)
+ }
+ } else {
+ histEnddate = time.Now().Add(-24 * time.Hour)
+ }
+
+ historyBookingsproto, err := h.services.GRPC.CarpoolService.GetCarpoolBookings(ctx, &proto.GetCarpoolBookingsRequest{
+ MinDate: timestamppb.New(histStartdate),
+ MaxDate: timestamppb.New(histEnddate),
+ })
+
+ bookingsHistory := []*proto.CarpoolServiceBooking{}
+ if err == nil {
+ for _, b := range historyBookingsproto.Bookings {
+ // Apply driver filter if specified
+ if histDriverID != "" && b.Driver.Id != histDriverID {
+ continue
+ }
+ // Apply status filter if specified
+ if histStatus != "" && b.Status.String() != histStatus {
+ continue
+ }
+ bookingsHistory = append(bookingsHistory, b)
+ }
+ }
+
+ // Apply geography filtering for history bookings
+ var histDeparturePolygons, histDestinationPolygons, histPassengerAddressPolygons []orb.Polygon
+ if histDepartureGeoLayer != "" && histDepartureGeoCode != "" {
+ histDeparturePolygons, err = h.loadGeographyPolygon(histDepartureGeoLayer, histDepartureGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load history departure geography filter")
+ }
+ }
+ if histDestinationGeoLayer != "" && histDestinationGeoCode != "" {
+ histDestinationPolygons, err = h.loadGeographyPolygon(histDestinationGeoLayer, histDestinationGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load history destination geography filter")
+ }
+ }
+ if histPassengerAddressGeoLayer != "" && histPassengerAddressGeoCode != "" {
+ histPassengerAddressPolygons, err = h.loadGeographyPolygon(histPassengerAddressGeoLayer, histPassengerAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load history passenger address geography filter")
+ }
+ }
+ bookingsHistory = filterOrganizedCarpoolBookingsByGeography(bookingsHistory, histDeparturePolygons, histDestinationPolygons)
+ bookingsHistory = filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookingsHistory, beneficiariesMap, histPassengerAddressPolygons)
+
+ // Sort accounts by last name, then first name
+ slices.SortFunc(accounts, func(a, b mobilityaccountsstorage.Account) int {
+ lastNameA := strings.ToLower(a.Data["last_name"].(string))
+ lastNameB := strings.ToLower(b.Data["last_name"].(string))
+ if lastNameA != lastNameB {
+ return strings.Compare(lastNameA, lastNameB)
+ }
+ firstNameA := strings.ToLower(a.Data["first_name"].(string))
+ firstNameB := strings.ToLower(b.Data["first_name"].(string))
+ return strings.Compare(firstNameA, firstNameB)
+ })
+ slices.SortFunc(bookings, func(a, b *proto.CarpoolServiceBooking) int {
+ return cmp.Compare(a.PassengerPickupDate.AsTime().Unix(), b.PassengerPickupDate.AsTime().Unix())
+ })
+ // Sort history bookings by date (most recent first)
+ slices.SortFunc(bookingsHistory, func(a, b *proto.CarpoolServiceBooking) int {
+ return cmp.Compare(b.PassengerPickupDate.AsTime().Unix(), a.PassengerPickupDate.AsTime().Unix())
+ })
+
+ return &OrganizedCarpoolOverviewResult{
+ Accounts: accounts,
+ AccountsMap: accountsMap,
+ BeneficiariesMap: beneficiariesMap,
+ Bookings: bookings,
+ BookingsHistory: bookingsHistory,
+ }, nil
+}
+
+type OrganizedCarpoolBookingDataResult struct {
+ Booking *proto.CarpoolServiceBooking
+ Driver mobilityaccountsstorage.Account
+ Passenger mobilityaccountsstorage.Account
+ DriverDepartureAddress string
+ DriverArrivalAddress string
+}
+
+func (h *ApplicationHandler) GetOrganizedCarpoolBookingData(ctx context.Context, bookingID string) (*OrganizedCarpoolBookingDataResult, error) {
+ resp, err := h.services.GRPC.CarpoolService.GetBooking(ctx, &proto.GetCarpoolBookingRequest{
+ BookingId: bookingID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not get carpool booking: %w", err)
+ }
+
+ if resp.Booking == nil {
+ return nil, fmt.Errorf("carpool booking not found")
+ }
+
+ driver, err := h.services.GetAccount(resp.Booking.Driver.Id)
+ if err != nil {
+ return nil, fmt.Errorf("driver retrieval issue: %w", err)
+ }
+
+ passenger, err := h.services.GetAccount(resp.Booking.Passenger.Id)
+ if err != nil {
+ return nil, fmt.Errorf("passenger retrieval issue: %w", err)
+ }
+
+ // Extract driver departure and arrival addresses from DriverRoute GeoJSON
+ var driverDepartureAddress, driverArrivalAddress string
+ if resp.Booking.DriverRoute != nil && resp.Booking.DriverRoute.Serialized != "" {
+ fc, err := geojson.UnmarshalFeatureCollection([]byte(resp.Booking.DriverRoute.Serialized))
+ if err != nil {
+ log.Error().Err(err).Msg("could not unmarshal driver route geojson")
+ } else {
+ // Extract departure address (first feature)
+ if len(fc.Features) > 0 {
+ if addr, ok := fc.Features[0].Properties["label"]; ok {
+ if addrStr, ok := addr.(string); ok {
+ driverDepartureAddress = addrStr
+ }
+ }
+ }
+ // Extract arrival address (last feature)
+ if len(fc.Features) > 1 {
+ if addr, ok := fc.Features[1].Properties["label"]; ok {
+ if addrStr, ok := addr.(string); ok {
+ driverArrivalAddress = addrStr
+ }
+ }
+ }
+ }
+ }
+
+ return &OrganizedCarpoolBookingDataResult{
+ Booking: resp.Booking,
+ Driver: driver,
+ Passenger: passenger,
+ DriverDepartureAddress: driverDepartureAddress,
+ DriverArrivalAddress: driverArrivalAddress,
+ }, nil
+}
+
+func (h *ApplicationHandler) UpdateOrganizedCarpoolBookingStatus(ctx context.Context, bookingID, action string) error {
+ var status proto.CarpoolServiceBookingStatus
+ if action == "confirm" {
+ status = proto.CarpoolServiceBookingStatus_CONFIRMED
+ } else if action == "cancel" {
+ status = proto.CarpoolServiceBookingStatus_CANCELLED
+ } else if action == "waitconfirmation" {
+ status = proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION
+ } else {
+ return fmt.Errorf("unknown booking action: %s", action)
+ }
+
+ // Get booking details BEFORE updating to capture previous status
+ result, err := h.GetOrganizedCarpoolBookingData(ctx, bookingID)
+ if err != nil {
+ return err
+ }
+
+ booking := result.Booking
+ driver := result.Driver
+ passenger := result.Passenger
+ previousStatus := booking.Status
+
+ // Update booking status
+ _, err = h.services.GRPC.CarpoolService.UpdateBooking(ctx, &proto.UpdateCarpoolBookingRequest{
+ BookingId: bookingID,
+ Status: status,
+ })
+ if err != nil {
+ return fmt.Errorf("update carpool booking status issue: %w", err)
+ }
+
+ // Handle wallet operations based on status transitions
+ // Credit driver / debit passenger when previous status was not CONFIRMED and new status is CONFIRMED
+ if previousStatus != proto.CarpoolServiceBookingStatus_CONFIRMED && status == proto.CarpoolServiceBookingStatus_CONFIRMED {
+ if booking.Price != nil && booking.Price.Amount != nil {
+ if err := h.CreditWallet(ctx, passenger.ID, -1*(*booking.Price.Amount), "Covoiturage solidaire", "Débit covoiturage solidaire"); err != nil {
+ return fmt.Errorf("could not debit passenger wallet: %w", err)
+ }
+ }
+ if booking.DriverCompensationAmount != nil && *booking.DriverCompensationAmount > 0 {
+ if err := h.CreditWallet(ctx, driver.ID, *booking.DriverCompensationAmount, "Covoiturage solidaire", "Crédit covoiturage solidaire"); err != nil {
+ return fmt.Errorf("could not credit driver wallet: %w", err)
+ }
+ }
+ }
+
+ // Credit passenger / debit driver when previous status was CONFIRMED and new status is not CONFIRMED anymore
+ if previousStatus == proto.CarpoolServiceBookingStatus_CONFIRMED && status != proto.CarpoolServiceBookingStatus_CONFIRMED {
+ if booking.Price != nil && booking.Price.Amount != nil {
+ if err := h.CreditWallet(ctx, passenger.ID, *booking.Price.Amount, "Covoiturage solidaire", "Remboursement annulation covoiturage solidaire"); err != nil {
+ return fmt.Errorf("could not credit passenger wallet: %w", err)
+ }
+ }
+ if booking.DriverCompensationAmount != nil && *booking.DriverCompensationAmount > 0 {
+ if err := h.CreditWallet(ctx, driver.ID, -1*(*booking.DriverCompensationAmount), "Covoiturage solidaire", "Débit annulation covoiturage solidaire"); err != nil {
+ return fmt.Errorf("could not debit driver wallet: %w", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (h *ApplicationHandler) CreateOrganizedCarpoolDriver(ctx context.Context, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address, addressDestination any, gender string) (string, error) {
+ dataMap := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ "phone_number": phoneNumber,
+ "file_number": fileNumber,
+ "gender": gender,
+ }
+
+ // Convert birthdate to string format for structpb compatibility
+ if birthdate != nil {
+ dataMap["birthdate"] = birthdate.Format("2006-01-02")
+ }
+
+ if address != nil {
+ dataMap["address"] = address
+ }
+ if addressDestination != nil {
+ dataMap["address_destination"] = addressDestination
+ }
+
+ // Validate the data
+ formData := OrganizedCarpoolDriversForm{
+ FirstName: firstName,
+ LastName: lastName,
+ Email: email,
+ Birthdate: birthdate,
+ PhoneNumber: phoneNumber,
+ FileNumber: fileNumber,
+ Address: address,
+ AddressDestination: addressDestination,
+ Gender: gender,
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return "", err
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ request := &mobilityaccounts.RegisterRequest{
+ Account: &mobilityaccounts.Account{
+ Namespace: "organized_carpool_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.Account.Id, nil
+}
+
+type OrganizedCarpoolDriverDataResult struct {
+ Driver mobilityaccountsstorage.Account
+ Trips []*geojson.FeatureCollection
+ Documents []filestorage.FileInfo
+ Bookings any
+ BeneficiariesMap map[string]mobilityaccountsstorage.Account
+ Stats map[string]any
+ WalletBalance float64
+}
+
+func (h *ApplicationHandler) GetOrganizedCarpoolDriverData(ctx context.Context, driverID string) (*OrganizedCarpoolDriverDataResult, error) {
+ documents := h.filestorage.List(filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS + "/" + driverID)
+
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return nil, fmt.Errorf("issue retrieving driver account: %w", err)
+ }
+
+ // Security check: ensure this is actually an organized carpool driver account
+ if driver.Namespace != "organized_carpool_drivers" {
+ return nil, fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace)
+ }
+
+ trips := []*geojson.FeatureCollection{}
+ resp, err := h.services.GRPC.CarpoolService.GetRegularRoutes(ctx, &proto.GetRegularRoutesRequest{
+ UserId: driverID,
+ })
+
+ for _, r := range resp.Routes {
+ t, err := geojson.UnmarshalFeatureCollection([]byte(r.Serialized))
+ if err != nil {
+ log.Error().Err(err).Msg("could not unmarshall feature collection")
+ continue
+ }
+ trips = append(trips, t)
+ }
+
+ // Get driver bookings
+ bookingsRequest := &proto.GetUserBookingsRequest{
+ UserId: driverID,
+ MinDate: timestamppb.New(time.Now().Add(-365 * 24 * time.Hour)),
+ MaxDate: timestamppb.New(time.Now().Add(365 * 24 * time.Hour)),
+ }
+
+ bookingsResp, err := h.services.GRPC.CarpoolService.GetUserBookings(ctx, bookingsRequest)
+ bookings := []*proto.CarpoolServiceBooking{}
+ if err == nil {
+ bookings = bookingsResp.Bookings
+ }
+
+ // Collect unique passenger IDs
+ passengerIDs := []string{}
+ passengerIDsMap := make(map[string]bool)
+ for _, booking := range bookings {
+ if booking.Passenger != nil && booking.Passenger.Id != "" {
+ if !passengerIDsMap[booking.Passenger.Id] {
+ passengerIDs = append(passengerIDs, booking.Passenger.Id)
+ passengerIDsMap[booking.Passenger.Id] = true
+ }
+ }
+ }
+
+ // Get beneficiaries in batch
+ beneficiariesMap := make(map[string]mobilityaccountsstorage.Account)
+ if len(passengerIDs) > 0 {
+ beneficiariesResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: passengerIDs,
+ })
+ if err == nil {
+ for _, account := range beneficiariesResp.Accounts {
+ a := account.ToStorageType()
+ beneficiariesMap[a.ID] = a
+ }
+ }
+ }
+
+ // Calculate stats
+ confirmedCount := 0
+ kmnb := 0
+ for _, booking := range bookings {
+ if booking.Status.String() == "CONFIRMED" {
+ confirmedCount++
+ if booking.Distance != nil {
+ kmnb += int(*booking.Distance)
+ }
+ }
+ }
+ stats := map[string]any{
+ "bookings": map[string]any{
+ "count": len(bookings),
+ "confirmed": confirmedCount,
+ "km": kmnb,
+ },
+ }
+
+ // Calculate wallet balance
+ walletBalance := h.calculateWalletBalance(driver)
+
+ return &OrganizedCarpoolDriverDataResult{
+ Driver: driver,
+ Trips: trips,
+ Documents: documents,
+ Bookings: bookings,
+ BeneficiariesMap: beneficiariesMap,
+ Stats: stats,
+ WalletBalance: walletBalance,
+ }, nil
+}
+
+type OrganizedCarpoolDriverResult struct {
+ Driver mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) GetOrganizedCarpoolDriver(ctx context.Context, driverID string) (*OrganizedCarpoolDriverResult, error) {
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return nil, fmt.Errorf("issue retrieving driver account: %w", err)
+ }
+
+ // Security check: ensure this is actually an organized carpool driver account
+ if driver.Namespace != "organized_carpool_drivers" {
+ return nil, fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace)
+ }
+
+ return &OrganizedCarpoolDriverResult{
+ Driver: driver,
+ }, nil
+}
+
+func (h *ApplicationHandler) UpdateOrganizedCarpoolDriver(ctx context.Context, driverID, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address, addressDestination any, gender, otherProperties string) (string, error) {
+ // Security check: verify the account exists and is an organized carpool driver
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return "", fmt.Errorf("issue retrieving driver account: %w", err)
+ }
+ if driver.Namespace != "organized_carpool_drivers" {
+ return "", fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace)
+ }
+
+ dataMap := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ "phone_number": phoneNumber,
+ "file_number": fileNumber,
+ "gender": gender,
+ }
+
+ // Convert birthdate to string format for structpb compatibility
+ if birthdate != nil {
+ dataMap["birthdate"] = birthdate.Format("2006-01-02")
+ }
+
+ if address != nil {
+ dataMap["address"] = address
+ }
+ if addressDestination != nil {
+ dataMap["address_destination"] = addressDestination
+ }
+
+ // Handle other_properties for update form
+ if otherProperties != "" {
+ var otherProps map[string]any
+ if err := json.Unmarshal([]byte(otherProperties), &otherProps); err == nil {
+ if dataMap["other_properties"] == nil {
+ dataMap["other_properties"] = make(map[string]any)
+ }
+ for k, v := range otherProps {
+ dataMap["other_properties"].(map[string]any)[k] = v
+ }
+ }
+ }
+
+ // Validate the data
+ formData := OrganizedCarpoolDriversForm{
+ FirstName: firstName,
+ LastName: lastName,
+ Email: email,
+ Birthdate: birthdate,
+ PhoneNumber: phoneNumber,
+ FileNumber: fileNumber,
+ Address: address,
+ AddressDestination: addressDestination,
+ Gender: gender,
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return "", err
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: driverID,
+ Namespace: "organized_carpool_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.Account.Id, nil
+}
+
+func (h *ApplicationHandler) ArchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error {
+ // Security check: verify the account exists and is an organized carpool driver
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return fmt.Errorf("issue retrieving driver account: %w", err)
+ }
+ if driver.Namespace != "organized_carpool_drivers" {
+ return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace)
+ }
+
+ data, err := structpb.NewValue(map[string]any{
+ "archived": true,
+ })
+ if err != nil {
+ return err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: driverID,
+ Namespace: "organized_carpool_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ return err
+}
+
+func (h *ApplicationHandler) UnarchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error {
+ // Security check: verify the account exists and is an organized carpool driver
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return fmt.Errorf("issue retrieving driver account: %w", err)
+ }
+ if driver.Namespace != "organized_carpool_drivers" {
+ return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace)
+ }
+
+ data, err := structpb.NewValue(map[string]any{
+ "archived": false,
+ })
+ if err != nil {
+ return err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: driverID,
+ Namespace: "organized_carpool_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ return err
+}
+
+func (h *ApplicationHandler) AddOrganizedCarpoolDriverDocument(ctx context.Context, driverID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error {
+ // Security check: verify the account exists and is an organized carpool driver
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return fmt.Errorf("issue retrieving driver account: %w", err)
+ }
+ if driver.Namespace != "organized_carpool_drivers" {
+ return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace)
+ }
+
+ fileid := uuid.NewString()
+
+ metadata := map[string]string{
+ "type": documentType,
+ "name": documentName,
+ }
+
+ if err := h.filestorage.Put(file, filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS, fmt.Sprintf("%s/%s_%s", driverID, fileid, filename), fileSize, metadata); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *ApplicationHandler) GetOrganizedCarpoolDriverDocument(ctx context.Context, driverID, document string) (io.Reader, *filestorage.FileInfo, error) {
+ return h.GetDocument(ctx, OrganizedCarpoolDriverDocumentConfig, driverID, document)
+}
+
+func (h *ApplicationHandler) DeleteOrganizedCarpoolDriverDocument(ctx context.Context, driverID, document string) error {
+ return h.DeleteDocument(ctx, OrganizedCarpoolDriverDocumentConfig, driverID, document)
+}
+
+func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driverID, outwardtime, returntime string, departure, destination *geojson.Feature, days map[string]bool) error {
+ // Security check: verify the account exists and is an organized carpool driver
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return fmt.Errorf("issue retrieving driver account: %w", err)
+ }
+ if driver.Namespace != "organized_carpool_drivers" {
+ return fmt.Errorf("account %s is not an organized carpool driver (namespace: %s)", driverID, driver.Namespace)
+ }
+
+ trips := []*proto.CarpoolFeatureCollection{}
+
+ outwardroute, err := h.services.Routing.Route([]orb.Point{departure.Point(), destination.Point()})
+ if err != nil {
+ return fmt.Errorf("failed calling route search: %w", err)
+ }
+ returnroute, err := h.services.Routing.Route([]orb.Point{destination.Point(), departure.Point()})
+ if err != nil {
+ return fmt.Errorf("failed calling route search: %w", err)
+ }
+
+ outwardschedules := []map[string]any{}
+ returnschedules := []map[string]any{}
+
+ dayMap := map[string]string{
+ "monday": "MON",
+ "tuesday": "TUE",
+ "wednesday": "WED",
+ "thursday": "THU",
+ "friday": "FRI",
+ "saturday": "SAT",
+ "sunday": "SUN",
+ }
+
+ for day, enabled := range days {
+ if enabled {
+ dayCode := dayMap[day]
+ outwardschedules = append(outwardschedules, map[string]any{
+ "day": dayCode,
+ "time_of_day": outwardtime,
+ })
+ returnschedules = append(returnschedules, map[string]any{
+ "day": dayCode,
+ "time_of_day": returntime,
+ })
+ }
+ }
+
+ outward_fc := geojson.NewFeatureCollection()
+ outward_fc.Append(departure)
+ outward_fc.Append(destination)
+ outward_fc.ExtraMembers = geojson.Properties{}
+ outward_fc.ExtraMembers["properties"] = map[string]any{
+ "is_driver": true,
+ "is_passenger": false,
+ "user": mobilityaccountsstorage.Account{
+ ID: driverID,
+ },
+ "polyline": outwardroute.Summary.Polyline,
+ "schedules": outwardschedules,
+ "driver_options": map[string]any{},
+ "passenger_options": map[string]any{},
+ }
+ outwardtrip, err := outward_fc.MarshalJSON()
+ if err != nil {
+ return fmt.Errorf("failed marshaling outward geojson: %w", err)
+ }
+
+ return_fc := geojson.NewFeatureCollection()
+ return_fc.Append(destination)
+ return_fc.Append(departure)
+ return_fc.ExtraMembers = geojson.Properties{}
+ return_fc.ExtraMembers["properties"] = map[string]any{
+ "is_driver": true,
+ "is_passenger": false,
+ "user": mobilityaccountsstorage.Account{
+ ID: driverID,
+ },
+ "polyline": returnroute.Summary.Polyline,
+ "schedules": returnschedules,
+ "driver_options": map[string]any{},
+ "passenger_options": map[string]any{},
+ }
+ returntrip, err := return_fc.MarshalJSON()
+ if err != nil {
+ return fmt.Errorf("failed marshaling return geojson: %w", err)
+ }
+
+ trips = append(trips, &proto.CarpoolFeatureCollection{
+ Serialized: string(outwardtrip),
+ })
+ trips = append(trips, &proto.CarpoolFeatureCollection{
+ Serialized: string(returntrip),
+ })
+
+ req := &proto.CreateRegularRoutesRequest{
+ Routes: trips,
+ }
+ _, err = h.services.GRPC.CarpoolService.CreateRegularRoutes(ctx, req)
+ if err != nil {
+ return fmt.Errorf("could not create regular routes: %w", err)
+ }
+
+ return nil
+}
+
+func (h *ApplicationHandler) DeleteOrganizedCarpoolTrip(ctx context.Context, tripID string) error {
+ req := &proto.DeleteRegularRoutesRequest{
+ Ids: []string{tripID},
+ }
+
+ _, err := h.services.GRPC.CarpoolService.DeleteRegularRoutes(ctx, req)
+ if err != nil {
+ return fmt.Errorf("could not delete regular routes: %w", err)
+ }
+
+ return nil
+}
+
+type OrganizedCarpoolJourneyDataResult struct {
+ Journey *geojson.FeatureCollection
+ Driver mobilityaccountsstorage.Account
+ Passenger mobilityaccountsstorage.Account
+ Beneficiaries []mobilityaccountsstorage.Account
+ PassengerWalletBalance float64
+ PricingResult map[string]pricing.Price
+}
+
+func (h *ApplicationHandler) GetOrganizedCarpoolJourneyData(ctx context.Context, driverID, journeyID, passengerID string, currentUserGroup groupstorage.Group) (*OrganizedCarpoolJourneyDataResult, error) {
+ // Get the planned trip data
+ journeyResp, err := h.services.GRPC.CarpoolService.GetPlannedTrip(ctx, &proto.GetPlannedTripRequest{
+ Id: journeyID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not get carpool journey: %w", err)
+ }
+
+ journey, err := geojson.UnmarshalFeatureCollection([]byte(journeyResp.PlannedTrip.Serialized))
+ if err != nil {
+ return nil, fmt.Errorf("could not unmarshal carpool journey: %w", err)
+ }
+
+ driver, err := h.services.GetAccount(driverID)
+ if err != nil {
+ return nil, fmt.Errorf("could not get driver: %w", err)
+ }
+
+ var passenger mobilityaccountsstorage.Account
+ if passengerID != "" {
+ passenger, err = h.services.GetAccount(passengerID)
+ if err != nil {
+ return nil, fmt.Errorf("could not get passenger account: %w", err)
+ }
+ }
+
+ // Get beneficiaries in current user's group
+ beneficiaries, err := h.services.GetBeneficiariesInGroup(currentUserGroup)
+ if err != nil {
+ return nil, fmt.Errorf("could not get beneficiaries: %w", err)
+ }
+
+ // Calculate pricing
+ pricingResult, err := h.calculateOrganizedCarpoolPricing(ctx, journey, passengerID, passenger)
+ if err != nil {
+ log.Error().Err(err).Msg("error calculating organized carpool pricing")
+ pricingResult = map[string]pricing.Price{
+ "passenger": {Amount: 0.0, Currency: "EUR"},
+ "driver": {Amount: 0.0, Currency: "EUR"},
+ }
+ }
+
+ // Calculate passenger wallet balance
+ passengerWalletBalance := h.calculateWalletBalance(passenger)
+
+ return &OrganizedCarpoolJourneyDataResult{
+ Journey: journey,
+ Driver: driver,
+ Passenger: passenger,
+ Beneficiaries: beneficiaries,
+ PassengerWalletBalance: passengerWalletBalance,
+ PricingResult: pricingResult,
+ }, nil
+}
+
+func (h *ApplicationHandler) CreateOrganizedCarpoolJourneyBooking(ctx context.Context, driverID, journeyID, passengerID, motivation, message string, doNotSend bool) (string, error) {
+ if passengerID == "" {
+ return "", fmt.Errorf("missing passenger ID for carpool booking")
+ }
+
+ // Get the planned trip data
+ journeyResp, err := h.services.GRPC.CarpoolService.GetPlannedTrip(ctx, &proto.GetPlannedTripRequest{
+ Id: journeyID,
+ })
+ if err != nil {
+ return "", fmt.Errorf("could not get carpool journey: %w", err)
+ }
+
+ journey, err := geojson.UnmarshalFeatureCollection([]byte(journeyResp.PlannedTrip.Serialized))
+ if err != nil {
+ return "", fmt.Errorf("could not unmarshal carpool journey: %w", err)
+ }
+
+ departureDate := journey.ExtraMembers["departure_date"]
+ var departureTime *timestamppb.Timestamp
+ if departureDate != nil {
+ if dd, ok := departureDate.(string); ok {
+ dt, _ := time.Parse("2006-01-02 15:04", dd)
+ departureTime = timestamppb.New(dt)
+ }
+ }
+
+ // Extract operator from journey data
+ var operatorID string
+ if operator, ok := journey.ExtraMembers["operator"]; ok {
+ if op, ok := operator.(string); ok {
+ operatorID = op
+ }
+ }
+ if operatorID == "" {
+ operatorID = "example.coopgo.fr" // fallback to default
+ }
+
+ if departureTime == nil {
+ // Fallback to current time if we can't extract departure time
+ departureTime = timestamppb.New(time.Now())
+ }
+
+ // Extract journey properties from the geojson data
+ var journeyProps map[string]any
+ if props, ok := journey.ExtraMembers["properties"]; ok {
+ if propsMap, ok := props.(map[string]any); ok {
+ journeyProps = propsMap
+ }
+ }
+
+ if journeyProps == nil {
+ return "", fmt.Errorf("could not extract journey properties")
+ }
+
+ // Extract departure date from journey ExtraMembers
+ if depDate, ok := journey.ExtraMembers["departure_date"]; ok {
+ if depDateStr, ok := depDate.(string); ok {
+ if parsedTime, err := time.Parse("2006-01-02T15:04:05Z", depDateStr); err == nil {
+ departureTime = timestamppb.New(parsedTime)
+ } else if parsedTime, err := time.Parse("2006-01-02", depDateStr); err == nil {
+ departureTime = timestamppb.New(parsedTime)
+ } else {
+ log.Warn().Str("departure_date", depDateStr).Msg("could not parse departure date, using current time")
+ }
+ }
+ }
+
+ // Extract passenger pickup/drop coordinates and addresses from stored passenger data
+ var pickupLat, pickupLng, dropLat, dropLng float64
+ var pickupAddress, dropAddress string
+
+ // Check if we have passenger pickup and drop features in the journey's extra members
+ if pickupData, ok := journey.ExtraMembers["passenger_pickup"]; ok {
+ if pickupMap, ok := pickupData.(map[string]interface{}); ok {
+ if geometry, ok := pickupMap["geometry"].(map[string]interface{}); ok {
+ if coords, ok := geometry["coordinates"].([]interface{}); ok && len(coords) >= 2 {
+ if lng, ok := coords[0].(float64); ok {
+ pickupLng = lng
+ }
+ if lat, ok := coords[1].(float64); ok {
+ pickupLat = lat
+ }
+ }
+ }
+ if properties, ok := pickupMap["properties"].(map[string]interface{}); ok {
+ if label, ok := properties["label"].(string); ok {
+ pickupAddress = label
+ }
+ }
+ }
+ }
+
+ if dropData, ok := journey.ExtraMembers["passenger_drop"]; ok {
+ if dropMap, ok := dropData.(map[string]interface{}); ok {
+ if geometry, ok := dropMap["geometry"].(map[string]interface{}); ok {
+ if coords, ok := geometry["coordinates"].([]interface{}); ok && len(coords) >= 2 {
+ if lng, ok := coords[0].(float64); ok {
+ dropLng = lng
+ }
+ if lat, ok := coords[1].(float64); ok {
+ dropLat = lat
+ }
+ }
+ }
+ if properties, ok := dropMap["properties"].(map[string]interface{}); ok {
+ if label, ok := properties["label"].(string); ok {
+ dropAddress = label
+ }
+ }
+ }
+ }
+
+ // Extract time from schedules if available and no specific departure_date was found
+ if departureTime.AsTime().Equal(time.Now().Truncate(time.Second)) {
+ if schedules, ok := journeyProps["schedules"]; ok {
+ if schedulesList, ok := schedules.([]any); ok && len(schedulesList) > 0 {
+ if schedule, ok := schedulesList[0].(map[string]any); ok {
+ if timeOfDay, ok := schedule["time_of_day"].(string); ok {
+ // Parse time and combine with current date
+ now := time.Now()
+ timeStr := fmt.Sprintf("%s %s", now.Format("2006-01-02"), timeOfDay)
+ if depTime, err := time.Parse("2006-01-02 15:04", timeStr); err == nil {
+ departureTime = timestamppb.New(depTime)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Get passenger account and calculate pricing
+ var passenger mobilityaccountsstorage.Account
+ if passengerID != "" {
+ passenger, err = h.services.GetAccount(passengerID)
+ if err != nil {
+ return "", fmt.Errorf("could not get passenger account: %w", err)
+ }
+ }
+
+ pricingResult, err := h.calculateOrganizedCarpoolPricing(ctx, journey, passengerID, passenger)
+ if err != nil {
+ log.Error().Err(err).Msg("error calculating organized carpool pricing")
+ pricingResult = map[string]pricing.Price{
+ "passenger": {Amount: 0.0, Currency: "EUR"},
+ "driver": {Amount: 0.0, Currency: "EUR"},
+ }
+ }
+
+ // Extract price values
+ priceAmount := pricingResult["passenger"].Amount
+ priceCurrency := pricingResult["passenger"].Currency
+ driverCompensationAmount := pricingResult["driver"].Amount
+ driverCompensationCurrency := pricingResult["driver"].Currency
+
+ // Determine price type
+ priceType := proto.CarpoolServicePriceType_PAYING
+ if priceAmount == 0 {
+ priceType = proto.CarpoolServicePriceType_FREE
+ }
+
+ // Extract passenger distance from journey
+ var passengerDistance *int64
+ if dist, ok := journey.ExtraMembers["passenger_distance"].(float64); ok {
+ distInt := int64(dist)
+ passengerDistance = &distInt
+ }
+
+ // Create carpool booking using extracted journey data
+ booking := &proto.CarpoolServiceBooking{
+ Id: uuid.NewString(),
+ Driver: &proto.CarpoolServiceUser{
+ Id: driverID,
+ Operator: operatorID,
+ },
+ Passenger: &proto.CarpoolServiceUser{
+ Id: passengerID,
+ Operator: operatorID,
+ },
+ PassengerPickupDate: departureTime,
+ PassengerPickupLat: pickupLat,
+ PassengerPickupLng: pickupLng,
+ PassengerDropLat: dropLat,
+ PassengerDropLng: dropLng,
+ PassengerPickupAddress: &pickupAddress,
+ PassengerDropAddress: &dropAddress,
+ Status: proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION,
+ Distance: passengerDistance,
+ DriverJourneyId: journeyID,
+ Price: &proto.CarpoolServicePrice{
+ Type: &priceType,
+ Amount: &priceAmount,
+ Currency: &priceCurrency,
+ },
+ DriverCompensationAmount: &driverCompensationAmount,
+ DriverCompensationCurrency: &driverCompensationCurrency,
+ Motivation: &motivation,
+ }
+
+ bookingRes, err := h.services.GRPC.CarpoolService.CreateBooking(ctx, &proto.CreateCarpoolBookingRequest{
+ Booking: booking,
+ })
+ if err != nil {
+ return "", fmt.Errorf("cannot create carpool booking: %w", err)
+ }
+
+ // Send SMS notification if requested
+ if message != "" && !doNotSend {
+ send_message := strings.ReplaceAll(message, "{booking_id}", bookingRes.Booking.Id)
+ log.Debug().Str("message", send_message).Msg("Carpool booking created: sending message")
+ h.GenerateSMS(driverID, send_message)
+ }
+
+ return bookingRes.Booking.Id, nil
+}
+
+func (h *ApplicationHandler) calculateOrganizedCarpoolPricing(ctx context.Context, tripGeoJSON *geojson.FeatureCollection, passengerID string, passenger mobilityaccountsstorage.Account) (map[string]pricing.Price, error) {
+ // For organized carpool, use simple pricing based on distance
+ // Extract distance from journey features if available
+ var passengerDistance int64 = 0
+ var driverDistance int64 = 0
+
+ // Try to extract distance from journey extra members
+ if tripGeoJSON != nil && tripGeoJSON.ExtraMembers != nil {
+ if dist, ok := tripGeoJSON.ExtraMembers["passenger_distance"].(float64); ok {
+ passengerDistance = int64(dist)
+ }
+ if dist, ok := tripGeoJSON.ExtraMembers["driver_distance"].(float64); ok {
+ driverDistance = int64(dist)
+ }
+ }
+
+ benefParams := pricing.BeneficiaryParams{}
+ if passengerID == "" {
+ benefParams = pricing.BeneficiaryParams{
+ History: 99,
+ Priority: false,
+ }
+ } else {
+ // Get organized carpool history for passenger
+ carpoolBookings, err := h.services.GRPC.CarpoolService.GetUserBookings(ctx, &proto.GetUserBookingsRequest{
+ UserId: passengerID,
+ })
+
+ // Check priority status
+ priority := false
+ if a, ok := passenger.Data["other_properties"]; ok {
+ if b, ok := a.(map[string]any); ok {
+ if c, ok := b["status"]; ok {
+ if p, ok := c.(string); ok {
+ priority = (p == "Prioritaire")
+ }
+ }
+ }
+ }
+
+ // Calculate history from previous bookings and stored value
+ history := 0
+ if op, ok := passenger.Data["other_properties"]; ok {
+ if op_map, ok := op.(map[string]any); ok {
+ if poc, ok := op_map["previous_organized_carpool"]; ok {
+ if poc_str, ok := poc.(string); ok {
+ if poc_str != "" {
+ if n, err := strconv.Atoi(poc_str); err == nil {
+ history = history + n
+ } else {
+ log.Error().Err(err).Str("n", poc_str).Msg("string to int conversion error")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Count only WAITING_DRIVER_CONFIRMATION, CONFIRMED and VALIDATED bookings
+ if err == nil {
+ for _, booking := range carpoolBookings.Bookings {
+ if booking.Status == proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION ||
+ booking.Status == proto.CarpoolServiceBookingStatus_CONFIRMED ||
+ booking.Status == proto.CarpoolServiceBookingStatus_VALIDATED {
+ history++
+ }
+ }
+ }
+
+ benefParams = pricing.BeneficiaryParams{
+ History: history,
+ Priority: priority,
+ }
+ }
+
+ pricingParams := pricing.PricingParams{
+ MobilityType: "organized_carpool",
+ Beneficiary: benefParams,
+ SharedMobility: pricing.SharedMobilityParams{
+ DriverDistance: driverDistance,
+ PassengerDistance: passengerDistance,
+ },
+ }
+
+ log.Info().
+ Str("mobility_type", pricingParams.MobilityType).
+ Int("beneficiary_history", pricingParams.Beneficiary.History).
+ Bool("beneficiary_priority", pricingParams.Beneficiary.Priority).
+ Int64("driver_distance", pricingParams.SharedMobility.DriverDistance).
+ Int64("passenger_distance", pricingParams.SharedMobility.PassengerDistance).
+ Str("passenger_id", passengerID).
+ Msg("calling pricing service for organized carpool")
+
+ pricingResult, err := h.services.Pricing.Prices(pricingParams)
+ if err != nil {
+ log.Error().Err(err).Msg("pricing service returned error")
+ return nil, err
+ }
+
+ log.Info().
+ Float64("passenger_price", pricingResult["passenger"].Amount).
+ Str("passenger_currency", pricingResult["passenger"].Currency).
+ Float64("driver_price", pricingResult["driver"].Amount).
+ Str("driver_currency", pricingResult["driver"].Currency).
+ Msg("pricing service result for organized carpool")
+
+ return pricingResult, nil
+}
+
+type OrganizedCarpoolBookingsResult struct {
+ Bookings []*proto.CarpoolServiceBooking
+ DriversMap map[string]mobilityaccountsstorage.Account
+ BeneficiariesMap map[string]mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) GetOrganizedCarpoolBookings(ctx context.Context, startDate, endDate *time.Time, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode string) (*OrganizedCarpoolBookingsResult, error) {
+ // Get all drivers
+ drivers, err := h.getOrganizedCarpoolDrivers(ctx, "", false)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting organized carpool drivers")
+ drivers = []mobilityaccountsstorage.Account{}
+ }
+
+ driversMap := map[string]mobilityaccountsstorage.Account{}
+ for _, d := range drivers {
+ driversMap[d.ID] = d
+ }
+
+ // Get beneficiaries
+ beneficiariesMap, err := h.services.GetBeneficiariesMap()
+ if err != nil {
+ beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
+ }
+
+ // Determine date range
+ var start, end time.Time
+ if startDate != nil {
+ start = *startDate
+ } else {
+ start = time.Now().Add(-365 * 24 * time.Hour)
+ }
+ if endDate != nil {
+ end = *endDate
+ } else {
+ end = time.Now()
+ }
+
+ // Get bookings from gRPC service
+ request := &proto.GetCarpoolBookingsRequest{
+ MinDate: timestamppb.New(start),
+ MaxDate: timestamppb.New(end),
+ }
+
+ resp, err := h.services.GRPC.CarpoolService.GetCarpoolBookings(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ bookings := []*proto.CarpoolServiceBooking{}
+ for _, b := range resp.Bookings {
+ // Apply driver filter if specified
+ if driverID != "" && b.Driver.Id != driverID {
+ continue
+ }
+ // Apply status filter if specified
+ if status != "" && b.Status.String() != status {
+ continue
+ }
+ bookings = append(bookings, b)
+ }
+
+ // Apply geography filtering
+ var departurePolygons, destinationPolygons, passengerAddressPolygons []orb.Polygon
+ if departureGeoLayer != "" && departureGeoCode != "" {
+ departurePolygons, err = h.loadGeographyPolygon(departureGeoLayer, departureGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load departure geography filter")
+ }
+ }
+ if destinationGeoLayer != "" && destinationGeoCode != "" {
+ destinationPolygons, err = h.loadGeographyPolygon(destinationGeoLayer, destinationGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load destination geography filter")
+ }
+ }
+ if passengerAddressGeoLayer != "" && passengerAddressGeoCode != "" {
+ passengerAddressPolygons, err = h.loadGeographyPolygon(passengerAddressGeoLayer, passengerAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load passenger address geography filter")
+ }
+ }
+ bookings = filterOrganizedCarpoolBookingsByGeography(bookings, departurePolygons, destinationPolygons)
+ bookings = filterOrganizedCarpoolBookingsByPassengerAddressGeography(bookings, beneficiariesMap, passengerAddressPolygons)
+
+ // Sort by date (most recent first)
+ slices.SortFunc(bookings, func(a, b *proto.CarpoolServiceBooking) int {
+ return cmp.Compare(b.PassengerPickupDate.AsTime().Unix(), a.PassengerPickupDate.AsTime().Unix())
+ })
+
+ return &OrganizedCarpoolBookingsResult{
+ Bookings: bookings,
+ DriversMap: driversMap,
+ BeneficiariesMap: beneficiariesMap,
+ }, nil
+}
diff --git a/core/application/sms.go b/core/application/sms.go
new file mode 100644
index 0000000..91292bc
--- /dev/null
+++ b/core/application/sms.go
@@ -0,0 +1,41 @@
+package application
+
+import (
+ "context"
+ "errors"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *ApplicationHandler) SendSMS(ctx context.Context, beneficiaryID, message string) error {
+ return h.GenerateSMS(beneficiaryID, message)
+}
+
+func (h *ApplicationHandler) GenerateSMS(recipientid string, message string) error {
+ recipient, err := h.services.GetAccount(recipientid)
+ if err != nil {
+ log.Error().Err(err).Msg("user not found")
+ return err
+ }
+
+ pn, ok := recipient.Data["phone_number"]
+ if !ok {
+ log.Error().Msg("Beneficiary doesn't have a phone number")
+ return errors.New("missing phone number")
+ }
+ phoneNumber, ok := pn.(string)
+ if !ok {
+ log.Error().Msg("phone number type error")
+ return errors.New("phone number type error")
+ }
+
+ sender := h.config.GetString("service_name")
+
+ err = h.services.SMS.Send(phoneNumber, message, sender)
+ if err != nil {
+ log.Error().Err(err).Msg("cannot send SMS")
+ return err
+ }
+
+ return nil
+}
diff --git a/core/application/solidarity-transport.go b/core/application/solidarity-transport.go
new file mode 100644
index 0000000..5b3ac23
--- /dev/null
+++ b/core/application/solidarity-transport.go
@@ -0,0 +1,1514 @@
+package application
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
+ groupstorage "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"
+ "git.coopgo.io/coopgo-platform/payments/pricing"
+ "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
+ solidaritytransformers "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
+ solidaritytypes "git.coopgo.io/coopgo-platform/solidarity-transport/types"
+ "github.com/google/uuid"
+ "github.com/paulmach/orb"
+ "github.com/paulmach/orb/geojson"
+ "github.com/paulmach/orb/planar"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+)
+
+type DriversForm struct {
+ FirstName string `json:"first_name" validate:"required"`
+ LastName string `json:"last_name" validate:"required"`
+ Email string `json:"email" validate:"required,email"`
+ Birthdate *time.Time `json:"birthdate" validate:"required"`
+ PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
+ Address any `json:"address,omitempty"`
+ Gender string `json:"gender"`
+}
+
+const (
+ Sunday = iota
+ Monday
+ Tuesday
+ Wednesday
+ Thursday
+ Friday
+ Saturday
+)
+
+type SolidarityTransportOverviewResult struct {
+ Accounts []mobilityaccountsstorage.Account
+ AccountsMap map[string]mobilityaccountsstorage.Account
+ BeneficiariesMap map[string]mobilityaccountsstorage.Account
+ Bookings []*solidaritytypes.Booking
+ BookingsHistory []*solidaritytypes.Booking
+}
+
+// loadGeographyPolygon loads a single geography polygon for filtering
+func (h *ApplicationHandler) loadGeographyPolygon(layer, code string) ([]orb.Polygon, error) {
+ if layer == "" || code == "" {
+ return nil, nil
+ }
+
+ // Fetch geography from service
+ geoFeature, err := h.services.Geography.Find(layer, code)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load geography %s/%s: %w", layer, code, err)
+ }
+
+ polygons := []orb.Polygon{}
+ // Extract polygon from the feature
+ if geoFeature != nil && geoFeature.Geometry != nil {
+ switch geom := geoFeature.Geometry.(type) {
+ case orb.Polygon:
+ polygons = append(polygons, geom)
+ case orb.MultiPolygon:
+ for _, poly := range geom {
+ polygons = append(polygons, poly)
+ }
+ }
+ }
+
+ return polygons, nil
+}
+
+// isPointInGeographies checks if a point is within any of the geography polygons
+func isPointInGeographies(point orb.Point, polygons []orb.Polygon) bool {
+ for _, poly := range polygons {
+ if planar.PolygonContains(poly, point) {
+ return true
+ }
+ }
+ return false
+}
+
+// filterBookingsByPassengerAddressGeography filters bookings where passenger address is within geography
+func filterBookingsByPassengerAddressGeography(bookings []*solidaritytypes.Booking, beneficiariesMap map[string]mobilityaccountsstorage.Account, addressPolygons []orb.Polygon) []*solidaritytypes.Booking {
+ if len(addressPolygons) == 0 {
+ return bookings
+ }
+
+ filtered := []*solidaritytypes.Booking{}
+ for _, booking := range bookings {
+ passenger, ok := beneficiariesMap[booking.PassengerId]
+ if !ok {
+ continue
+ }
+
+ // Check if passenger has address with geometry
+ if address, ok := passenger.Data["address"].(map[string]any); ok {
+ if geometry, ok := address["geometry"].(map[string]any); ok {
+ if geomType, ok := geometry["type"].(string); ok && geomType == "Point" {
+ if coords, ok := geometry["coordinates"].([]any); ok && len(coords) == 2 {
+ if lon, ok := coords[0].(float64); ok {
+ if lat, ok := coords[1].(float64); ok {
+ point := orb.Point{lon, lat}
+ if isPointInGeographies(point, addressPolygons) {
+ filtered = append(filtered, booking)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return filtered
+}
+
+// filterBookingsByGeography filters bookings based on geography constraints
+func filterBookingsByGeography(bookings []*solidaritytypes.Booking, departurePolygons, destinationPolygons []orb.Polygon) []*solidaritytypes.Booking {
+ // If no filters, return all bookings
+ if len(departurePolygons) == 0 && len(destinationPolygons) == 0 {
+ return bookings
+ }
+
+ filtered := []*solidaritytypes.Booking{}
+ for _, booking := range bookings {
+ if booking.Journey == nil {
+ continue
+ }
+
+ includeBooking := true
+
+ // Check departure filter if provided
+ if len(departurePolygons) > 0 {
+ departureMatch := false
+ if booking.Journey.PassengerPickup != nil && booking.Journey.PassengerPickup.Geometry != nil {
+ if point, ok := booking.Journey.PassengerPickup.Geometry.(orb.Point); ok {
+ departureMatch = isPointInGeographies(point, departurePolygons)
+ }
+ }
+ if !departureMatch {
+ includeBooking = false
+ }
+ }
+
+ // Check destination filter if provided
+ if len(destinationPolygons) > 0 && includeBooking {
+ destinationMatch := false
+ if booking.Journey.PassengerDrop != nil && booking.Journey.PassengerDrop.Geometry != nil {
+ if point, ok := booking.Journey.PassengerDrop.Geometry.(orb.Point); ok {
+ destinationMatch = isPointInGeographies(point, destinationPolygons)
+ }
+ }
+ if !destinationMatch {
+ includeBooking = false
+ }
+ }
+
+ if includeBooking {
+ filtered = append(filtered, booking)
+ }
+ }
+
+ return filtered
+}
+
+func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context, status, driverID, startDate, endDate, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode, histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeoLayer, histDepartureGeoCode, histDestinationGeoLayer, histDestinationGeoCode, histPassengerAddressGeoLayer, histPassengerAddressGeoCode string, archivedFilter bool, driverAddressGeoLayer, driverAddressGeoCode string) (*SolidarityTransportOverviewResult, error) {
+ // Get ALL drivers for the accountsMap (used in bookings display)
+ allDrivers, err := h.solidarityDrivers("", false)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting all solidarity drivers")
+ allDrivers = []mobilityaccountsstorage.Account{}
+ }
+
+ // Build accountsMap with ALL drivers (for bookings to reference)
+ accountsMap := map[string]mobilityaccountsstorage.Account{}
+ for _, a := range allDrivers {
+ accountsMap[a.ID] = a
+ }
+
+ // Get filtered drivers for the drivers tab display
+ accounts, err := h.solidarityDrivers("", archivedFilter)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting solidarity drivers")
+ accounts = []mobilityaccountsstorage.Account{}
+ }
+
+ // Apply driver address geography filtering only to the drivers tab list
+ if driverAddressGeoLayer != "" && driverAddressGeoCode != "" {
+ driverAddressPolygons, err := h.loadGeographyPolygon(driverAddressGeoLayer, driverAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load driver address geography filter")
+ } else {
+ filtered := []mobilityaccountsstorage.Account{}
+ for _, account := range accounts {
+ // Check if driver has address - unmarshal as GeoJSON Feature
+ if addr, ok := account.Data["address"]; ok {
+ jsonAddr, err := json.Marshal(addr)
+ if err == nil {
+ addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
+ if err == nil && addrGeojson.Geometry != nil {
+ if point, ok := addrGeojson.Geometry.(orb.Point); ok {
+ if isPointInGeographies(point, driverAddressPolygons) {
+ filtered = append(filtered, account)
+ }
+ }
+ }
+ }
+ }
+ }
+ accounts = filtered
+ }
+ }
+
+ // Sort accounts by last name, then first name
+ slices.SortFunc(accounts, func(a, b mobilityaccountsstorage.Account) int {
+ lastNameA := strings.ToLower(a.Data["last_name"].(string))
+ lastNameB := strings.ToLower(b.Data["last_name"].(string))
+ if lastNameA != lastNameB {
+ return strings.Compare(lastNameA, lastNameB)
+ }
+ firstNameA := strings.ToLower(a.Data["first_name"].(string))
+ firstNameB := strings.ToLower(b.Data["first_name"].(string))
+ return strings.Compare(firstNameA, firstNameB)
+ })
+
+ beneficiariesMap, err := h.services.GetBeneficiariesMap()
+ if err != nil {
+ beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
+ }
+
+ // Parse start date or use default
+ var startdate time.Time
+ if startDate != "" {
+ if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
+ startdate = parsed
+ } else {
+ startdate = time.Now()
+ }
+ } else {
+ startdate = time.Now()
+ }
+
+ // Parse end date or use default
+ 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
+ } else {
+ enddate = time.Now().Add(24 * 365 * time.Hour)
+ }
+ } else {
+ enddate = time.Now().Add(24 * 365 * time.Hour)
+ }
+
+ request := &gen.GetSolidarityTransportBookingsRequest{
+ StartDate: timestamppb.New(startdate),
+ EndDate: timestamppb.New(enddate),
+ Status: status,
+ Driverid: driverID,
+ }
+
+ resp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, request)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting solidarity transport bookings")
+ return &SolidarityTransportOverviewResult{
+ Accounts: accounts,
+ AccountsMap: accountsMap,
+ BeneficiariesMap: beneficiariesMap,
+ Bookings: []*solidaritytypes.Booking{},
+ BookingsHistory: []*solidaritytypes.Booking{},
+ }, nil
+ }
+
+ // Get bookings history with filters
+ // Parse history start date or use default (1 month ago)
+ var histStartdate time.Time
+ if histStartDate != "" {
+ if parsed, err := time.Parse("2006-01-02", histStartDate); err == nil {
+ histStartdate = parsed
+ } else {
+ histStartdate = time.Now().Add(-30 * 24 * time.Hour)
+ }
+ } else {
+ histStartdate = time.Now().Add(-30 * 24 * time.Hour)
+ }
+
+ // Parse history end date or use default (yesterday)
+ var histEnddate time.Time
+ if histEndDate != "" {
+ if parsed, err := time.Parse("2006-01-02", histEndDate); err == nil {
+ histEnddate = parsed.Add(24 * time.Hour) // End of day
+ } else {
+ histEnddate = time.Now().Add(-24 * time.Hour)
+ }
+ } else {
+ histEnddate = time.Now().Add(-24 * time.Hour)
+ }
+
+ historyRequest := &gen.GetSolidarityTransportBookingsRequest{
+ StartDate: timestamppb.New(histStartdate),
+ EndDate: timestamppb.New(histEnddate),
+ Status: histStatus,
+ Driverid: histDriverID,
+ }
+
+ historyResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, historyRequest)
+ bookingsHistory := []*gen.SolidarityTransportBooking{}
+ if err == nil {
+ bookingsHistory = historyResp.Bookings
+ }
+
+ // Transform bookings to types
+ transformedBookings := []*solidaritytypes.Booking{}
+ for _, booking := range resp.Bookings {
+ if transformed, err := solidaritytransformers.BookingProtoToType(booking); err == nil {
+ transformedBookings = append(transformedBookings, transformed)
+ }
+ }
+
+ // Apply geography filtering for current bookings
+ var departurePolygons, destinationPolygons, passengerAddressPolygons []orb.Polygon
+ if departureGeoLayer != "" && departureGeoCode != "" {
+ departurePolygons, err = h.loadGeographyPolygon(departureGeoLayer, departureGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load departure geography filter")
+ }
+ }
+ if destinationGeoLayer != "" && destinationGeoCode != "" {
+ destinationPolygons, err = h.loadGeographyPolygon(destinationGeoLayer, destinationGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load destination geography filter")
+ }
+ }
+ if passengerAddressGeoLayer != "" && passengerAddressGeoCode != "" {
+ passengerAddressPolygons, err = h.loadGeographyPolygon(passengerAddressGeoLayer, passengerAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load passenger address geography filter")
+ }
+ }
+ transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons)
+ transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons)
+
+ // Filter out replaced bookings from upcoming bookings list
+ filteredBookings := []*solidaritytypes.Booking{}
+ for _, booking := range transformedBookings {
+ if booking.Data != nil {
+ if _, hasReplacedBy := booking.Data["replaced_by"]; hasReplacedBy {
+ continue // Skip bookings that have been replaced
+ }
+ }
+ filteredBookings = append(filteredBookings, booking)
+ }
+ transformedBookings = filteredBookings
+
+ // Sort upcoming bookings by date (ascending - earliest first)
+ sort.Slice(transformedBookings, func(i, j int) bool {
+ if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil {
+ return transformedBookings[i].Journey.PassengerPickupDate.Before(transformedBookings[j].Journey.PassengerPickupDate)
+ }
+ return false
+ })
+
+ transformedBookingsHistory := []*solidaritytypes.Booking{}
+ for _, booking := range bookingsHistory {
+ if transformed, err := solidaritytransformers.BookingProtoToType(booking); err == nil {
+ transformedBookingsHistory = append(transformedBookingsHistory, transformed)
+ }
+ }
+
+ // Apply geography filtering for history bookings
+ var histDeparturePolygons, histDestinationPolygons, histPassengerAddressPolygons []orb.Polygon
+ if histDepartureGeoLayer != "" && histDepartureGeoCode != "" {
+ histDeparturePolygons, err = h.loadGeographyPolygon(histDepartureGeoLayer, histDepartureGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load history departure geography filter")
+ }
+ }
+ if histDestinationGeoLayer != "" && histDestinationGeoCode != "" {
+ histDestinationPolygons, err = h.loadGeographyPolygon(histDestinationGeoLayer, histDestinationGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load history destination geography filter")
+ }
+ }
+ if histPassengerAddressGeoLayer != "" && histPassengerAddressGeoCode != "" {
+ histPassengerAddressPolygons, err = h.loadGeographyPolygon(histPassengerAddressGeoLayer, histPassengerAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load history passenger address geography filter")
+ }
+ }
+ transformedBookingsHistory = filterBookingsByGeography(transformedBookingsHistory, histDeparturePolygons, histDestinationPolygons)
+ transformedBookingsHistory = filterBookingsByPassengerAddressGeography(transformedBookingsHistory, beneficiariesMap, histPassengerAddressPolygons)
+
+ // Don't filter out replaced bookings from history - we want to see them in history
+
+ // Sort history bookings by date (descending - most recent first)
+ sort.Slice(transformedBookingsHistory, func(i, j int) bool {
+ if transformedBookingsHistory[i].Journey != nil && transformedBookingsHistory[j].Journey != nil {
+ return transformedBookingsHistory[i].Journey.PassengerPickupDate.After(transformedBookingsHistory[j].Journey.PassengerPickupDate)
+ }
+ return false
+ })
+
+ return &SolidarityTransportOverviewResult{
+ Accounts: accounts,
+ AccountsMap: accountsMap,
+ BeneficiariesMap: beneficiariesMap,
+ Bookings: transformedBookings,
+ BookingsHistory: transformedBookingsHistory,
+ }, nil
+}
+
+type SolidarityTransportBookingsResult struct {
+ Bookings []*solidaritytypes.Booking
+ DriversMap map[string]mobilityaccountsstorage.Account
+ BeneficiariesMap map[string]mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) GetSolidarityTransportBookings(ctx context.Context, startDate, endDate *time.Time, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode string) (*SolidarityTransportBookingsResult, error) {
+ // Get all drivers
+ drivers, err := h.solidarityDrivers("", false)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting solidarity drivers")
+ drivers = []mobilityaccountsstorage.Account{}
+ }
+
+ driversMap := map[string]mobilityaccountsstorage.Account{}
+ for _, d := range drivers {
+ driversMap[d.ID] = d
+ }
+
+ // Get beneficiaries
+ beneficiariesMap, err := h.services.GetBeneficiariesMap()
+ if err != nil {
+ beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
+ }
+
+ // Determine date range
+ var start, end time.Time
+ if startDate != nil {
+ start = *startDate
+ } else {
+ // Default: 1 year ago
+ start = time.Now().Add(-365 * 24 * time.Hour)
+ }
+
+ if endDate != nil {
+ end = *endDate
+ } else {
+ // Default: now
+ end = time.Now()
+ }
+
+ // Get bookings
+ request := &gen.GetSolidarityTransportBookingsRequest{
+ StartDate: timestamppb.New(start),
+ EndDate: timestamppb.New(end),
+ Status: status,
+ Driverid: driverID,
+ }
+
+ resp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, request)
+ if err != nil {
+ log.Error().Err(err).Msg("issue getting solidarity transport bookings")
+ return &SolidarityTransportBookingsResult{
+ Bookings: []*solidaritytypes.Booking{},
+ DriversMap: driversMap,
+ BeneficiariesMap: beneficiariesMap,
+ }, nil
+ }
+
+ // Transform bookings to types
+ transformedBookings := []*solidaritytypes.Booking{}
+ for _, booking := range resp.Bookings {
+ if transformed, err := solidaritytransformers.BookingProtoToType(booking); err == nil {
+ transformedBookings = append(transformedBookings, transformed)
+ }
+ }
+
+ // Apply geography filtering
+ var departurePolygons, destinationPolygons, passengerAddressPolygons []orb.Polygon
+ if departureGeoLayer != "" && departureGeoCode != "" {
+ departurePolygons, err = h.loadGeographyPolygon(departureGeoLayer, departureGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load departure geography filter for export")
+ }
+ }
+ if destinationGeoLayer != "" && destinationGeoCode != "" {
+ destinationPolygons, err = h.loadGeographyPolygon(destinationGeoLayer, destinationGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load destination geography filter for export")
+ }
+ }
+ if passengerAddressGeoLayer != "" && passengerAddressGeoCode != "" {
+ passengerAddressPolygons, err = h.loadGeographyPolygon(passengerAddressGeoLayer, passengerAddressGeoCode)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to load passenger address geography filter for export")
+ }
+ }
+ transformedBookings = filterBookingsByGeography(transformedBookings, departurePolygons, destinationPolygons)
+ transformedBookings = filterBookingsByPassengerAddressGeography(transformedBookings, beneficiariesMap, passengerAddressPolygons)
+
+ // Don't filter out replaced bookings for exports - include all bookings
+
+ // Sort bookings by date
+ sort.Slice(transformedBookings, func(i, j int) bool {
+ if transformedBookings[i].Journey != nil && transformedBookings[j].Journey != nil {
+ return transformedBookings[i].Journey.PassengerPickupDate.Before(transformedBookings[j].Journey.PassengerPickupDate)
+ }
+ return false
+ })
+
+ return &SolidarityTransportBookingsResult{
+ Bookings: transformedBookings,
+ DriversMap: driversMap,
+ BeneficiariesMap: beneficiariesMap,
+ }, nil
+}
+
+func (h *ApplicationHandler) CreateSolidarityTransportDriver(ctx context.Context, firstName, lastName, email string, birthdate *time.Time, phoneNumber string, address any, gender string, otherProperties any) (string, error) {
+ dataMap := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ "phone_number": phoneNumber,
+ "gender": gender,
+ }
+
+ // Convert birthdate to string for structpb compatibility
+ if birthdate != nil {
+ dataMap["birthdate"] = birthdate.Format(time.RFC3339)
+ }
+
+ if address != nil {
+ dataMap["address"] = address
+ }
+
+ if otherProperties != nil {
+ dataMap["other_properties"] = otherProperties
+ }
+
+ // Validate the data
+ formData := DriversForm{
+ FirstName: firstName,
+ LastName: lastName,
+ Email: email,
+ Birthdate: birthdate,
+ PhoneNumber: phoneNumber,
+ Address: address,
+ Gender: gender,
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return "", err
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ request := &mobilityaccounts.RegisterRequest{
+ Account: &mobilityaccounts.Account{
+ Namespace: "solidarity_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.Account.Id, nil
+}
+
+type SolidarityTransportDriverResult struct {
+ Driver mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) GetSolidarityTransportDriver(ctx context.Context, driverID string) (*SolidarityTransportDriverResult, error) {
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Security check: ensure this is actually a solidarity transport driver account
+ if resp.Account.Namespace != "solidarity_drivers" {
+ return nil, fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, resp.Account.Namespace)
+ }
+
+ return &SolidarityTransportDriverResult{
+ Driver: resp.Account.ToStorageType(),
+ }, nil
+}
+
+func (h *ApplicationHandler) UpdateSolidarityTransportDriver(ctx context.Context, driverID, firstName, lastName, email string, birthdate *time.Time, phoneNumber string, address any, gender string, otherProperties any) (string, error) {
+ // Security check: verify the account exists and is a solidarity transport driver
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return "", err
+ }
+ if getResp.Account.Namespace != "solidarity_drivers" {
+ return "", fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace)
+ }
+
+ dataMap := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ "phone_number": phoneNumber,
+ "gender": gender,
+ }
+
+ // Convert birthdate to string for structpb compatibility
+ if birthdate != nil {
+ dataMap["birthdate"] = birthdate.Format(time.RFC3339)
+ }
+
+ if address != nil {
+ dataMap["address"] = address
+ }
+
+ if otherProperties != nil {
+ dataMap["other_properties"] = otherProperties
+ }
+
+ // Validate the data
+ formData := DriversForm{
+ FirstName: firstName,
+ LastName: lastName,
+ Email: email,
+ Birthdate: birthdate,
+ PhoneNumber: phoneNumber,
+ Address: address,
+ Gender: gender,
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return "", err
+ }
+
+ data, err := structpb.NewValue(dataMap)
+ if err != nil {
+ return "", err
+ }
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: driverID,
+ Namespace: "solidarity_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ return resp.Account.Id, nil
+}
+
+type SolidarityTransportDriverDataResult struct {
+ Driver mobilityaccountsstorage.Account
+ Availabilities []*gen.DriverRegularAvailability
+ Documents []filestorage.FileInfo
+ Bookings []*solidaritytypes.Booking
+ BeneficiariesMap map[string]mobilityaccountsstorage.Account
+ Stats map[string]any
+ WalletBalance float64
+}
+
+func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Context, driverID string) (*SolidarityTransportDriverDataResult, error) {
+ // Get driver account
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Security check: ensure this is actually a solidarity transport driver account
+ if resp.Account.Namespace != "solidarity_drivers" {
+ return nil, fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, resp.Account.Namespace)
+ }
+
+ driver := resp.Account.ToStorageType()
+
+ // Ensure other_properties exists to prevent template errors
+ if driver.Data == nil {
+ driver.Data = make(map[string]interface{})
+ }
+ if driver.Data["other_properties"] == nil {
+ driver.Data["other_properties"] = make(map[string]interface{})
+ }
+
+ // Get availabilities
+ availRequest := &gen.GetDriverRegularAvailabilitiesRequest{
+ DriverId: driverID,
+ }
+
+ availResp, err := h.services.GRPC.SolidarityTransport.GetDriverRegularAvailabilities(ctx, availRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get documents
+ documents := h.filestorage.List(filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS + "/" + driverID)
+
+ // Get driver bookings
+ bookingsRequest := &gen.GetSolidarityTransportBookingsRequest{
+ StartDate: timestamppb.New(time.Now().Add(-365 * 24 * time.Hour)),
+ EndDate: timestamppb.New(time.Now().Add(365 * 24 * time.Hour)),
+ Driverid: driverID,
+ }
+
+ bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, bookingsRequest)
+ protoBookings := []*gen.SolidarityTransportBooking{}
+ if err == nil {
+ protoBookings = bookingsResp.Bookings
+ }
+
+ // Convert proto bookings to types with geojson.Feature
+ bookings := []*solidaritytypes.Booking{}
+ for _, protoBooking := range protoBookings {
+ booking, err := solidaritytransformers.BookingProtoToType(protoBooking)
+ if err != nil {
+ log.Error().Err(err).Msg("error converting booking proto to type")
+ continue
+ }
+ bookings = append(bookings, booking)
+ }
+
+ // Don't filter out replaced bookings from driver profile - show all bookings
+
+ // Collect unique passenger IDs
+ passengerIDs := []string{}
+ passengerIDsMap := make(map[string]bool)
+ for _, booking := range bookings {
+ if booking.PassengerId != "" {
+ if !passengerIDsMap[booking.PassengerId] {
+ passengerIDs = append(passengerIDs, booking.PassengerId)
+ passengerIDsMap[booking.PassengerId] = true
+ }
+ }
+ }
+
+ // Get beneficiaries in batch
+ beneficiariesMap := make(map[string]mobilityaccountsstorage.Account)
+ if len(passengerIDs) > 0 {
+ beneficiariesResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: passengerIDs,
+ })
+ if err == nil {
+ for _, account := range beneficiariesResp.Accounts {
+ a := account.ToStorageType()
+ beneficiariesMap[a.ID] = a
+ }
+ }
+ }
+
+ // Calculate stats only for validated bookings
+ validatedCount := 0
+ kmnb := 0
+ for _, booking := range bookings {
+ if booking.Status == "VALIDATED" {
+ validatedCount++
+ if booking.Journey != nil {
+ kmnb += int(booking.Journey.DriverDistance)
+ }
+ }
+ }
+
+ stats := map[string]any{
+ "bookings": map[string]any{
+ "count": validatedCount,
+ "km": kmnb,
+ },
+ }
+
+ // Calculate wallet balance like in original handler
+ walletBalance := h.calculateWalletBalance(driver)
+
+ return &SolidarityTransportDriverDataResult{
+ Driver: driver,
+ Availabilities: availResp.Results,
+ Documents: documents,
+ Bookings: bookings,
+ BeneficiariesMap: beneficiariesMap,
+ Stats: stats,
+ WalletBalance: walletBalance,
+ }, nil
+}
+
+func (h *ApplicationHandler) ArchiveSolidarityTransportDriver(ctx context.Context, driverID string) error {
+ // Security check: verify the account exists and is a solidarity transport driver
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ if getResp.Account.Namespace != "solidarity_drivers" {
+ return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace)
+ }
+
+ data, _ := structpb.NewValue(map[string]any{
+ "archived": true,
+ })
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: driverID,
+ Namespace: "solidarity_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ return err
+}
+
+func (h *ApplicationHandler) UnarchiveSolidarityTransportDriver(ctx context.Context, driverID string) error {
+ // Security check: verify the account exists and is a solidarity transport driver
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ if getResp.Account.Namespace != "solidarity_drivers" {
+ return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace)
+ }
+
+ data, _ := structpb.NewValue(map[string]any{
+ "archived": false,
+ })
+
+ request := &mobilityaccounts.UpdateDataRequest{
+ Account: &mobilityaccounts.Account{
+ Id: driverID,
+ Namespace: "solidarity_drivers",
+ Data: data.GetStructValue(),
+ },
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
+ return err
+}
+
+func (h *ApplicationHandler) AddSolidarityTransportDriverDocument(ctx context.Context, driverID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error {
+ // Security check: verify the account exists and is a solidarity transport driver
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ if getResp.Account.Namespace != "solidarity_drivers" {
+ return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace)
+ }
+
+ fileid := uuid.NewString()
+
+ metadata := map[string]string{
+ "type": documentType,
+ "name": documentName,
+ }
+
+ if err := h.filestorage.Put(file, filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS, fmt.Sprintf("%s/%s_%s", driverID, fileid, filename), fileSize, metadata); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (h *ApplicationHandler) GetSolidarityTransportDriverDocument(ctx context.Context, driverID, document string) (io.Reader, *filestorage.FileInfo, error) {
+ return h.GetDocument(ctx, SolidarityDriverDocumentConfig, driverID, document)
+}
+
+func (h *ApplicationHandler) DeleteSolidarityTransportDriverDocument(ctx context.Context, driverID, document string) error {
+ return h.DeleteDocument(ctx, SolidarityDriverDocumentConfig, driverID, document)
+}
+
+func (h *ApplicationHandler) AddSolidarityTransportAvailability(ctx context.Context, driverID, starttime, endtime string, address any, days map[string]bool) error {
+ // Security check: verify the account exists and is a solidarity transport driver
+ getRequest := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+ getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ if getResp.Account.Namespace != "solidarity_drivers" {
+ return fmt.Errorf("account %s is not a solidarity transport driver (namespace: %s)", driverID, getResp.Account.Namespace)
+ }
+
+ availabilities := []*gen.DriverRegularAvailability{}
+
+ // Convert address to JSON string for the GRPC call
+ addressJSON := ""
+ if address != nil {
+ if addressBytes, err := json.Marshal(address); err == nil {
+ addressJSON = string(addressBytes)
+ }
+ }
+
+ for day, enabled := range days {
+ if enabled {
+ dayValue := h.getDayValue(day)
+ a := &gen.DriverRegularAvailability{
+ DriverId: driverID,
+ Day: dayValue,
+ StartTime: starttime,
+ EndTime: endtime,
+ Address: &gen.GeoJsonFeature{
+ Serialized: addressJSON,
+ },
+ }
+ availabilities = append(availabilities, a)
+ }
+ }
+
+ req := &gen.AddDriverRegularAvailabilitiesRequest{
+ Availabilities: availabilities,
+ }
+ _, err = h.services.GRPC.SolidarityTransport.AddDriverRegularAvailabilities(ctx, req)
+ return err
+}
+
+func (h *ApplicationHandler) getDayValue(day string) int32 {
+ switch day {
+ case "sunday":
+ return Sunday
+ case "monday":
+ return Monday
+ case "tuesday":
+ return Tuesday
+ case "wednesday":
+ return Wednesday
+ case "thursday":
+ return Thursday
+ case "friday":
+ return Friday
+ case "saturday":
+ return Saturday
+ default:
+ return Monday
+ }
+}
+
+func (h *ApplicationHandler) DeleteSolidarityTransportAvailability(ctx context.Context, driverID, availabilityID string) error {
+ req := &gen.DeleteDriverRegularAvailabilityRequest{
+ DriverId: driverID,
+ AvailabilityId: availabilityID,
+ }
+ _, err := h.services.GRPC.SolidarityTransport.DeleteDriverRegularAvailability(ctx, req)
+ return err
+}
+
+type SolidarityTransportJourneyDataResult struct {
+ Journey *solidaritytypes.DriverJourney
+ Driver mobilityaccountsstorage.Account
+ Passenger mobilityaccountsstorage.Account
+ Beneficiaries []mobilityaccountsstorage.Account
+ PassengerWalletBalance float64
+ PricingResult map[string]pricing.Price
+}
+
+func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Context, driverID, journeyID, passengerID string, currentUserGroup groupstorage.Group) (*SolidarityTransportJourneyDataResult, error) {
+ // Get journey using the correct API
+ journeyRequest := &gen.GetDriverJourneyRequest{
+ DriverId: driverID,
+ JourneyId: journeyID,
+ }
+
+ journeyResp, err := h.services.GRPC.SolidarityTransport.GetDriverJourney(ctx, journeyRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ // Transform proto to type
+ journey, err := solidaritytransformers.DriverJourneyProtoToType(journeyResp.DriverJourney)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get driver account
+ driverRequest := &mobilityaccounts.GetAccountRequest{
+ Id: driverID,
+ }
+
+ driverResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, driverRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get passenger account
+ var passenger mobilityaccountsstorage.Account
+ if passengerID != "" {
+ passengerResp, err := h.services.GetAccount(passengerID)
+ if err != nil {
+ return nil, fmt.Errorf("could not get passenger account: %w", err)
+ }
+ passenger = passengerResp
+ }
+
+ // Calculate pricing
+ pricingResult, err := h.calculateSolidarityTransportPricing(ctx, journeyResp.DriverJourney, passengerID, passenger)
+ if err != nil {
+ log.Error().Err(err).Msg("error calculating pricing")
+ pricingResult = map[string]pricing.Price{
+ "passenger": {Amount: 0.0, Currency: "EUR"},
+ "driver": {Amount: 0.0, Currency: "EUR"},
+ }
+ }
+
+ // Get beneficiaries in current user's group
+ beneficiaries, err := h.services.GetBeneficiariesInGroup(currentUserGroup)
+ if err != nil {
+ return nil, fmt.Errorf("could not get beneficiaries: %w", err)
+ }
+
+ // Calculate passenger wallet balance like in original handler
+ passengerWalletBalance := h.calculateWalletBalance(passenger)
+
+ return &SolidarityTransportJourneyDataResult{
+ Journey: journey,
+ Driver: driverResp.Account.ToStorageType(),
+ Passenger: passenger,
+ Beneficiaries: beneficiaries,
+ PassengerWalletBalance: passengerWalletBalance,
+ PricingResult: pricingResult,
+ }, nil
+}
+
+func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context.Context, driverID, journeyID, passengerID, motivation, message string, doNotSend bool, returnWaitingTimeMinutes int, replacesBookingID string) (string, error) {
+ // If this is a replacement booking, get the old booking's group_id
+ var groupID string
+ if replacesBookingID != "" {
+ oldBookingResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBooking(ctx, &gen.GetSolidarityTransportBookingRequest{
+ Id: replacesBookingID,
+ })
+ if err != nil {
+ return "", fmt.Errorf("could not get booking to replace: %w", err)
+ }
+ groupID = oldBookingResp.Booking.GroupId
+ }
+
+ // Get journey for pricing calculation
+ journeyRequest := &gen.GetDriverJourneyRequest{
+ DriverId: driverID,
+ JourneyId: journeyID,
+ }
+
+ journeyResp, err := h.services.GRPC.SolidarityTransport.GetDriverJourney(ctx, journeyRequest)
+ if err != nil {
+ return "", err
+ }
+
+ // Get passenger account for pricing
+ var passenger mobilityaccountsstorage.Account
+ if passengerID != "" {
+ passengerResp, err := h.services.GetAccount(passengerID)
+ if err != nil {
+ return "", fmt.Errorf("could not get passenger account: %w", err)
+ }
+ passenger = passengerResp
+ }
+
+ // Calculate pricing
+ pricingResult, err := h.calculateSolidarityTransportPricing(ctx, journeyResp.DriverJourney, passengerID, passenger)
+ priceAmount := float64(0)
+ driverCompensation := float64(0)
+ if err == nil {
+ priceAmount = pricingResult["passenger"].Amount
+ driverCompensation = pricingResult["driver"].Amount
+ }
+
+ // Convert return waiting time from minutes to nanoseconds (time.Duration is in nanoseconds)
+ returnWaitingDuration := int64(returnWaitingTimeMinutes) * int64(time.Minute)
+
+ // Create booking request
+ dataFields := map[string]*structpb.Value{
+ "motivation": structpb.NewStringValue(motivation),
+ "message": structpb.NewStringValue(message),
+ "do_not_send": structpb.NewBoolValue(doNotSend),
+ }
+
+ bookingRequest := &gen.BookDriverJourneyRequest{
+ PassengerId: passengerID,
+ DriverId: driverID,
+ DriverJourneyId: journeyID,
+ ReturnWaitingDuration: returnWaitingDuration,
+ PriceAmount: priceAmount,
+ PriceCurrency: "EUR",
+ DriverCompensationAmount: driverCompensation,
+ DriverCompensationCurrency: "EUR",
+ Data: &structpb.Struct{
+ Fields: dataFields,
+ },
+ }
+
+ // Set group_id if this is a replacement booking
+ if groupID != "" {
+ bookingRequest.GroupId = &groupID
+ }
+
+ resp, err := h.services.GRPC.SolidarityTransport.BookDriverJourney(ctx, bookingRequest)
+ if err != nil {
+ return "", err
+ }
+
+ // If this is a replacement booking, update the old booking to mark it as replaced
+ if replacesBookingID != "" {
+ oldBookingResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBooking(ctx, &gen.GetSolidarityTransportBookingRequest{
+ Id: replacesBookingID,
+ })
+ if err == nil {
+ oldBooking, err := solidaritytransformers.BookingProtoToType(oldBookingResp.Booking)
+ if err == nil && oldBooking != nil {
+ if oldBooking.Data == nil {
+ oldBooking.Data = make(map[string]any)
+ }
+ oldBooking.Data["replaced_by"] = resp.Booking.Id
+
+ // Update the booking
+ oldBookingProto, _ := solidaritytransformers.BookingTypeToProto(oldBooking)
+ _, err = h.services.GRPC.SolidarityTransport.UpdateSolidarityTransportBooking(ctx, &gen.UpdateSolidarityTransportBookingRequest{
+ Booking: oldBookingProto,
+ })
+ if err != nil {
+ log.Error().Err(err).Str("old_booking_id", replacesBookingID).Str("new_booking_id", resp.Booking.Id).Msg("failed to mark old booking as replaced")
+ }
+ }
+ }
+ }
+
+ // Send SMS if not disabled
+ if !doNotSend && message != "" {
+ send_message := strings.ReplaceAll(message, "{booking_id}", resp.Booking.Id)
+ if err := h.GenerateSMS(driverID, send_message); err != nil {
+ log.Error().Err(err).Msg("failed to send SMS")
+ }
+ }
+
+ return resp.Booking.Id, nil
+}
+
+func (h *ApplicationHandler) ToggleSolidarityTransportJourneyNoreturn(ctx context.Context, driverID, journeyID string) error {
+ // Toggle noreturn status
+ updateRequest := &gen.ToggleSolidarityTransportNoreturnRequest{
+ JourneyId: journeyID,
+ }
+
+ _, err := h.services.GRPC.SolidarityTransport.ToggleSolidarityTransportNoreturn(ctx, updateRequest)
+ return err
+}
+
+type SolidarityTransportBookingDataResult struct {
+ Booking *solidaritytypes.Booking
+ Driver mobilityaccountsstorage.Account
+ Passenger mobilityaccountsstorage.Account
+ Journey *solidaritytypes.DriverJourney
+ PassengerWalletBalance float64
+}
+
+func (h *ApplicationHandler) GetSolidarityTransportBookingData(ctx context.Context, bookingID string) (*SolidarityTransportBookingDataResult, error) {
+ // Get booking
+ bookingRequest := &gen.GetSolidarityTransportBookingRequest{
+ Id: bookingID,
+ }
+
+ bookingResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBooking(ctx, bookingRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ booking := bookingResp.Booking
+
+ // Get driver account
+ driverRequest := &mobilityaccounts.GetAccountRequest{
+ Id: booking.DriverId,
+ }
+
+ driverResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, driverRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get passenger account
+ passengerRequest := &mobilityaccounts.GetAccountRequest{
+ Id: booking.PassengerId,
+ }
+
+ passengerResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, passengerRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ // Transform booking proto to type
+ bookingType, err := solidaritytransformers.BookingProtoToType(booking)
+ if err != nil {
+ return nil, err
+ }
+
+ // Calculate passenger wallet balance like in original handler
+ passengerWalletBalance := h.calculateWalletBalance(passengerResp.Account.ToStorageType())
+
+ return &SolidarityTransportBookingDataResult{
+ Booking: bookingType,
+ Driver: driverResp.Account.ToStorageType(),
+ Passenger: passengerResp.Account.ToStorageType(),
+ Journey: bookingType.Journey,
+ PassengerWalletBalance: passengerWalletBalance,
+ }, nil
+}
+
+func (h *ApplicationHandler) solidarityDrivers(searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) {
+ request := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: []string{"solidarity_drivers"},
+ }
+
+ resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create iterator that filters and transforms accounts
+ filteredAccounts := func(yield func(mobilityaccountsstorage.Account) bool) {
+ for _, account := range resp.Accounts {
+ if h.filterSolidarityDriver(account, searchFilter, archivedFilter) {
+ if !yield(account.ToStorageType()) {
+ return
+ }
+ }
+ }
+ }
+
+ return slices.Collect(filteredAccounts), nil
+}
+
+func (h *ApplicationHandler) filterSolidarityDriver(a *mobilityaccounts.Account, searchFilter string, archivedFilter bool) bool {
+ // Search filter
+ if searchFilter != "" {
+ name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
+ if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter)) {
+ return false
+ }
+ }
+
+ // Archived filter
+ if archivedFilter {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return true
+ }
+ return false
+ } else {
+ if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (h *ApplicationHandler) pricingGeography(loc *geojson.Feature) pricing.GeographyParams {
+ if loc == nil {
+ return pricing.GeographyParams{}
+ }
+
+ geo, err := h.services.Geography.GeoSearch(loc)
+ if err != nil {
+ log.Error().Err(err).Msg("issue in geosearch")
+ return pricing.GeographyParams{}
+ }
+ return pricing.GeographyParams{
+ Location: loc,
+ CityCode: geo["communes"].Properties.MustString("code"),
+ IntercommunalityCode: geo["epci"].Properties.MustString("code"),
+ RegionCode: geo["regions"].Properties.MustString("code"),
+ DepartmentCode: geo["departements"].Properties.MustString("code"),
+ }
+}
+
+// CalculateSolidarityTransportPricing is the exported wrapper for calculateSolidarityTransportPricing
+func (h *ApplicationHandler) CalculateSolidarityTransportPricing(ctx context.Context, journey *gen.SolidarityTransportDriverJourney, passengerID string) (map[string]pricing.Price, error) {
+ // Get passenger account
+ var passenger mobilityaccountsstorage.Account
+ if passengerID != "" {
+ passengerResp, err := h.services.GetAccount(passengerID)
+ if err != nil {
+ return nil, fmt.Errorf("could not get passenger account: %w", err)
+ }
+ passenger = passengerResp
+ }
+ return h.calculateSolidarityTransportPricing(ctx, journey, passengerID, passenger)
+}
+
+func (h *ApplicationHandler) calculateSolidarityTransportPricing(ctx context.Context, journey *gen.SolidarityTransportDriverJourney, passengerID string, passenger mobilityaccountsstorage.Account) (map[string]pricing.Price, error) {
+ // Transform proto to type for geography access
+ journeyType, err := solidaritytransformers.DriverJourneyProtoToType(journey)
+ if err != nil {
+ return nil, err
+ }
+
+ benefParams := pricing.BeneficiaryParams{}
+ if passengerID == "" {
+ benefParams = pricing.BeneficiaryParams{
+ Address: h.pricingGeography(journeyType.PassengerPickup),
+ History: 99,
+ Priority: false,
+ }
+ } else {
+ // Get solidarity transport history for passenger
+ solidarity, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{
+ Passengerid: passengerID,
+ StartDate: timestamppb.New(time.Now().Add(-12 * 730 * time.Hour)),
+ EndDate: timestamppb.New(time.Now().Add(12 * 730 * time.Hour)),
+ })
+
+ priority := false
+ if a, ok := passenger.Data["other_properties"]; ok {
+ if b, ok := a.(map[string]any); ok {
+ if c, ok := b["status"]; ok {
+ if p, ok := c.(string); ok {
+ priority = (p == "Prioritaire")
+ }
+ }
+ }
+ }
+
+ history := 0
+ if op, ok := passenger.Data["other_properties"]; ok {
+ if op_map, ok := op.(map[string]any); ok {
+ if pst, ok := op_map["previous_solidarity_transport"]; ok {
+ if pst_str, ok := pst.(string); ok {
+ if pst_str != "" {
+ if n, err := strconv.Atoi(pst_str); err == nil {
+ history = history + n
+ } else {
+ log.Error().Err(err).Str("n", pst_str).Msg("string to int conversion error")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if err == nil {
+ history = history + len(solidarity.Bookings)
+ }
+
+ var passengerGeo pricing.GeographyParams
+ if pa, ok := passenger.Data["address"]; ok {
+ jsonpa, err := json.Marshal(pa)
+ if err == nil {
+ passGeojson, err := geojson.UnmarshalFeature(jsonpa)
+ if err == nil {
+ passengerGeo = h.pricingGeography(passGeojson)
+ }
+ }
+ }
+
+ benefParams = pricing.BeneficiaryParams{
+ Address: passengerGeo,
+ History: history,
+ Priority: priority,
+ }
+ }
+
+ pricingResult, err := h.services.Pricing.Prices(pricing.PricingParams{
+ MobilityType: "solidarity_transport",
+ Beneficiary: benefParams,
+ SharedMobility: pricing.SharedMobilityParams{
+ DriverDistance: journey.DriverDistance,
+ PassengerDistance: journey.PassengerDistance,
+ Departure: h.pricingGeography(journeyType.PassengerPickup),
+ Destination: h.pricingGeography(journeyType.PassengerDrop),
+ OutwardOnly: journey.Noreturn,
+ },
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("error in pricing calculation")
+ return nil, err
+ }
+
+ return pricingResult, nil
+}
+
+func (h *ApplicationHandler) UpdateSolidarityTransportBookingStatus(ctx context.Context, bookingID, action, reason, message string, notify bool) error {
+ var status string
+ switch action {
+ case "confirm":
+ status = "VALIDATED"
+ case "cancel":
+ status = "CANCELLED"
+ case "waitconfirmation":
+ status = "WAITING_CONFIRMATION"
+ default:
+ return fmt.Errorf("invalid action: %s", action)
+ }
+
+ // Get booking details BEFORE updating to capture previous status
+ result, err := h.GetSolidarityTransportBookingData(ctx, bookingID)
+ if err != nil {
+ return err
+ }
+
+ booking := result.Booking
+ driver := result.Driver
+ passenger := result.Passenger
+ previousStatus := booking.Status
+
+ // Update booking status
+ _, err = h.services.GRPC.SolidarityTransport.UpdateSolidarityTransportBookingStatus(ctx, &gen.UpdateSolidarityTransportBookingStatusRequest{
+ BookingId: bookingID,
+ NewStatus: status,
+ Reason: reason,
+ })
+ if err != nil {
+ return fmt.Errorf("update booking status issue: %w", err)
+ }
+
+ // Handle wallet operations based on status transitions
+ // Credit driver / debit passenger when previous status was not VALIDATED and new status is VALIDATED
+ if previousStatus != "VALIDATED" && status == "VALIDATED" {
+ if message != "" {
+ send_message := strings.ReplaceAll(message, "{booking_id}", bookingID)
+ h.GenerateSMS(passenger.ID, send_message)
+ }
+ if err := h.CreditWallet(ctx, passenger.ID, -1*booking.Journey.Price.Amount, "Transport solidaire", "Débit transport solidaire"); err != nil {
+ return fmt.Errorf("could not debit passenger wallet: %w", err)
+ }
+ if err := h.CreditWallet(ctx, driver.ID, booking.DriverCompensationAmount, "Transport solidaire", "Crédit transport solidaire"); err != nil {
+ return fmt.Errorf("could not credit driver wallet: %w", err)
+ }
+ }
+
+ // Credit passenger / debit driver when previous status was VALIDATED and new status is not VALIDATED anymore
+ if previousStatus == "VALIDATED" && status != "VALIDATED" {
+ if err := h.CreditWallet(ctx, passenger.ID, booking.Journey.Price.Amount, "Transport solidaire", "Remboursement annulation transport solidaire"); err != nil {
+ return fmt.Errorf("could not credit passenger wallet: %w", err)
+ }
+ if err := h.CreditWallet(ctx, driver.ID, -1*booking.DriverCompensationAmount, "Transport solidaire", "Débit annulation transport solidaire"); err != nil {
+ return fmt.Errorf("could not debit driver wallet: %w", err)
+ }
+ }
+
+ // Handle notifications for cancelled status
+ if status == "CANCELLED" && notify {
+ // NOTIFY GROUP MEMBERS
+ groupsrequest := &groupsmanagement.GetGroupsRequest{
+ Namespaces: []string{"parcoursmob_organizations"},
+ Member: booking.PassengerId,
+ }
+
+ groupsresp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, groupsrequest)
+ if err != nil {
+ log.Error().Err(err).Msg("")
+ return nil // Don't fail the whole operation for notification issues
+ }
+
+ if len(groupsresp.Groups) > 0 {
+ members, _, err := h.groupmembers(groupsresp.Groups[0].Id)
+ if err != nil {
+ log.Error().Err(err).Msg("could not retrieve groupe members")
+ } else {
+ for _, m := range members {
+ if email, ok := m.Data["email"].(string); ok {
+ h.emailing.Send("solidarity_transport.booking_driver_decline", email, map[string]string{
+ "bookingid": booking.Id,
+ "baseUrl": h.config.GetString("base_url"),
+ })
+ }
+ }
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/core/application/support.go b/core/application/support.go
new file mode 100755
index 0000000..4c9f7d9
--- /dev/null
+++ b/core/application/support.go
@@ -0,0 +1,33 @@
+package application
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/rs/zerolog/log"
+)
+
+type Message struct {
+ Content string
+}
+
+func (h *ApplicationHandler) SendSupportMessage(ctx context.Context, comment, userEmail string) error {
+ data := map[string]any{
+ "key": comment,
+ "user": userEmail,
+ }
+
+ supportEmail := h.config.GetString("modules.support.email")
+ if supportEmail == "" {
+ supportEmail = "support@mobicoop.fr"
+ }
+
+ log.Debug().Str("user_email", userEmail).Str("support_email", supportEmail).Msg("Sending support message")
+
+ if err := h.emailing.Send("support.request", supportEmail, data); err != nil {
+ return fmt.Errorf("failed to send support email: %w", err)
+ }
+
+ return nil
+}
+
diff --git a/core/application/vehicles-management.go b/core/application/vehicles-management.go
new file mode 100755
index 0000000..d5b7ef8
--- /dev/null
+++ b/core/application/vehicles-management.go
@@ -0,0 +1,598 @@
+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 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
+}
+
+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
+}
+
diff --git a/core/application/vehicles.go b/core/application/vehicles.go
new file mode 100755
index 0000000..a5a64b7
--- /dev/null
+++ b/core/application/vehicles.go
@@ -0,0 +1,375 @@
+package application
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "slices"
+ "sort"
+ "strings"
+ "time"
+
+ "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"
+ "git.coopgo.io/coopgo-platform/fleets/storage"
+ groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
+ groupsmanagementstorage "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/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/protobuf/types/known/structpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type VehiclesSearchResult struct {
+ Vehicles []storage.Vehicle
+ Beneficiary mobilityaccountsstorage.Account
+ BeneficiaryDocuments []filestorage.FileInfo
+ Groups map[string]any
+ Searched bool
+ StartDate time.Time
+ EndDate time.Time
+ VehicleType string
+ Automatic bool
+ MandatoryDocuments []string
+ FileTypesMap map[string]string
+ VehicleTypes []string
+ Beneficiaries []mobilityaccountsstorage.Account
+}
+
+func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID string, startdate, enddate time.Time, vehicleType string, automatic bool) (*VehiclesSearchResult, error) {
+ var beneficiary mobilityaccountsstorage.Account
+ beneficiarydocuments := []filestorage.FileInfo{}
+ vehicles := []storage.Vehicle{}
+ searched := false
+ administrators := []string{}
+
+ if beneficiaryID != "" && startdate.After(time.Now().Add(-24*time.Hour)) && enddate.After(startdate) {
+ // Handler form
+ searched = true
+
+ requestbeneficiary := &mobilityaccounts.GetAccountRequest{
+ Id: beneficiaryID,
+ }
+
+ respbeneficiary, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, requestbeneficiary)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get beneficiary: %w", err)
+ }
+
+ beneficiary = respbeneficiary.Account.ToStorageType()
+
+ request := &fleets.GetVehiclesRequest{
+ Namespaces: []string{"parcoursmob"},
+ AvailabilityFrom: timestamppb.New(startdate),
+ AvailabilityTo: timestamppb.New(enddate),
+ }
+
+ if vehicleType != "" {
+ request.Types = []string{vehicleType}
+ }
+
+ resp, err := h.services.GRPC.Fleets.GetVehicles(ctx, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get vehicles: %w", err)
+ }
+
+ for _, vehicle := range resp.Vehicles {
+ v := vehicle.ToStorageType()
+
+ if vehicleType == "Voiture" && automatic {
+ if auto, ok := v.Data["automatic"].(bool); !ok || !auto {
+ continue
+ }
+ }
+
+ adminfound := false
+ for _, a := range administrators {
+ if a == v.Administrators[0] {
+ adminfound = true
+ break
+ }
+ }
+ if !adminfound {
+ administrators = append(administrators, v.Administrators[0])
+ }
+
+ vehicles = append(vehicles, v)
+ }
+
+ // Sort vehicles if beneficiary address is set
+ if beneficiaryAddress, ok := beneficiary.Data["address"]; ok {
+ beneficiaryAddressJson, err := json.Marshal(beneficiaryAddress)
+ if err == nil {
+ beneficiaryAddressGeojson, err := geojson.UnmarshalFeature(beneficiaryAddressJson)
+ if err == nil {
+ slices.SortFunc(vehicles, sorting.VehiclesByDistanceFrom(*beneficiaryAddressGeojson))
+ } else {
+ log.Error().Err(err).Msg("error transforming beneficiary address to GeoJSON")
+ }
+ } else {
+ log.Error().Err(err).Msg("error transforming beneficiary address to JSON")
+ }
+ }
+
+ beneficiarydocuments = h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiary.ID)
+ }
+
+ accounts, err := h.services.GetBeneficiariesMap()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get beneficiaries: %w", err)
+ }
+
+ // Convert map to slice for compatibility
+ beneficiaries := make([]mobilityaccountsstorage.Account, 0, len(accounts))
+ for _, account := range accounts {
+ beneficiaries = append(beneficiaries, account)
+ }
+
+ groups := map[string]any{}
+ if len(administrators) > 0 {
+ admingroups, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, &groupsmanagement.GetGroupsBatchRequest{
+ Groupids: administrators,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to get admin groups: %w", err)
+ }
+
+ for _, g := range admingroups.Groups {
+ groups[g.Id] = g.ToStorageType()
+ }
+ }
+
+ sort.Sort(sorting.BeneficiariesByName(beneficiaries))
+
+ mandatoryDocuments := h.config.GetStringSlice("modules.fleets.booking_documents.mandatory")
+ fileTypesMap := h.config.GetStringMapString("storage.files.file_types")
+ vehicleTypes := h.config.GetStringSlice("modules.fleets.vehicle_types")
+
+ return &VehiclesSearchResult{
+ Vehicles: vehicles,
+ Beneficiary: beneficiary,
+ BeneficiaryDocuments: beneficiarydocuments,
+ Groups: groups,
+ Searched: searched,
+ StartDate: startdate,
+ EndDate: enddate,
+ VehicleType: vehicleType,
+ Automatic: automatic,
+ MandatoryDocuments: mandatoryDocuments,
+ FileTypesMap: fileTypesMap,
+ VehicleTypes: vehicleTypes,
+ Beneficiaries: beneficiaries,
+ }, nil
+}
+
+type BookVehicleResult struct {
+ BookingID string
+}
+
+func (h *ApplicationHandler) BookVehicle(ctx context.Context, vehicleID, beneficiaryID string, startdate, enddate time.Time, documents map[string]io.Reader, documentHeaders map[string]string, existingDocs map[string]string, currentUserID string, currentUserClaims map[string]any, currentGroup any) (*BookVehicleResult, error) {
+ group := currentGroup.(groupsmanagementstorage.Group)
+
+ vehicle, err := h.services.GRPC.Fleets.GetVehicle(ctx, &fleets.GetVehicleRequest{
+ Vehicleid: vehicleID,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("vehicle not found: %w", err)
+ }
+
+ data := map[string]any{
+ "booked_by": map[string]any{
+ "user": map[string]any{
+ "id": currentUserID,
+ "display_name": fmt.Sprintf("%s %s", currentUserClaims["first_name"], currentUserClaims["last_name"]),
+ },
+ "group": map[string]any{
+ "id": group.ID,
+ "name": group.Data["name"],
+ },
+ },
+ }
+ datapb, err := structpb.NewStruct(data)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create booking metadata: %w", err)
+ }
+
+ bookingID := uuid.NewString()
+ booking := &fleets.Booking{
+ Id: bookingID,
+ Vehicleid: vehicleID,
+ Driver: beneficiaryID,
+ Startdate: timestamppb.New(startdate),
+ Enddate: timestamppb.New(enddate),
+ Unavailablefrom: timestamppb.New(startdate),
+ Unavailableto: timestamppb.New(enddate.Add(72 * time.Hour)),
+ Data: datapb,
+ }
+
+ request := &fleets.CreateBookingRequest{
+ Booking: booking,
+ }
+
+ // Handle document uploads
+ for docType, file := range documents {
+ fileid := uuid.NewString()
+ filename := documentHeaders[docType]
+
+ metadata := map[string]string{
+ "type": docType,
+ "name": filename,
+ }
+
+ if err := h.filestorage.Put(file, filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s_%s", bookingID, fileid, filename), -1, metadata); err != nil {
+ return nil, fmt.Errorf("failed to upload document %s: %w", docType, err)
+ }
+ }
+
+ // Handle existing documents
+ for docType, existingFile := range existingDocs {
+ path := strings.Split(existingFile, "/")
+ if err := h.filestorage.Copy(existingFile, fmt.Sprintf("%s/%s/%s", filestorage.PREFIX_BOOKINGS, bookingID, path[len(path)-1])); err != nil {
+ return nil, fmt.Errorf("failed to copy existing document %s: %w", docType, err)
+ }
+ }
+
+ _, err = h.services.GRPC.Fleets.CreateBooking(ctx, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create booking: %w", err)
+ }
+
+ // NOTIFY GROUP MEMBERS
+ members, _, err := h.groupmembers(vehicle.Vehicle.Administrators[0])
+ if err != nil {
+ log.Error().Err(err).Msg("failed to get group members for notification")
+ } else {
+ for _, m := range members {
+ if email, ok := m.Data["email"].(string); ok {
+ h.emailing.Send("fleets.bookings.creation_admin_alert", email, map[string]string{
+ "bookingid": bookingID,
+ })
+ }
+ }
+ }
+
+ return &BookVehicleResult{
+ BookingID: bookingID,
+ }, nil
+}
+
+
+
+
+type VehicleBookingDetailsResult struct {
+ Booking storage.Booking
+ Vehicle storage.Vehicle
+ Beneficiary mobilityaccountsstorage.Account
+ Group groupsmanagementstorage.Group
+ Documents []filestorage.FileInfo
+ FileTypesMap map[string]string
+}
+
+func (h *ApplicationHandler) GetVehicleBookingDetails(ctx context.Context, bookingID string) (*VehicleBookingDetailsResult, error) {
+ request := &fleets.GetBookingRequest{
+ Bookingid: bookingID,
+ }
+ resp, err := h.services.GRPC.Fleets.GetBooking(ctx, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get booking: %w", err)
+ }
+
+ booking := resp.Booking.ToStorageType()
+
+ beneficiaryrequest := &mobilityaccounts.GetAccountRequest{
+ Id: booking.Driver,
+ }
+
+ beneficiaryresp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, beneficiaryrequest)
+ if err != nil {
+ beneficiaryresp = &mobilityaccounts.GetAccountResponse{
+ Account: &mobilityaccounts.Account{},
+ }
+ }
+
+ 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)
+ }
+
+ documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingID)
+ fileTypesMap := h.config.GetStringMapString("storage.files.file_types")
+
+ return &VehicleBookingDetailsResult{
+ Booking: booking,
+ Vehicle: booking.Vehicle,
+ Beneficiary: beneficiaryresp.Account.ToStorageType(),
+ Group: groupresp.Group.ToStorageType(),
+ Documents: documents,
+ FileTypesMap: fileTypesMap,
+ }, nil
+}
+
+type VehicleBookingsListResult struct {
+ Bookings []storage.Booking
+ VehiclesMap map[string]storage.Vehicle
+ GroupsMap map[string]groupsmanagementstorage.Group
+}
+
+func (h *ApplicationHandler) GetVehicleBookingsList(ctx context.Context, groupID string) (*VehicleBookingsListResult, error) {
+ request := &fleets.GetBookingsRequest{}
+ resp, err := h.services.GRPC.Fleets.GetBookings(ctx, request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get bookings: %w", err)
+ }
+
+ bookings := []storage.Booking{}
+
+ for _, b := range resp.Bookings {
+ booking := b.ToStorageType()
+ if b1, ok := booking.Data["booked_by"].(map[string]any); ok {
+ if b2, ok := b1["group"].(map[string]any); ok {
+ if b2["id"] == groupID {
+ bookings = append(bookings, booking)
+ }
+ }
+ }
+ }
+
+ vehiclesMap, err := h.services.GetVehiclesMap()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get vehicles map: %w", err)
+ }
+
+ groupsMap, err := h.services.GetGroupsMap()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get groups map: %w", err)
+ }
+
+ return &VehicleBookingsListResult{
+ Bookings: bookings,
+ VehiclesMap: vehiclesMap,
+ GroupsMap: groupsMap,
+ }, nil
+}
+
+func (h *ApplicationHandler) GetBookingDocument(ctx context.Context, bookingID, document string) (io.Reader, string, error) {
+ file, info, err := h.filestorage.Get(filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s", bookingID, document))
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to get document: %w", err)
+ }
+
+ return file, info.ContentType, nil
+}
+
+// Helper method to expose config to web handlers
+func (h *ApplicationHandler) GetConfig() interface{} {
+ return h.config
+}
diff --git a/core/application/wallets.go b/core/application/wallets.go
new file mode 100644
index 0000000..f7045b4
--- /dev/null
+++ b/core/application/wallets.go
@@ -0,0 +1,146 @@
+package application
+
+import (
+ "context"
+ "time"
+
+ "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *ApplicationHandler) CreditWallet(ctx context.Context, userid string, amount float64, paymentMethod string, description string) error {
+ account, err := h.services.GetAccount(userid)
+ if err != nil {
+ log.Error().Err(err).Msg("could not retrieve account")
+ return err
+ }
+
+ // Initialize wallet if it doesn't exist
+ if account.Data["wallet"] == nil {
+ account.Data["wallet"] = float64(0)
+ }
+
+ // Initialize wallet history if it doesn't exist
+ if account.Data["wallet_history"] == nil {
+ account.Data["wallet_history"] = []map[string]interface{}{}
+ }
+
+ // Determine operation type based on amount sign
+ operationType := "credit"
+ if amount < 0 {
+ operationType = "debit"
+ }
+
+ // Create wallet operation record
+ operation := map[string]interface{}{
+ "timestamp": time.Now().Format(time.RFC3339),
+ "amount": amount,
+ "payment_method": paymentMethod,
+ "description": description,
+ "operation_type": operationType,
+ }
+
+ // Add operation to history
+ var history []map[string]interface{}
+ if existingHistory, ok := account.Data["wallet_history"].([]interface{}); ok {
+ // Convert []interface{} to []map[string]interface{}
+ for _, item := range existingHistory {
+ if historyItem, ok := item.(map[string]interface{}); ok {
+ history = append(history, historyItem)
+ }
+ }
+ } else if existingHistory, ok := account.Data["wallet_history"].([]map[string]interface{}); ok {
+ history = existingHistory
+ }
+
+ history = append(history, operation)
+ account.Data["wallet_history"] = history
+
+ log.Debug().
+ Str("userid", userid).
+ Float64("amount", amount).
+ Str("paymentMethod", paymentMethod).
+ Str("description", description).
+ Int("historyCount", len(history)).
+ Msg("Adding operation to wallet history")
+
+ // Note: wallet balance is NOT updated here - it remains as initial amount
+ // Balance is calculated from initial amount + sum of all operations
+
+ accountproto, err := grpcapi.AccountFromStorageType(&account)
+ if err != nil {
+ log.Error().Err(err).Msg("account type transformation issue")
+ return err
+ }
+
+ _, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &grpcapi.UpdateDataRequest{
+ Account: accountproto,
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("account update issue")
+ return err
+ }
+
+ log.Info().
+ Str("userid", userid).
+ Float64("amount", amount).
+ Str("payment_method", paymentMethod).
+ Str("description", description).
+ Msg("Wallet credited successfully")
+
+ return nil
+}
+
+// calculateWalletBalance calculates the current wallet balance from initial amount + all operations
+func (h *ApplicationHandler) calculateWalletBalance(account mobilityaccountsstorage.Account) float64 {
+ // Return 0 if account data is nil
+ if account.Data == nil {
+ log.Debug().Msg("calculateWalletBalance: account.Data is nil, returning 0")
+ return float64(0)
+ }
+
+ // Get initial wallet amount (default to 0 if not set)
+ initialAmount := float64(0)
+ if walletValue, exists := account.Data["wallet"]; exists && walletValue != nil {
+ if val, ok := walletValue.(float64); ok {
+ initialAmount = val
+ }
+ }
+
+ // Calculate total from all operations
+ operationsTotal := float64(0)
+ operationCount := 0
+ if historyValue, exists := account.Data["wallet_history"]; exists && historyValue != nil {
+ var operations []map[string]interface{}
+
+ // Handle both []interface{} and []map[string]interface{} types
+ if history, ok := historyValue.([]interface{}); ok {
+ for _, item := range history {
+ if operation, ok := item.(map[string]interface{}); ok {
+ operations = append(operations, operation)
+ }
+ }
+ } else if history, ok := historyValue.([]map[string]interface{}); ok {
+ operations = history
+ }
+
+ for _, operation := range operations {
+ if amount, ok := operation["amount"].(float64); ok {
+ operationsTotal += amount
+ operationCount++
+ }
+ }
+ }
+
+ result := initialAmount + operationsTotal
+ log.Debug().
+ Str("accountId", account.ID).
+ Float64("initialAmount", initialAmount).
+ Float64("operationsTotal", operationsTotal).
+ Int("operationCount", operationCount).
+ Float64("result", result).
+ Msg("calculateWalletBalance")
+
+ return result
+}
diff --git a/core/utils/cache/cache.go b/core/utils/cache/cache.go
new file mode 100644
index 0000000..7922254
--- /dev/null
+++ b/core/utils/cache/cache.go
@@ -0,0 +1,74 @@
+package cache
+
+import (
+ "encoding/json"
+ "strconv"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "github.com/rs/zerolog/log"
+)
+
+type CacheService struct {
+ cache storage.CacheHandler
+}
+
+func NewCacheService(cache storage.CacheHandler) *CacheService {
+ return &CacheService{cache: cache}
+}
+
+type GetCacheResult struct {
+ Data []byte
+}
+
+func (s *CacheService) GetCacheData(cacheID string, limitsMin, limitsMax *int) (*GetCacheResult, error) {
+ d, err := s.cache.Get(cacheID)
+ if err != nil {
+ log.Error().Err(err).Msg("")
+ return nil, err
+ }
+
+ var data []any
+ if val, ok := d.([]any); ok {
+ data = val
+ } else {
+ data = []any{d}
+ }
+
+ result := data
+ if limitsMin != nil {
+ min := *limitsMin
+ if limitsMax != nil {
+ max := *limitsMax
+ if max > len(data) {
+ result = data[min:]
+ } else {
+ result = data[min:max]
+ }
+ } else {
+ result = data[min:]
+ }
+ }
+
+ j, err := json.Marshal(result)
+ if err != nil {
+ return nil, err
+ }
+
+ return &GetCacheResult{
+ Data: j,
+ }, nil
+}
+
+func ParseLimits(limitsMinStr, limitsMaxStr string) (limitsMin, limitsMax *int) {
+ if limitsMinStr != "" {
+ if min, err := strconv.Atoi(limitsMinStr); err == nil {
+ limitsMin = &min
+ }
+ }
+ if limitsMaxStr != "" {
+ if max, err := strconv.Atoi(limitsMaxStr); err == nil {
+ limitsMax = &max
+ }
+ }
+ return
+}
\ No newline at end of file
diff --git a/utils/form-validators/form-validators.go b/core/utils/form-validators/form-validators.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/form-validators/form-validators.go
rename to core/utils/form-validators/form-validators.go
diff --git a/utils/form-validators/phone-numbers.go b/core/utils/form-validators/phone-numbers.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/form-validators/phone-numbers.go
rename to core/utils/form-validators/phone-numbers.go
diff --git a/core/utils/gender/gender.go b/core/utils/gender/gender.go
new file mode 100644
index 0000000..202d9f4
--- /dev/null
+++ b/core/utils/gender/gender.go
@@ -0,0 +1,17 @@
+package gender
+
+// ISO5218ToString converts ISO 5218 gender codes to French text labels
+func ISO5218ToString(value string) string {
+ switch value {
+ case "0":
+ return "Inconnu"
+ case "1":
+ return "Masculin"
+ case "2":
+ return "Féminin"
+ case "9":
+ return "Sans objet"
+ default:
+ return value
+ }
+}
diff --git a/core/utils/geo/geo.go b/core/utils/geo/geo.go
new file mode 100644
index 0000000..922a1a1
--- /dev/null
+++ b/core/utils/geo/geo.go
@@ -0,0 +1,45 @@
+package geo
+
+import (
+ "io"
+ "net/http"
+
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+)
+
+type GeoService struct {
+ geoType string
+ baseURL string
+ autocompleteURL string
+}
+
+func NewGeoService(geoType, baseURL, autocompleteEndpoint string) *GeoService {
+ return &GeoService{
+ geoType: geoType,
+ baseURL: baseURL,
+ autocompleteURL: baseURL + autocompleteEndpoint,
+ }
+}
+
+func (s *GeoService) Autocomplete(text string) (*geojson.FeatureCollection, error) {
+ resp, err := http.Get(s.autocompleteURL + text)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to read response body")
+ return nil, err
+ }
+
+ featureCollection, err := geojson.UnmarshalFeatureCollection(body)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to unmarshal feature collection")
+ return nil, err
+ }
+
+ return featureCollection, nil
+}
\ No newline at end of file
diff --git a/utils/icons/svg-icons.go b/core/utils/icons/svg-icons.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/icons/svg-icons.go
rename to core/utils/icons/svg-icons.go
diff --git a/utils/identification/groups.go b/core/utils/identification/groups.go
old mode 100644
new mode 100755
similarity index 96%
rename from utils/identification/groups.go
rename to core/utils/identification/groups.go
index 52c19e5..04f75cc
--- a/utils/identification/groups.go
+++ b/core/utils/identification/groups.go
@@ -6,6 +6,7 @@ import (
"net/http"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
+ "github.com/rs/zerolog/log"
)
const GroupKey ContextKey = "group"
@@ -19,7 +20,6 @@ func (p *IdentificationProvider) GroupsMiddleware(next http.Handler) http.Handle
o, ok := session.Values["organization"]
if !ok || o == nil {
- fmt.Println("no organization")
http.Redirect(w, r, "/auth/groups/", http.StatusFound)
return
}
@@ -29,7 +29,7 @@ func (p *IdentificationProvider) GroupsMiddleware(next http.Handler) http.Handle
claimgroups, ok := claims["groups"].([]any)
if !ok {
- fmt.Println("cast issue")
+ log.Error().Msg("cast issue")
w.WriteHeader(http.StatusInternalServerError)
return
}
diff --git a/utils/identification/oidc.go b/core/utils/identification/oidc.go
old mode 100644
new mode 100755
similarity index 73%
rename from utils/identification/oidc.go
rename to core/utils/identification/oidc.go
index 488546b..8d2be67
--- a/utils/identification/oidc.go
+++ b/core/utils/identification/oidc.go
@@ -4,14 +4,14 @@ import (
"context"
"crypto/rand"
"encoding/base64"
- "fmt"
"io"
"net/http"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
- "github.com/coreos/go-oidc"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/sessions"
+ "github.com/rs/zerolog/log"
"github.com/spf13/viper"
"golang.org/x/oauth2"
)
@@ -40,7 +40,27 @@ func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHand
provider, err := oidc.NewProvider(context.Background(), providerURL)
if err != nil {
- return nil, err
+ var (
+ issuerUrl = cfg.GetString("identification.oidc.provider_config.issuer_url")
+ authUrl = cfg.GetString("identification.oidc.provider_config.auth_url")
+ tokenUrl = cfg.GetString("identification.oidc.provider_config.token_url")
+ userInfoUrl = cfg.GetString("identification.oidc.provider_config.user_info_url")
+ jwksUrl = cfg.GetString("identification.oidc.provider_config.jwks_url")
+ algorithms = []string{"RS256"}
+ )
+ if issuerUrl == "" || authUrl == "" || tokenUrl == "" || jwksUrl == "" {
+ return nil, err
+ }
+ providerConfig := oidc.ProviderConfig{
+ IssuerURL: issuerUrl,
+ AuthURL: authUrl,
+ TokenURL: tokenUrl,
+ UserInfoURL: userInfoUrl,
+ JWKSURL: jwksUrl,
+ Algorithms: algorithms,
+ }
+
+ provider = providerConfig.NewProvider(context.Background())
}
oauth2Config := oauth2.Config{
@@ -71,18 +91,18 @@ func (p *IdentificationProvider) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := p.SessionsStore.Get(r, "parcoursmob_session")
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
}
if session.Values["idtoken"] == nil || session.Values["idtoken"] == "" {
-
state, err := newState()
if err != nil {
panic(err)
}
session.Values["state"] = state
session.Save(r, w)
- http.Redirect(w, r, p.OAuth2Config.AuthCodeURL(state), http.StatusFound)
+ url := p.OAuth2Config.AuthCodeURL(state)
+ http.Redirect(w, r, url, http.StatusFound)
return
}
@@ -102,7 +122,7 @@ func (p *IdentificationProvider) Middleware(next http.Handler) http.Handler {
err = idtoken.Claims(&claims)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
}
ctx := context.WithValue(r.Context(), IdtokenKey, idtoken)
diff --git a/utils/profile-pictures/profile-pictures.go b/core/utils/profile-pictures/profile-pictures.go
old mode 100644
new mode 100755
similarity index 92%
rename from utils/profile-pictures/profile-pictures.go
rename to core/utils/profile-pictures/profile-pictures.go
index 75dc9c4..4df7ae5
--- a/utils/profile-pictures/profile-pictures.go
+++ b/core/utils/profile-pictures/profile-pictures.go
@@ -1,12 +1,12 @@
package profilepictures
import (
- "fmt"
"image"
"image/color"
"image/draw"
"github.com/fogleman/gg"
+ "github.com/rs/zerolog/log"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
@@ -21,7 +21,7 @@ func DefaultProfilePicture(initials string) *image.RGBA {
ff, err := gg.LoadFontFace("themes/default/web/fonts/bitter.ttf", 150.0)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return img
}
diff --git a/utils/sorting/beneficiaries.go b/core/utils/sorting/beneficiaries.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/sorting/beneficiaries.go
rename to core/utils/sorting/beneficiaries.go
diff --git a/utils/sorting/events.go b/core/utils/sorting/events.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/sorting/events.go
rename to core/utils/sorting/events.go
diff --git a/core/utils/sorting/fleets.go b/core/utils/sorting/fleets.go
new file mode 100755
index 0000000..ca1536f
--- /dev/null
+++ b/core/utils/sorting/fleets.go
@@ -0,0 +1,73 @@
+package sorting
+
+import (
+ "cmp"
+ "encoding/json"
+ "strings"
+
+ "git.coopgo.io/coopgo-platform/fleets/storage"
+ fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
+ "github.com/paulmach/orb/geo"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+)
+
+type VehiclesByLicencePlate []fleetsstorage.Vehicle
+
+func (a VehiclesByLicencePlate) Len() int { return len(a) }
+func (a VehiclesByLicencePlate) Less(i, j int) bool {
+ return strings.Compare(a[i].Data["licence_plate"].(string), a[j].Data["licence_plate"].(string)) < 0
+}
+func (a VehiclesByLicencePlate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+type BookingsByStartdate []fleetsstorage.Booking
+
+func (a BookingsByStartdate) Len() int { return len(a) }
+func (a BookingsByStartdate) Less(i, j int) bool {
+ return a[i].Startdate.Before(a[j].Startdate)
+}
+func (a BookingsByStartdate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+// Functions
+
+func VehiclesByDistanceFrom(from geojson.Feature) func(vehicle1, vehicle2 storage.Vehicle) int {
+ return func(vehicle1, vehicle2 storage.Vehicle) int {
+ vehicle1Address, ok := vehicle1.Data["address"]
+ if !ok {
+ return 1
+ }
+ vehicle1Json, err := json.Marshal(vehicle1Address)
+ if err != nil {
+ log.Error().Err(err).Msg("failed marshalling vehicle 1 json")
+ return 1
+ }
+
+ vehicle1Geojson, err := geojson.UnmarshalFeature(vehicle1Json)
+ if err != nil {
+ log.Error().Err(err).Msg("failed unmarshalling vehicle 1 geojson")
+ return 1
+ }
+
+ vehicle2Address, ok := vehicle2.Data["address"]
+ if !ok {
+ log.Debug().Msg("Vehicle 2 does not have an address")
+ return -1
+ }
+ vehicle2Json, err := json.Marshal(vehicle2Address)
+ if err != nil {
+ log.Error().Err(err).Msg("failed marshalling vehicle 2 json")
+ return -1
+ }
+
+ vehicle2Geojson, err := geojson.UnmarshalFeature(vehicle2Json)
+ if err != nil {
+ log.Error().Err(err).Msg("failed unmarshalling vehicle 2 geojson")
+ return -1
+ }
+
+ distance1 := geo.Distance(from.Point(), vehicle1Geojson.Point())
+ distance2 := geo.Distance(from.Point(), vehicle2Geojson.Point())
+
+ return cmp.Compare(distance1, distance2)
+ }
+}
diff --git a/utils/sorting/groups.go b/core/utils/sorting/groups.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/sorting/groups.go
rename to core/utils/sorting/groups.go
diff --git a/core/utils/sorting/solidarity-transport.go b/core/utils/sorting/solidarity-transport.go
new file mode 100644
index 0000000..8ee4de8
--- /dev/null
+++ b/core/utils/sorting/solidarity-transport.go
@@ -0,0 +1,32 @@
+package sorting
+
+import (
+ "strings"
+
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
+)
+
+type SolidarityDriversByName []mobilityaccountsstorage.Account
+
+func (e SolidarityDriversByName) Len() int { return len(e) }
+func (e SolidarityDriversByName) Less(i, j int) bool {
+ return e[i].Data["first_name"].(string) < e[j].Data["first_name"].(string)
+}
+func (e SolidarityDriversByName) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
+
+type SolidarityAvailabilitiesByDay []*gen.DriverRegularAvailability
+
+func (e SolidarityAvailabilitiesByDay) Len() int { return len(e) }
+func (e SolidarityAvailabilitiesByDay) Less(i, j int) bool {
+ if e[i].Day == e[j].Day {
+ return strings.Compare(e[i].StartTime, e[j].StartTime) < 0
+ }
+
+ if e[i].Day == 0 {
+ return false
+ }
+
+ return e[i].Day < e[j].Day
+}
+func (e SolidarityAvailabilitiesByDay) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
diff --git a/utils/sorting/sorting.go b/core/utils/sorting/sorting.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/sorting/sorting.go
rename to core/utils/sorting/sorting.go
diff --git a/core/utils/storage/badger.go b/core/utils/storage/badger.go
new file mode 100644
index 0000000..82be054
--- /dev/null
+++ b/core/utils/storage/badger.go
@@ -0,0 +1 @@
+package storage
diff --git a/utils/storage/cache.go b/core/utils/storage/cache.go
old mode 100644
new mode 100755
similarity index 100%
rename from utils/storage/cache.go
rename to core/utils/storage/cache.go
diff --git a/utils/storage/etcd.go b/core/utils/storage/etcd.go
old mode 100644
new mode 100755
similarity index 92%
rename from utils/storage/etcd.go
rename to core/utils/storage/etcd.go
index 14d044d..ce3e582
--- a/utils/storage/etcd.go
+++ b/core/utils/storage/etcd.go
@@ -8,6 +8,7 @@ import (
"fmt"
"time"
+ "github.com/rs/zerolog/log"
"github.com/spf13/viper"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/namespace"
@@ -75,7 +76,7 @@ func NewEtcdHandler(cfg *viper.Viper) (*EtcdHandler, error) {
DialTimeout: 5 * time.Second,
})
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return nil, err
}
@@ -92,7 +93,7 @@ func NewEtcdHandler(cfg *viper.Viper) (*EtcdHandler, error) {
func (s *EtcdHandler) Put(k string, v any) error {
data, err := s.serializer.Serialize(v)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String())
@@ -100,7 +101,7 @@ func (s *EtcdHandler) Put(k string, v any) error {
_, err = s.Client.KV.Put(ctx, k, string(data))
cancel()
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
return nil
@@ -109,13 +110,13 @@ func (s *EtcdHandler) Put(k string, v any) error {
func (s *EtcdHandler) PutWithTTL(k string, v any, duration time.Duration) error {
lease, err := s.Client.Lease.Grant(context.TODO(), int64(duration.Seconds()))
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
data, err := s.serializer.Serialize(v)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String(), clientv3.WithLease(lease.ID))
@@ -124,7 +125,7 @@ func (s *EtcdHandler) PutWithTTL(k string, v any, duration time.Duration) error
cancel()
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
return nil
@@ -135,14 +136,14 @@ func (s *EtcdHandler) Get(k string) (any, error) {
resp, err := s.Client.KV.Get(ctx, k)
cancel()
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return nil, err
}
for _, v := range resp.Kvs {
var data any
err := s.serializer.Deserialize([]byte(v.Value), &data)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return nil, err
}
// We return directly as we want to last revision of value
@@ -156,7 +157,7 @@ func (s *EtcdHandler) Delete(k string) error {
_, err := s.Client.KV.Delete(ctx, k)
cancel()
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
return nil
diff --git a/utils/storage/files.go b/core/utils/storage/files.go
old mode 100644
new mode 100755
similarity index 63%
rename from utils/storage/files.go
rename to core/utils/storage/files.go
index aad384d..b43f87f
--- a/utils/storage/files.go
+++ b/core/utils/storage/files.go
@@ -8,8 +8,11 @@ import (
)
const (
- PREFIX_BENEFICIARIES = "beneficiaries"
- PREFIX_BOOKINGS = "fleets_bookings"
+ PREFIX_BENEFICIARIES = "beneficiaries"
+ PREFIX_SOLIDARITY_TRANSPORT_DRIVERS = "solidarity_transport/drivers"
+ PREFIX_ORGANIZED_CARPOOL_DRIVERS = "organized_carpool/drivers"
+ PREFIX_BOOKINGS = "fleets_bookings"
+ PREFIX_AGENDA = "event_files"
)
type FileInfo struct {
@@ -25,6 +28,7 @@ type FileStorage interface {
List(prefix string) []FileInfo
Get(prefix string, file string) (io.Reader, *FileInfo, error)
Copy(src string, dest string) error
+ Delete(prefix string, file string) error
}
func NewFileStorage(cfg *viper.Viper) (FileStorage, error) {
diff --git a/utils/storage/kv.go b/core/utils/storage/kv.go
old mode 100644
new mode 100755
similarity index 94%
rename from utils/storage/kv.go
rename to core/utils/storage/kv.go
index f422a7b..1d545fe
--- a/utils/storage/kv.go
+++ b/core/utils/storage/kv.go
@@ -15,4 +15,6 @@ type KVHandler interface {
func NewKVHandler(cfg *viper.Viper) (KVHandler, error) {
return NewEtcdHandler(cfg)
+ return nil, nil
+
}
diff --git a/utils/storage/minio.go b/core/utils/storage/minio.go
old mode 100644
new mode 100755
similarity index 83%
rename from utils/storage/minio.go
rename to core/utils/storage/minio.go
index bd7ea57..79322c6
--- a/utils/storage/minio.go
+++ b/core/utils/storage/minio.go
@@ -2,12 +2,12 @@ package storage
import (
"context"
- "fmt"
"io"
"strings"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
+ "github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
@@ -23,7 +23,7 @@ func NewMinioStorageHandler(cfg *viper.Viper) (*MinioStorageHandler, error) {
Secure: cfg.GetBool("storage.files.minio.use_ssl"),
})
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return nil, err
}
@@ -53,13 +53,13 @@ func (s *MinioStorageHandler) List(prefix string) []FileInfo {
for object := range objectCh {
if object.Err != nil {
- fmt.Println("Error : ", object.Err)
+ log.Error().Str("prefix", prefix).Err(object.Err).Msg("Error listing files for prefix")
continue
}
objinfo, err := s.Client.StatObject(context.Background(), s.BucketName, object.Key, minio.StatObjectOptions{})
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
continue
}
@@ -82,12 +82,12 @@ func (s *MinioStorageHandler) List(prefix string) []FileInfo {
func (s *MinioStorageHandler) Get(prefix string, file string) (io.Reader, *FileInfo, error) {
object, err := s.Client.GetObject(context.Background(), s.BucketName, prefix+"/"+file, minio.GetObjectOptions{})
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return nil, nil, err
}
objinfo, err := s.Client.StatObject(context.Background(), s.BucketName, prefix+"/"+file, minio.StatObjectOptions{})
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return nil, nil, err
}
@@ -117,7 +117,16 @@ func (s *MinioStorageHandler) Copy(src string, dst string) error {
_, err := s.Client.CopyObject(context.Background(), dstOpts, srcOpts)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
+ return err
+ }
+ return nil
+}
+
+func (s *MinioStorageHandler) Delete(prefix string, file string) error {
+ err := s.Client.RemoveObject(context.Background(), s.BucketName, prefix+"/"+file, minio.RemoveObjectOptions{})
+ if err != nil {
+ log.Error().Err(err).Msg("Error deleting file from storage")
return err
}
return nil
diff --git a/utils/storage/sessions.go b/core/utils/storage/sessions.go
old mode 100644
new mode 100755
similarity index 96%
rename from utils/storage/sessions.go
rename to core/utils/storage/sessions.go
index 52a1207..e269b88
--- a/utils/storage/sessions.go
+++ b/core/utils/storage/sessions.go
@@ -11,6 +11,7 @@ import (
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
+ "github.com/rs/zerolog/log"
)
// Amount of time for cookies/kv keys to expire.
@@ -76,7 +77,7 @@ func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, session *ses
// Marked for deletion.
if session.Options.MaxAge <= 0 {
if err := s.delete(r.Context(), session); err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
@@ -87,13 +88,13 @@ func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, session *ses
}
if err := s.save(r.Context(), session); err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
@@ -104,13 +105,11 @@ func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, session *ses
// save stores the session in kv.
func (s *SessionStore) save(ctx context.Context, session *sessions.Session) error {
m := make(map[string]interface{}, len(session.Values))
- fmt.Println(m)
for k, v := range session.Values {
- fmt.Println(v)
ks, ok := k.(string)
if !ok {
err := fmt.Errorf("non-string key value, cannot serialize session: %v", k)
- fmt.Println(err)
+ log.Error().Err(err).Msg("")
return err
}
m[ks] = v
diff --git a/core/utils/validated-profile/validated-profile.go b/core/utils/validated-profile/validated-profile.go
new file mode 100644
index 0000000..f04df87
--- /dev/null
+++ b/core/utils/validated-profile/validated-profile.go
@@ -0,0 +1,102 @@
+package validatedprofile
+
+import (
+ "cmp"
+ "slices"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "github.com/go-viper/mapstructure/v2"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/viper"
+ "github.com/stretchr/objx"
+)
+
+type Comparison struct {
+ Field string
+ Type string
+ Value any
+}
+
+func ValidateProfile(cfg *viper.Viper) func(mobilityaccountsstorage.Account, []storage.FileInfo) bool {
+ enabled := cfg.GetBool("enabled")
+ requiredDocuments := cfg.GetStringSlice("required.documents")
+ requiredFields := cfg.GetStringSlice("required.fields")
+ comp := cfg.Get("assert.compare")
+ var comparisons []Comparison
+ err := mapstructure.Decode(comp, &comparisons)
+ if err != nil {
+ log.Error().Err(err).Msg("reading comparisons issue")
+ }
+ return func(account mobilityaccountsstorage.Account, docs []storage.FileInfo) bool {
+ if !enabled {
+ return true
+ }
+
+ for _, d := range requiredDocuments {
+ if !slices.ContainsFunc(docs, func(f storage.FileInfo) bool {
+ log.Debug().Str("required", d).Str("checked", f.Metadata["Type"]).Msg("file check")
+ return f.Metadata["Type"] == d
+ }) {
+ log.Debug().Msg("file missing")
+ return false
+ }
+ }
+
+ obj := objx.Map(account.Data)
+
+ for _, f := range requiredFields {
+ if obj.Get(f) == nil {
+ log.Debug().Msg("field missing")
+ return false
+ }
+ }
+
+ for _, c := range comparisons {
+ val := obj.Get(c.Field)
+ if val == nil {
+ return false
+ }
+ value := ""
+ if v, ok := c.Value.(string); ok {
+ value = v
+ } else if v, ok := c.Value.(time.Time); ok {
+ value = v.Format("2006-01-02")
+ } else {
+ log.Error().Msg("could not get type")
+ return false
+ }
+ result := cmp.Compare(val.String(), value)
+
+ if c.Type == "gte" {
+ if result < 0 {
+ log.Debug().Int("comparison result", result).Str("operand", c.Type).Msg("comparison issue")
+ return false
+ }
+ } else if c.Type == "gt" {
+ if result <= 0 {
+ log.Debug().Int("comparison result", result).Str("operand", c.Type).Msg("comparison issue")
+ return false
+ }
+ } else if c.Type == "lt" {
+ if result >= 0 {
+ log.Debug().Int("comparison result", result).Str("operand", c.Type).Msg("comparison issue")
+ return false
+ }
+ } else if c.Type == "lte" {
+ if result < 0 {
+ log.Debug().Int("comparison result", result).Str("operand", c.Type).Msg("comparison issue")
+ return false
+ }
+ } else if c.Type == "eq" {
+ if result != 0 {
+ log.Debug().Int("comparison result", result).Str("operand", c.Type).Msg("comparison issue")
+ return false
+ }
+ }
+ }
+
+ return true
+ }
+}
diff --git a/go.mod b/go.mod
old mode 100644
new mode 100755
index 3c44ada..4f0f8ce
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,6 @@
module git.coopgo.io/coopgo-apps/parcoursmob
-go 1.22.0
-
-toolchain go1.23.3
+go 1.24.6
// replace git.coopgo.io/coopgo-platform/mobility-accounts => ../../coopgo-platform/mobility-accounts/
@@ -14,98 +12,169 @@ toolchain go1.23.3
// replace git.coopgo.io/coopgo-platform/emailing => ../../coopgo-platform/emailing/
+// replace git.coopgo.io/coopgo-platform/data-hub => ../../coopgo-platform/data-hub/
+
+// replace git.coopgo.io/coopgo-platform/solidarity-transport => ../../coopgo-platform/solidarity-transport/
+
+// replace git.coopgo.io/coopgo-platform/saved-search => ../../coopgo-platform/saved-search/
+
+// replace git.coopgo.io/coopgo-platform/carpool-service => ../../coopgo-platform/carpool-service/
+
+// replace git.coopgo.io/coopgo-platform/multimodal-routing => ../../coopgo-platform/multimodal-routing/
+
+// replace git.coopgo.io/coopgo-platform/payments => ../../coopgo-platform/payments/
+
+// replace git.coopgo.io/coopgo-platform/geography => ../../coopgo-platform/geography/
+
+// replace git.coopgo.io/coopgo-platform/sms => ../../coopgo-platform/sms/
+
require (
- github.com/coreos/go-oidc v2.2.1+incompatible
github.com/fogleman/gg v1.3.0
- github.com/go-playground/validator/v10 v10.11.0
+ github.com/go-playground/validator/v10 v10.14.1
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
- github.com/paulmach/go.geojson v1.4.0
- github.com/spf13/viper v1.19.0
+ github.com/paulmach/go.geojson v1.4.0 // indirect
+ github.com/spf13/viper v1.21.0
gitlab.scity.coop/maas/navitia-golang v0.0.0-20220429110621-5c22d6efdd0c
go.etcd.io/etcd/client/v3 v3.5.12
- golang.org/x/image v0.5.0
- golang.org/x/oauth2 v0.25.0
- google.golang.org/grpc v1.67.3
- google.golang.org/protobuf v1.36.1
+ golang.org/x/image v0.25.0
+ golang.org/x/oauth2 v0.30.0
+ google.golang.org/grpc v1.76.0
+ google.golang.org/protobuf v1.36.10
)
require (
- git.coopgo.io/coopgo-platform/agenda v0.0.0-20230310121901-ef3add576f86
+ git.coopgo.io/coopgo-platform/agenda v1.0.0
+ git.coopgo.io/coopgo-platform/carpool-service v0.0.0-20251008165122-38cb3c5ad9b4
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260
- git.coopgo.io/coopgo-platform/fleets v0.0.0-20230310144446-feb935f8bf4e
+ git.coopgo.io/coopgo-platform/fleets v1.1.0
+ git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c
- git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230430115320-f5bb2e7c2c26
+ git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386
+ git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013140400-42fb40437ac3
+ git.coopgo.io/coopgo-platform/payments v0.0.0-20251013175712-75d0288d2d4f
+ git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536
+ git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463
+ git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af
+ git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc
+ github.com/arran4/golang-ical v0.3.1
+ github.com/coreos/go-oidc/v3 v3.11.0
+ github.com/go-viper/mapstructure/v2 v2.4.0
github.com/gorilla/securecookie v1.1.1
github.com/minio/minio-go/v7 v7.0.43
- github.com/xuri/excelize/v2 v2.7.1
+ github.com/modelcontextprotocol/go-sdk v1.0.0
+ github.com/paulmach/orb v0.12.0
+ github.com/rs/zerolog v1.34.0
+ github.com/stretchr/objx v0.5.3
+ github.com/xuri/excelize/v2 v2.9.1
)
require (
+ git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss v0.0.0-20251008142525-4392f227836a // indirect
+ github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/bits-and-blooms/bitset v1.22.0 // indirect
+ github.com/blevesearch/bleve/v2 v2.5.2 // indirect
+ github.com/blevesearch/bleve_index_api v1.2.8 // indirect
+ github.com/blevesearch/geo v0.2.3 // indirect
+ github.com/blevesearch/go-faiss v1.0.25 // indirect
+ github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
+ github.com/blevesearch/gtreap v0.1.1 // indirect
+ github.com/blevesearch/mmap-go v1.0.4 // indirect
+ github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect
+ github.com/blevesearch/segment v0.9.1 // indirect
+ github.com/blevesearch/snowballstem v0.9.0 // indirect
+ github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
+ github.com/blevesearch/vellum v1.1.0 // indirect
+ github.com/blevesearch/zapx/v11 v11.4.2 // indirect
+ github.com/blevesearch/zapx/v12 v12.4.2 // indirect
+ github.com/blevesearch/zapx/v13 v13.4.2 // indirect
+ github.com/blevesearch/zapx/v14 v14.4.2 // indirect
+ github.com/blevesearch/zapx/v15 v15.4.2 // indirect
+ github.com/blevesearch/zapx/v16 v16.2.4 // indirect
+ github.com/bmatcuk/doublestar v1.3.4 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+ github.com/gorilla/schema v1.4.1 // indirect
+ github.com/mschoch/smat v0.2.0 // indirect
+ github.com/sagikazarmark/locafero v0.12.0 // indirect
+ github.com/tidwall/geoindex v1.7.0 // indirect
+ github.com/tidwall/rtree v1.10.0 // indirect
+ github.com/twpayne/go-polyline v1.1.1 // indirect
+ github.com/zclconf/go-cty-yaml v1.1.0 // indirect
+ go.etcd.io/bbolt v1.4.0 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/mod v0.28.0 // indirect
+ golang.org/x/tools v0.37.0 // indirect
+)
+
+require (
+ ariga.io/atlas v0.37.0 // indirect
+ github.com/agext/levenshtein v1.2.3 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
- github.com/coreos/go-systemd/v22 v22.3.2 // indirect
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
- github.com/fsnotify/fsnotify v1.8.0 // indirect
- github.com/go-playground/locales v0.14.0 // indirect
- github.com/go-playground/universal-translator v0.18.0 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.2 // indirect
+ github.com/go-openapi/inflect v0.21.3 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
- github.com/golang/snappy v0.0.1 // indirect
- github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/golang/snappy v1.0.0 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
+ github.com/google/jsonschema-go v0.3.0 // indirect
+ github.com/hashicorp/hcl/v2 v2.24.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/compress v1.17.2 // indirect
- github.com/klauspost/cpuid/v2 v2.1.0 // indirect
- github.com/leodido/go-urn v1.2.1 // indirect
- github.com/magiconair/properties v1.8.9 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.5 // indirect
+ github.com/leodido/go-urn v1.2.4 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mb0/wkt v0.0.0-20170420051526-a30afd545ee1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
- github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
- github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
- github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+ github.com/montanaflynn/stats v0.7.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
- github.com/richardlehane/msoleps v1.0.3 // indirect
- github.com/rs/xid v1.4.0 // indirect
- github.com/sagikazarmark/locafero v0.7.0 // indirect
- github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/richardlehane/msoleps v1.0.4 // indirect
+ github.com/rs/xid v1.6.0 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect
- github.com/sirupsen/logrus v1.9.0 // indirect
- github.com/sourcegraph/conc v0.3.0 // indirect
- github.com/spf13/afero v1.12.0 // indirect
- github.com/spf13/cast v1.7.1 // indirect
- github.com/spf13/pflag v1.0.6 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/spf13/afero v1.15.0 // indirect
+ github.com/spf13/cast v1.10.0 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
- github.com/tidwall/pretty v1.1.0 // indirect
- github.com/twpayne/go-geom v1.2.1 // indirect
+ github.com/tiendc/go-deepcopy v1.7.1 // indirect
+ github.com/twpayne/go-geom v1.5.7 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
- github.com/xdg-go/scram v1.1.1 // indirect
- github.com/xdg-go/stringprep v1.0.3 // indirect
- github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
- github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
- github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
+ github.com/xuri/efp v0.0.1 // indirect
+ github.com/xuri/nfp v0.0.1 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+ github.com/zclconf/go-cty v1.17.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.12 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect
- go.mongodb.org/mongo-driver v1.10.1 // indirect
+ go.mongodb.org/mongo-driver v1.17.4 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.21.0 // indirect
- golang.org/x/crypto v0.33.0 // indirect
- golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 // indirect
- golang.org/x/net v0.33.0 // indirect
- golang.org/x/sync v0.11.0 // indirect
- golang.org/x/sys v0.30.0 // indirect
- golang.org/x/text v0.22.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
+ golang.org/x/crypto v0.43.0 // indirect
+ golang.org/x/net v0.46.0 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/sys v0.37.0 // indirect
+ golang.org/x/text v0.30.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
- gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index f182dba..a3e7644 100644
--- a/go.sum
+++ b/go.sum
@@ -1,34 +1,106 @@
-git.coopgo.io/coopgo-platform/agenda v0.0.0-20230310121901-ef3add576f86 h1:pSNHhPU8NB35G14QsSyw/XwsTWZo0qGB0U0X0gzqR1s=
-git.coopgo.io/coopgo-platform/agenda v0.0.0-20230310121901-ef3add576f86/go.mod h1:7jVyZSz//VW5+jlLeuwc+JJawNUhJZ7Vima+TdVwKfs=
-git.coopgo.io/coopgo-platform/emailing v0.0.0-20250211064035-3c2744fb32ec h1:kMbNAiC/y154+Sq4p/0Mk3OMb9vvMrAOwrf+3FiQY3k=
-git.coopgo.io/coopgo-platform/emailing v0.0.0-20250211064035-3c2744fb32ec/go.mod h1:6cvvjv0RLSwBthIQ4TiuZoXFGvQXZ55hNSJchWXAgB4=
+ariga.io/atlas v0.37.0 h1:MvbQ25CAHFslttEKEySwYNFrFUdLAPhtU1izOzjXV+o=
+ariga.io/atlas v0.37.0/go.mod h1:mHE83ptCxEkd3rO3c7Rvkk6Djf6mVhEiSVhoiNu96CI=
+git.coopgo.io/coopgo-platform/agenda v1.0.0 h1:rTHgva1JKKO0wAPlINegifMkHm+xOg3IWW4yQRy334w=
+git.coopgo.io/coopgo-platform/agenda v1.0.0/go.mod h1:/hToSla0p6SeWn1zo1MDrfxdmo7RBdZDkbLqCVituIM=
+git.coopgo.io/coopgo-platform/carpool-service v0.0.0-20251008165122-38cb3c5ad9b4 h1:L3HJnjQo5M0maOKkILAIq257tbHHCsr6OpAWGVQoGJs=
+git.coopgo.io/coopgo-platform/carpool-service v0.0.0-20251008165122-38cb3c5ad9b4/go.mod h1:4qy6Ha8/cA3L+hZyzPYbKJ0dFO+zyDh8W7bMXDXE6WQ=
+git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss v0.0.0-20251008142525-4392f227836a h1:roBn3oQRQ/j5JBAP01wIs1UQvtm3tjui+D4EAa/Hfa4=
+git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss v0.0.0-20251008142525-4392f227836a/go.mod h1:c9aJwNtY4PJuqAFYZ9afnx46UAZtWJ3P8ICZM02/DBA=
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260 h1:Li3dotY6raKu9+oxEgICU7nwdomYpjgu19i3mZNiqTc=
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260/go.mod h1:6cvvjv0RLSwBthIQ4TiuZoXFGvQXZ55hNSJchWXAgB4=
-git.coopgo.io/coopgo-platform/fleets v0.0.0-20230310144446-feb935f8bf4e h1:eHahRTKlC8aBWYCd6LbXNcX8HoQhuZj31OFWrw0EL0U=
-git.coopgo.io/coopgo-platform/fleets v0.0.0-20230310144446-feb935f8bf4e/go.mod h1:s9OIFCNcjBAbBzRNHwoCTYV6kAntPG9CpT3GVweGdTY=
+git.coopgo.io/coopgo-platform/fleets v1.1.0 h1:pfW/K3fWfap54yNfkLzBXjvOjjoTaEGFEqS/+VkHv7s=
+git.coopgo.io/coopgo-platform/fleets v1.1.0/go.mod h1:nuK2mi1M2+DdntinqK/8C4ttW4WWyKCCY/xD1D7XjkE=
+git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858 h1:4E0tbT8jj5oxaK66Ny61o7zqPaVc0qRN2cZG9IUR4Es=
+git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858/go.mod h1:TbR3g1Awa8hpAe6LR1z1EQbv2IBVgN5JQ/FjXfKX4K0=
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c h1:bY7PyrAgYY02f5IpDyf1WVfRqvWzivu31K6aEAYbWCw=
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c/go.mod h1:lozSy6qlIIYhvKKXscZzz28HAtS0qBDUTv5nofLRmYA=
-git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230430115320-f5bb2e7c2c26 h1:qiYVLLNU29xgT0RRj0Jz0WrjEJnz1Eng1X7l1UW9jGU=
-git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230430115320-f5bb2e7c2c26/go.mod h1:1typNYtO+PQT6KG77vs/PUv0fO60/nbeSGZL2tt1LLg=
+git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386 h1:v1JUdx8sknw2YYhFGz5cOAa1dEWNIBKvyiOpKr3RR+s=
+git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386/go.mod h1:1typNYtO+PQT6KG77vs/PUv0fO60/nbeSGZL2tt1LLg=
+git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013140400-42fb40437ac3 h1:jo7fF7tLIAU110tUSIYXkMAvu30g8wHzZCLq3YomooQ=
+git.coopgo.io/coopgo-platform/multimodal-routing v0.0.0-20251013140400-42fb40437ac3/go.mod h1:npYccQZcZj1WzTHhpLfFHFSBA3RiZOkO5R9x4uy1a9I=
+git.coopgo.io/coopgo-platform/payments v0.0.0-20251013175712-75d0288d2d4f h1:B/+AP+rLFx8AojO2bKV3R93kMU84g8Dhy7DNVoT8xCY=
+git.coopgo.io/coopgo-platform/payments v0.0.0-20251013175712-75d0288d2d4f/go.mod h1:gSAH2Tr9x8K8QC0vsUMwSWLrQOlsG+v64ACrjYw4BL0=
+git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536 h1:SllXX1VJXulfhNi+Pd0R9chksm8zO6gkWcTQ/uSMsdc=
+git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536/go.mod h1:Nh7o15LlV0OuO9zxvJIs9FlelpeAaLYkXtFdgIkFrgg=
+git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463 h1:TjjqFEkRLDqy300pGOLIhsVYBpE0J640pFW9//OknzA=
+git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463/go.mod h1:0fuGuYub5CBy9NB6YMqxawE0HoBaxPb9gmSw1gjfDy0=
+git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af h1:KxHim1dFcOVbFhRqelec8cJ65QBD2cma6eytW8llgYY=
+git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af/go.mod h1:mad9D+WICDdpJzB+8H/wEVVbllK2mU6VLVByrppc9x0=
+git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc h1:NLU5DUo5Kt3jkPhV3KkqQMahTHIrGildBvYlHwJ6JmM=
+git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/DATA-DOG/go-sqlmock v1.3.2/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
+github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
+github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
+github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
+github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
+github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
+github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/arran4/golang-ical v0.3.1 h1:v13B3eQZ9VDHTAvT6M11vVzxYgcYmjyPBE2eAZl3VZk=
+github.com/arran4/golang-ical v0.3.1/go.mod h1:LZWxF8ZIu/sjBVUCV0udiVPrQAgq3V0aa0RfbO99Qkk=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
+github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/blevesearch/bleve/v2 v2.5.2 h1:Ab0r0MODV2C5A6BEL87GqLBySqp/s9xFgceCju6BQk8=
+github.com/blevesearch/bleve/v2 v2.5.2/go.mod h1:5Dj6dUQxZM6aqYT3eutTD/GpWKGFSsV8f7LDidFbwXo=
+github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y=
+github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
+github.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg=
+github.com/blevesearch/geo v0.2.3/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
+github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
+github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
+github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
+github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
+github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
+github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
+github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
+github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
+github.com/blevesearch/scorch_segment_api/v2 v2.3.10 h1:Yqk0XD1mE0fDZAJXTjawJ8If/85JxnLd8v5vG/jWE/s=
+github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8=
+github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
+github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
+github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
+github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
+github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
+github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
+github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
+github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
+github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
+github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
+github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
+github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
+github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
+github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
+github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
+github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
+github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
+github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
+github.com/blevesearch/zapx/v16 v16.2.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww=
+github.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs=
+github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
+github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
-github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
-github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
+github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -38,47 +110,72 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
-github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
-github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
-github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
-github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
-github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
-github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
-github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
+github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/inflect v0.21.3 h1:TmQvw+9eLrsNp4X0BBQacEZZtAnzk2z1FaLdQQJsDiU=
+github.com/go-openapi/inflect v0.21.3/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
+github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
+github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
+github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
+github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
+github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -86,30 +183,35 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
-github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
-github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
+github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
-github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mb0/wkt v0.0.0-20170420051526-a30afd545ee1 h1:VCgV+ng800r1/AChRHzHbWCtQI06cPxoZQUljQHTyXc=
github.com/mb0/wkt v0.0.0-20170420051526-a30afd545ee1/go.mod h1:IhobDa5AIyiMAsnH/qkytD0NbG0JMOJ2ihQqe1NdXyg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
@@ -119,68 +221,73 @@ github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASM
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74=
+github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
-github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
+github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
+github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
-github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
-github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
+github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
+github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
-github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
-github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
-github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
+github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
-github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
-github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
-github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
-github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
+github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
+github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
+github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
+github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE=
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
-github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
-github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
-github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
-github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
-github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
+github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -188,45 +295,83 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/tidwall/cities v0.1.0 h1:CVNkmMf7NEC9Bvokf5GoSsArHCKRMTgLuubRTHnH0mE=
+github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4=
+github.com/tidwall/geoindex v1.7.0 h1:jtk41sfgwIt8MEDyC3xyKSj75iXXf6rjReJGDNPtR5o=
+github.com/tidwall/geoindex v1.7.0/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I=
+github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
+github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
-github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-github.com/twpayne/go-geom v1.2.1 h1:GazrButEFQmIiV5SUqZdkWvMbmgVNLhZlJF3mH6Rv5w=
+github.com/tidwall/rtree v1.10.0 h1:+EcI8fboEaW1L3/9oW/6AMoQ8HiEIHyR7bQOGnmz4Mg=
+github.com/tidwall/rtree v1.10.0/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ=
+github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
+github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/twpayne/go-geom v1.2.1/go.mod h1:90yvs0wf/gyT5eQ9W4v5WOZ9w/Xnrj5RMlA9XNKqxyA=
+github.com/twpayne/go-geom v1.5.7 h1:7fdceDUr03/MP7rAKOaTV6x9njMiQdxB/D0PDzMTCDc=
+github.com/twpayne/go-geom v1.5.7/go.mod h1:y4fTAQtLedXW8eG2Yo4tYrIGN1yIwwKkmA+K3iSHKBA=
github.com/twpayne/go-kml v1.5.0/go.mod h1:g/OG8Q8JUxqFw8LGXE44W7osn1uXDAYaVFr1Yld43yc=
github.com/twpayne/go-polyline v1.0.0/go.mod h1:ICh24bcLYBX8CknfvNPKqoTbe+eg+MX1NPyJmSBo7pU=
+github.com/twpayne/go-polyline v1.1.1 h1:/tSF1BR7rN4HWj4XKqvRUNrCiYVMCvywxTFVofvDV0w=
+github.com/twpayne/go-polyline v1.1.1/go.mod h1:ybd9IWWivW/rlXPXuuckeKUyF3yrIim+iqA7kSl4NFY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
-github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
-github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
-github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
-github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
-github.com/xuri/excelize/v2 v2.7.1 h1:gm8q0UCAyaTt3MEF5wWMjVdmthm2EHAWesGSKS9tdVI=
-github.com/xuri/excelize/v2 v2.7.1/go.mod h1:qc0+2j4TvAUrBw36ATtcTeC1VCM0fFdAXZOmcF4nTpY=
-github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
-github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
-github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
+github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
+github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
+github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
+github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
+github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
+github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
+github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
gitlab.scity.coop/maas/navitia-golang v0.0.0-20220429110621-5c22d6efdd0c h1:pCazzEsTvjDopl3bvo6H2f2xjo1cDjOZ9QpJRNFCc00=
gitlab.scity.coop/maas/navitia-golang v0.0.0-20220429110621-5c22d6efdd0c/go.mod h1:M1U2osA6dYQF8zuJOTb/0O1F/Xgcb+4AkRdw+Un6Rp4=
+go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
+go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c=
go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A=
go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4=
go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg=
go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw=
-go.mongodb.org/mongo-driver v1.10.1 h1:NujsPveKwHaWuKUer/ceo9DzEe7HIj1SlJ6uvXZG0S4=
-go.mongodb.org/mongo-driver v1.10.1/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8=
+go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
+go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
+go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@@ -237,26 +382,25 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
-golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
-golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
-golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
-golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
-golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
-golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
+golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
+golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -266,20 +410,17 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
-golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -292,28 +433,25 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -321,36 +459,39 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
+golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
-google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
-google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
-google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
-google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
-google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
+google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
+google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
-gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 h1:lwJmuuJQGclcankpPJwh8rorzB0bNbVALv8phDGh8TQ=
-gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/handlers/api/api.go b/handlers/api/api.go
deleted file mode 100644
index 785db34..0000000
--- a/handlers/api/api.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package api
-
-import (
- "net/http"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/services"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
- "github.com/spf13/viper"
-)
-
-type APIHandler struct {
- idp *identification.IdentificationProvider
- config *viper.Viper
- services *services.ServicesHandler
- cache cache.CacheHandler
-}
-
-func NewAPIHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache cache.CacheHandler) (*APIHandler, error) {
- return &APIHandler{
- idp: idp,
- config: cfg,
- services: svc,
- cache: cache,
- }, nil
-}
-
-func (h *APIHandler) NotFound(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusNotFound)
-}
diff --git a/handlers/api/cache.go b/handlers/api/cache.go
deleted file mode 100644
index 433d7fc..0000000
--- a/handlers/api/cache.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "strconv"
-
- "github.com/gorilla/mux"
-)
-
-func (h APIHandler) GetCache(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- cacheid := vars["cacheid"]
-
- d, err := h.cache.Get(cacheid)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusNotFound)
- return
- }
-
- result := d
-
- if data, ok := d.([]any); ok {
- if limitsmin, ok := r.URL.Query()["limits.min"]; ok {
- min, _ := strconv.Atoi(limitsmin[0])
- if limitsmax, ok := r.URL.Query()["limits.max"]; ok {
- max, _ := strconv.Atoi(limitsmax[0])
- if max > len(data) {
- result = data[min:]
- } else {
- result = data[min:max]
- }
- } else {
- result = data[min:]
- }
- }
- }
-
- j, err := json.Marshal(result)
- if err != nil {
- w.WriteHeader(http.StatusNotFound)
- return
- }
-
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
-
-}
diff --git a/handlers/api/export.go b/handlers/api/export.go
deleted file mode 100644
index 11f5309..0000000
--- a/handlers/api/export.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package api
-
-import (
- "encoding/csv"
- "fmt"
- "net/http"
- "sort"
- "strconv"
-
- "github.com/gorilla/mux"
-)
-
-type FlatMaps []map[string]any
-
-func (maps FlatMaps) GetHeaders() (res []string) {
- keys := map[string]bool{}
- for _, m := range maps {
- for k, _ := range m {
- if _, ok := keys[k]; !ok {
- keys[k] = true
- res = append(res, k)
- }
- }
- }
- sort.Strings(res)
- return
-}
-
-func (maps FlatMaps) GetValues() (res [][]string) {
- headers := maps.GetHeaders()
- for _, m := range maps {
- line := []string{}
- for _, k := range headers {
- if v, ok := m[k]; ok && v != nil {
- line = append(line, fmt.Sprint(v))
- } else {
- line = append(line, "")
- }
- }
- res = append(res, line)
- }
- return
-}
-
-func (h APIHandler) CacheExport(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- cacheid := vars["cacheid"]
-
- d, err := h.cache.Get(cacheid)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusNotFound)
- return
- }
-
- if data, ok := d.([]any); ok {
-
- flatmaps := FlatMaps{}
- //fmt.Println(data)
-
- for _, v := range data {
- fm := map[string]any{}
- flatten("", v.(map[string]any), fm)
- flatmaps = append(flatmaps, fm)
- }
-
- w.Header().Set("Content-Type", "text/csv")
- w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=export-%s.csv", cacheid))
- c := csv.NewWriter(w)
- c.Write(flatmaps.GetHeaders())
- c.WriteAll(flatmaps.GetValues())
- return
-
- }
-
- w.WriteHeader(http.StatusNotFound)
-
-}
-
-func flatten(prefix string, src map[string]any, dest map[string]any) {
- if len(prefix) > 0 {
- prefix += "."
- }
- for k, v := range src {
- switch child := v.(type) {
- case map[string]any:
- flatten(prefix+k, child, dest)
- case []any:
- for i := 0; i < len(child); i++ {
- dest[prefix+k+"."+strconv.Itoa(i)] = child[i]
- }
- default:
- fmt.Println(prefix+k, " : ", v)
- dest[prefix+k] = v
- }
- }
-}
diff --git a/handlers/api/geo.go b/handlers/api/geo.go
deleted file mode 100644
index ccdeca2..0000000
--- a/handlers/api/geo.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
-)
-
-func (h *APIHandler) GeoAutocomplete(w http.ResponseWriter, r *http.Request) {
-
- pelias := h.config.GetString("geo.pelias.url")
-
- t, ok := r.URL.Query()["text"]
-
- if !ok || len(t[0]) < 1 {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- text := t[0]
-
- resp, err := http.Get(fmt.Sprintf("%s/autocomplete?text=%s", pelias, text))
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- defer resp.Body.Close()
-
- body, err := ioutil.ReadAll(resp.Body)
-
- if err != nil {
- log.Fatal(err)
- }
-
- var response map[string]any
- jsonErr := json.Unmarshal(body, &response)
- if jsonErr != nil {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- j, err := json.Marshal(response["features"])
- if err != nil {
- w.WriteHeader(http.StatusNotFound)
- return
- }
-
- w.WriteHeader(http.StatusOK)
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
-}
diff --git a/handlers/api/oidc.go b/handlers/api/oidc.go
deleted file mode 100644
index eead6ea..0000000
--- a/handlers/api/oidc.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package api
-
-import (
- "context"
- "fmt"
- "net/http"
-)
-
-func (h APIHandler) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
- oauth2Token, err := h.idp.OAuth2Config.Exchange(context.Background(), r.URL.Query().Get("code"))
- if err != nil {
- fmt.Println("Exchange error")
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Extract the ID Token from OAuth2 token.
- rawIDToken, ok := oauth2Token.Extra("id_token").(string)
- if !ok {
- fmt.Println("issue retrieving token")
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- _, err = h.idp.TokenVerifier.Verify(context.Background(), rawIDToken)
- if err != nil {
- fmt.Println("not able to verify token")
- fmt.Println(err)
- w.WriteHeader(http.StatusUnauthorized)
- return
- }
-
- session, _ := h.idp.SessionsStore.Get(r, "parcoursmob_session")
- session.Values["idtoken"] = rawIDToken
-
- redirect := "/app/"
-
- if session.Values["redirect"] != nil && session.Values["redirect"] != "" {
- redirect = session.Values["redirect"].(string)
- delete(session.Values, "redirect")
- }
-
- if err = session.Save(r, w); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, redirect, http.StatusFound)
-}
diff --git a/handlers/application/administration.go b/handlers/application/administration.go
deleted file mode 100644
index 7caaf45..0000000
--- a/handlers/application/administration.go
+++ /dev/null
@@ -1,602 +0,0 @@
-package application
-
-import (
- "context"
- "crypto/rand"
- "encoding/base64"
- "fmt"
- "io"
- "net/http"
- "sort"
- "time"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- "git.coopgo.io/coopgo-apps/parcoursmob/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"
- mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
- "github.com/google/uuid"
- "github.com/gorilla/mux"
- "google.golang.org/protobuf/types/known/structpb"
-)
-
-func (h *ApplicationHandler) Administration(w http.ResponseWriter, r *http.Request) {
-
- accounts, err := h.services.GetAccounts()
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiaries, err := h.services.GetBeneficiaries()
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- bookings, err := h.services.GetBookings()
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request := &groupsmanagement.GetGroupsRequest{
- Namespaces: []string{"parcoursmob_organizations"},
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var groups = []groupstorage.Group{}
-
- for _, group := range resp.Groups {
- g := group.ToStorageType()
- groups = append(groups, g)
- }
-
- sort.Sort(sorting.GroupsByName(groups))
- ////////////////////////////////////add event////////////////////////////////////////////
- rresp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
- Namespaces: []string{"parcoursmob_dispositifs"},
- })
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- responses := []agendastorage.Event{}
-
- groupids := []string{}
- for _, e := range rresp.Events {
- groupids = append(groupids, e.Owners...)
- responses = append(responses, e.ToStorageType())
- }
-
- sort.Sort(sorting.EventsByStartdate(responses))
-
- groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
- Groupids: groupids,
- })
- groupps := map[string]any{}
-
- if err == nil {
- for _, g := range groupsresp.Groups {
- groupps[g.Id] = g.ToStorageType()
- }
- }
-
- h.Renderer.Administration(w, r, accounts, beneficiaries, groups, bookings, responses)
-}
-
-func (h *ApplicationHandler) AdministrationCreateGroup(w http.ResponseWriter, r *http.Request) {
- if r.Method == "POST" {
- r.ParseForm()
-
- if r.FormValue("name") == "" {
-
- fmt.Println("invalid name")
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- modules := map[string]any{
- "beneficiaries": r.FormValue("modules.beneficiaries") == "on",
- "journeys": r.FormValue("modules.journeys") == "on",
- "vehicles": r.FormValue("modules.vehicles") == "on",
- "vehicles_management": r.FormValue("modules.vehicles_management") == "on",
- "events": r.FormValue("modules.events") == "on",
- "agenda": r.FormValue("modules.agenda") == "on",
- "groups": r.FormValue("modules.groups") == "on",
- "administration": r.FormValue("modules.administration") == "on",
- "support": r.FormValue("modules.support") == "on",
- "group_module": r.FormValue("modules.group_module") == "on",
- }
-
- groupid := uuid.NewString()
-
- dataMap := map[string]any{
- "name": r.FormValue("name"),
- "modules": modules,
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- 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",
- },
- }
-
- _, err = h.services.GRPC.GroupsManagement.AddGroup(context.TODO(), request_organization)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Create the admin role for the organization
- _, err = h.services.GRPC.GroupsManagement.AddGroup(context.TODO(), request_role)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/administration/groups/%s", groupid), http.StatusFound)
- return
- }
- h.Renderer.AdministrationCreateGroup(w, r)
-}
-
-func (h *ApplicationHandler) AdministrationGroupDisplay(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- groupid := vars["groupid"]
-
- request := &groupsmanagement.GetGroupRequest{
- Id: groupid,
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- groupmembers, admins, err := h.groupmembers(groupid)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- h.Renderer.AdministrationGroupDisplay(w, r, resp.Group.ToStorageType(), groupmembers, admins)
-}
-
-func (h *ApplicationHandler) AdministrationGroupInviteAdmin(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- groupid := vars["groupid"]
-
- groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), &groupsmanagement.GetGroupRequest{
- Id: groupid,
- Namespace: "parcoursmob_organizations",
- })
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- r.ParseForm()
-
- accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &accounts.GetAccountUsernameRequest{
- Username: r.FormValue("username"),
- Namespace: "parcoursmob",
- })
-
- if err == nil {
- // Account already exists : adding the existing account to admin list
- account := accountresp.Account.ToStorageType()
- account.Data["groups"] = append(account.Data["groups"].([]any), groupid, groupid)
- account.Data["groups"] = append(account.Data["groups"].([]any), groupid, groupid+":admin")
-
- as, _ := accounts.AccountFromStorageType(&account)
-
- _, err = h.services.GRPC.MobilityAccounts.UpdateData(
- context.TODO(),
- &accounts.UpdateDataRequest{
- Account: as,
- },
- )
-
- fmt.Println(err)
-
- data := map[string]any{
- "group": groupresp.Group.ToStorageType().Data["name"],
- "baseUrl": h.config.GetString("base_url"),
- }
-
- if err := h.emailing.Send("onboarding.existing_administrator", r.FormValue("username"), data); err != nil {
- fmt.Println(err)
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/administration/groups/%s", groupid), http.StatusFound)
- return
- } else {
- // Onboard now administrator
- onboarding := map[string]any{
- "username": r.FormValue("username"),
- "group": groupid,
- "admin": true,
- }
-
- b := make([]byte, 16)
- if _, err := io.ReadFull(rand.Reader, b); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- key := base64.RawURLEncoding.EncodeToString(b)
-
- if err := h.cache.PutWithTTL("onboarding/"+key, onboarding, 168*time.Hour); err != nil { // 1 week TTL
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- data := map[string]any{
- "group": groupresp.Group.ToStorageType().Data["name"],
- "key": key,
- "baseUrl": h.config.GetString("base_url"),
- }
-
- if err := h.emailing.Send("onboarding.new_administrator", r.FormValue("username"), data); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/administration/groups/%s", groupid), http.StatusFound)
-}
-
-func (h *ApplicationHandler) AdministrationGroupInviteMember(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- groupid := vars["groupid"]
-
- groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), &groupsmanagement.GetGroupRequest{
- Id: groupid,
- Namespace: "parcoursmob_organizations",
- })
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- group := groupresp.Group.ToStorageType()
-
- r.ParseForm()
-
- accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &accounts.GetAccountUsernameRequest{
- Username: r.FormValue("username"),
- Namespace: "parcoursmob",
- })
-
- if err == nil {
- account := accountresp.Account.ToStorageType()
- account.Data["groups"] = append(account.Data["groups"].([]any), group.ID)
-
- as, _ := accounts.AccountFromStorageType(&account)
-
- _, err = h.services.GRPC.MobilityAccounts.UpdateData(
- context.TODO(),
- &accounts.UpdateDataRequest{
- Account: as,
- },
- )
-
- fmt.Println(err)
-
- data := map[string]any{
- "group": group.Data["name"],
- "baseUrl": h.config.GetString("base_url"),
- }
-
- if err := h.emailing.Send("onboarding.existing_member", r.FormValue("username"), data); err != nil {
- fmt.Println(err)
- }
-
- http.Redirect(w, r, "/app/group/settings", http.StatusFound)
- return
- } else {
- // Onboard now administrator
- onboarding := map[string]any{
- "username": r.FormValue("username"),
- "group": group.ID,
- "admin": false,
- }
-
- b := make([]byte, 16)
- if _, err := io.ReadFull(rand.Reader, b); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- key := base64.RawURLEncoding.EncodeToString(b)
-
- h.cache.PutWithTTL("onboarding/"+key, onboarding, 168*time.Hour) // 1 week TTL
-
- data := map[string]any{
- "group": group.Data["name"],
- "key": key,
- "baseUrl": h.config.GetString("base_url"),
- }
-
- if err := h.emailing.Send("onboarding.new_member", r.FormValue("username"), data); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- }
-
- http.Redirect(w, r, "/app/administration/groups/"+group.ID, http.StatusFound)
- return
-}
-
-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) AdminStatVehicles(w http.ResponseWriter, r *http.Request) {
-
- bookings := []fleetsstorage.Booking{}
- administrators := []string{}
- reequest := &fleets.GetVehiclesRequest{
- Namespaces: []string{"parcoursmob"},
- }
- reesp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), reequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- }
-
- vehicles := []fleetsstorage.Vehicle{}
- for _, vehiicle := range reesp.Vehicles {
-
- v := vehiicle.ToStorageType()
- adminfound := false
- for _, a := range administrators {
- if a == v.Administrators[0] {
- adminfound = true
- break
- }
- }
- if !adminfound {
- 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 {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- for _, g := range admingroups.Groups {
- groups[g.Id] = g.ToStorageType()
- }
-
- }
- sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
- sort.Sort(sorting.BookingsByStartdate(bookings))
- h.Renderer.AdminStatVehicles(w, r, vehicles, bookings, groups)
-}
-
-func (h ApplicationHandler) AdminStatBookings(w http.ResponseWriter, r *http.Request) {
-
- vehicles := map[string]fleetsstorage.Vehicle{}
- bookings := []fleetsstorage.Booking{}
-
- reequest := &fleets.GetVehiclesRequest{
- Namespaces: []string{"parcoursmob"},
- }
- reesp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), reequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiaries_ids := []string{}
-
- for _, vehicle := range reesp.Vehicles {
-
- v := vehicle.ToStorageType()
-
- for _, b := range v.Bookings {
- 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 {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- 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 {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiaries_map := map[string]any{}
- for _, ben := range beneficiaries.Accounts {
- beneficiaries_map[ben.Id] = ben.ToStorageType()
- }
-
- sort.Sort(sorting.BookingsByStartdate(bookings))
- h.Renderer.AdminStatBookings(w, r, vehicles, bookings, groups, beneficiaries_map)
-}
-
-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 {
- fmt.Println(err)
- 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
-}
-
-func (h ApplicationHandler) AdminStatBeneficaires(w http.ResponseWriter, r *http.Request) {
- beneficiaries, err := h.services.GetBeneficiaries()
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- cacheid := uuid.NewString()
- h.cache.PutWithTTL(cacheid, beneficiaries, 1*time.Hour)
- h.Renderer.AdminStatBeneficaires(w, r, beneficiaries, cacheid)
-}
-
-func (h ApplicationHandler) AdminStatEvents(w http.ResponseWriter, r *http.Request) {
- resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
- Namespaces: []string{"parcoursmob_dispositifs"},
- })
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- responses := []agendastorage.Event{}
-
- groupids := []string{}
- for _, e := range resp.Events {
- groupids = append(groupids, e.Owners...)
- responses = append(responses, e.ToStorageType())
- }
-
- sort.Sort(sorting.EventsByStartdate(responses))
-
- groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
- Groupids: groupids,
- })
- groups := map[string]any{}
-
- if err == nil {
- for _, g := range groupsresp.Groups {
- groups[g.Id] = g.ToStorageType()
- }
- }
- h.Renderer.AdminStatEvents(w, r, responses, groups)
-}
diff --git a/handlers/application/agenda.go b/handlers/application/agenda.go
deleted file mode 100644
index 70a1095..0000000
--- a/handlers/application/agenda.go
+++ /dev/null
@@ -1,687 +0,0 @@
-package application
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "sort"
- "strconv"
- "strings"
- "time"
-
- formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/utils/form-validators"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
- agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
- agendastorage "git.coopgo.io/coopgo-platform/agenda/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"
- "github.com/gorilla/mux"
- "google.golang.org/protobuf/types/known/structpb"
- "google.golang.org/protobuf/types/known/timestamppb"
-)
-
-type EventsForm struct {
- Name string `json:"name" validate:"required"`
- Type string `json:"type" validate:"required"`
- Description string `json:"description"`
- Address any `json:"address,omitempty"`
- Allday bool `json:"allday"`
- Startdate *time.Time `json:"startdate"`
- Enddate *time.Time `json:"enddate"`
- Starttime string `json:"starttime"`
- Endtime string `json:"endtime"`
- MaxSubscribers int `json:"max_subscribers"`
-}
-
-func (h *ApplicationHandler) AgendaHome(w http.ResponseWriter, r *http.Request) {
- resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
- Namespaces: []string{"parcoursmob_dispositifs"},
- Mindate: timestamppb.New(time.Now().Add(-24 * time.Hour)),
- })
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- responses := []agendastorage.Event{}
-
- groupids := []string{}
- for _, e := range resp.Events {
- groupids = append(groupids, e.Owners...)
- responses = append(responses, e.ToStorageType())
- }
-
- sort.Sort(sorting.EventsByStartdate(responses))
-
- groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
- Groupids: groupids,
- })
- groups := map[string]any{}
-
- if err == nil {
- for _, g := range groupsresp.Groups {
- groups[g.Id] = g.ToStorageType()
- }
- }
- h.Renderer.AgendaHome(w, r, responses, groups)
-}
-
-func (h *ApplicationHandler) AgendaHistory(w http.ResponseWriter, r *http.Request) {
- resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
- Namespaces: []string{"parcoursmob_dispositifs"},
- //Maxdate: timestamppb.New(time.Now().Add(24 * time.Hour)),
- })
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- responses := []agendastorage.Event{}
-
- groupids := []string{}
- for _, e := range resp.Events {
- groupids = append(groupids, e.Owners...)
- responses = append(responses, e.ToStorageType())
- }
-
- sort.Sort(sorting.EventsByStartdate(responses))
-
- groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
- Groupids: groupids,
- })
- groups := map[string]any{}
-
- if err == nil {
- for _, g := range groupsresp.Groups {
- groups[g.Id] = g.ToStorageType()
- }
- }
- h.Renderer.AgendaHistory(w, r, responses, groups)
-}
-
-func (h *ApplicationHandler) AgendaCreateEvent(w http.ResponseWriter, r *http.Request) {
- if r.Method == "POST" {
- // Get current group
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(storage.Group)
-
- eventForm, err := parseEventsForm(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- data, _ := structpb.NewStruct(map[string]any{
- "address": eventForm.Address,
- })
-
- request := &agenda.CreateEventRequest{
- Event: &agenda.Event{
- Namespace: "parcoursmob_dispositifs",
- Owners: []string{group.ID},
- Type: eventForm.Type,
- Name: eventForm.Name,
- Description: eventForm.Description,
- Startdate: timestamppb.New(*eventForm.Startdate),
- Enddate: timestamppb.New(*eventForm.Enddate),
- Starttime: eventForm.Starttime,
- Endtime: eventForm.Endtime,
- Allday: eventForm.Allday,
- MaxSubscribers: int64(eventForm.MaxSubscribers),
- Data: data,
- Deleted: false,
- },
- }
-
- resp, err := h.services.GRPC.Agenda.CreateEvent(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", resp.Event.Id), http.StatusFound)
- return
- }
- h.Renderer.AgendaCreateEvent(w, r)
-}
-
-func (h *ApplicationHandler) AgendaDisplayEvent(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- eventid := vars["eventid"]
-
- request := &agenda.GetEventRequest{
- Id: eventid,
- }
-
- resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- grouprequest := &groupsmanagement.GetGroupRequest{
- Id: resp.Event.Owners[0],
- }
-
- groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), grouprequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- subscribers := map[string]any{}
-
- accids := []string{}
- for _, v := range resp.Event.Subscriptions {
- accids = append(accids, v.Subscriber)
- }
-
- subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
- context.TODO(),
- &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: accids,
- },
- )
-
- if err == nil {
- for _, sub := range subscriberresp.Accounts {
- subscribers[sub.Id] = sub.ToStorageType()
- }
- }
-
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- group := g.(storage.Group)
-
- accountids := []string{}
- for _, m := range group.Members {
- if !contains(resp.Event.Subscriptions, m) {
- accountids = append(accountids, m)
- }
- }
-
- accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
- context.TODO(),
- &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: accountids,
- },
- )
-
- accounts := []any{}
-
- if err == nil {
- for _, acc := range accountresp.Accounts {
- accounts = append(accounts, acc)
- }
- }
-
- h.Renderer.AgendaDisplayEvent(w, r, resp.Event.ToStorageType(), groupresp.Group.ToStorageType(), subscribers, accounts)
-}
-
-func (h *ApplicationHandler) AgendaSubscribeEvent(w http.ResponseWriter, r *http.Request) {
- current_group, err := h.currentGroup(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- current_user_token, current_user_claims, err := h.currentUser(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- vars := mux.Vars(r)
- eventid := vars["eventid"]
-
- if err := r.ParseForm(); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- subscriber := r.FormValue("subscriber")
- data := map[string]any{
- "subscribed_by": map[string]any{
- "user": map[string]any{
- "id": current_user_token.Subject,
- "display_name": current_user_claims["first_name"].(string) + " " + current_user_claims["last_name"].(string),
- "email": current_user_claims["email"].(string),
- },
- "group": map[string]any{
- "id": current_group.ID,
- "name": current_group.Data["name"],
- },
- },
- }
- datapb, err := structpb.NewStruct(data)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request := &agenda.SubscribeEventRequest{
- Eventid: eventid,
- Subscriber: subscriber,
- Data: datapb,
- }
-
- _, err = h.services.GRPC.Agenda.SubscribeEvent(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventid), http.StatusFound)
-}
-
-func parseEventsForm(r *http.Request) (*EventsForm, error) {
- if err := r.ParseForm(); err != nil {
- return nil, err
- }
-
- var startdate *time.Time
- var enddate *time.Time
-
- if r.PostFormValue("startdate") != "" {
- d, err := time.Parse("2006-01-02", r.PostFormValue("startdate"))
- if err != nil {
- return nil, err
- }
- startdate = &d
- }
-
- if r.PostFormValue("enddate") != "" {
- d, err := time.Parse("2006-01-02", r.PostFormValue("enddate"))
- if err != nil {
- return nil, err
- }
- enddate = &d
- }
-
- max_subscribers, err := strconv.Atoi(r.PostFormValue("max_subscribers"))
- if err != nil {
- return nil, err
- }
-
- formData := &EventsForm{
- Name: r.PostFormValue("name"),
- Type: r.PostFormValue("type"),
- Description: r.PostFormValue("description"),
- Startdate: startdate,
- Enddate: enddate,
- Starttime: r.PostFormValue("starttime"),
- Endtime: r.PostFormValue("endtime"),
- MaxSubscribers: max_subscribers,
- }
-
- if r.PostFormValue("allday") == "true" {
- formData.Allday = true
- }
-
- if r.PostFormValue("address") != "" {
- var a any
- json.Unmarshal([]byte(r.PostFormValue("address")), &a)
-
- formData.Address = a
- }
-
- validate := formvalidators.New()
- if err := validate.Struct(formData); err != nil {
- return nil, err
- }
-
- return formData, nil
-}
-
-func contains(s []*agenda.Subscription, e string) bool {
- for _, a := range s {
- if a.Subscriber == e {
- return true
- }
- }
- return false
-}
-
-///////////////////////////////Update Event/////////////////////////////////////////
-func (h *ApplicationHandler) AgendaUpdateEvent(w http.ResponseWriter, r *http.Request) {
- adm := strings.Split(r.URL.Path, "/")
- eventID := adm[3]
- request := &agenda.GetEventRequest{
- Id: eventID,
- }
-
- resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- if r.Method == "POST" {
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(storage.Group)
-
- eventForm, err := parseEventsForm(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- data, _ := structpb.NewStruct(map[string]any{
- "address": eventForm.Address,
- })
-
- request := &agenda.UpdateEventRequest{
- Event: &agenda.Event{
- Namespace: "parcoursmob_dispositifs",
- Id: eventID,
- Owners: []string{group.ID},
- Type: eventForm.Type,
- Name: eventForm.Name,
- Description: eventForm.Description,
- Startdate: timestamppb.New(*eventForm.Startdate),
- Enddate: timestamppb.New(*eventForm.Enddate),
- Starttime: eventForm.Starttime,
- Endtime: eventForm.Endtime,
- Allday: eventForm.Allday,
- MaxSubscribers: int64(eventForm.MaxSubscribers),
- Data: data,
- Subscriptions: resp.Event.Subscriptions,
- },
- }
-
- resp, err := h.services.GRPC.Agenda.UpdateEvent(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", resp.Event.Id), http.StatusFound)
- return
- }
- h.Renderer.AgendaUpdateEvent(w, r, resp.Event.ToStorageType())
-}
-
-func (h *ApplicationHandler) AgendaDeleteEvent(w http.ResponseWriter, r *http.Request) {
-
- vars := mux.Vars(r)
- eventID := vars["eventid"]
- request := &agenda.GetEventRequest{
- Id: eventID,
- }
-
- resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if r.Method == "POST" {
-
- request := &agenda.UpdateEventRequest{
- Event: &agenda.Event{
- Namespace: resp.Event.Namespace,
- Id: resp.Event.Id,
- Owners: resp.Event.Owners,
- Type: resp.Event.Type,
- Name: resp.Event.Name,
- Description: resp.Event.Description,
- Startdate: resp.Event.Startdate,
- Enddate: resp.Event.Enddate,
- Starttime: resp.Event.Starttime,
- Endtime: resp.Event.Endtime,
- Allday: resp.Event.Allday,
- MaxSubscribers: int64(resp.Event.MaxSubscribers),
- Data: resp.Event.Data,
- Subscriptions: resp.Event.Subscriptions,
- Deleted: true,
- },
- }
-
- _, err := h.services.GRPC.Agenda.UpdateEvent(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, "/app/agenda/", http.StatusFound)
- return
- }
- h.Renderer.AgendaDeleteEvent(w, r, resp.Event.ToStorageType())
-}
-
-///////////////////////////Delete subscriber///////////////////////////////
-func (h *ApplicationHandler) AgendaDeleteSubscribeEvent(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- eventId := vars["eventid"]
- subscribeid := vars["subscribeid"]
- s_b_id := ""
- s_b_name := ""
- s_b_email := ""
- s_b_group_id := ""
- s_b_group_name := ""
- request := &agenda.GetEventRequest{
- Id: eventId,
- }
-
- resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- for i := range resp.Event.Subscriptions {
- if resp.Event.Subscriptions[i].Subscriber == subscribeid {
- subscribed_by_id := resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["id"].GetStringValue()
- subscribed_by_name := resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["display_name"].GetStringValue()
- subscribed_by_email := resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["email"].GetStringValue()
- subscribed_by_group_id := resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["id"].GetStringValue()
- subscribed_by_group_name := resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["name"].GetStringValue()
- s_b_id = subscribed_by_id
- s_b_name = subscribed_by_name
- s_b_email = subscribed_by_email
- s_b_group_id = subscribed_by_group_id
- s_b_group_name = subscribed_by_group_name
- }
- }
-
- current_group, err := h.currentGroup(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- current_user_token, current_user_claims, err := h.currentUser(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- data := map[string]any{
- "subscribed_by": map[string]any{
- "user": map[string]any{
- "id": s_b_id,
- "display_name": s_b_name,
- "email": s_b_email,
- },
- "group": map[string]any{
- "id": s_b_group_id,
- "name": s_b_group_name,
- },
- },
- "unsubscribed_by": map[string]any{
- "user": map[string]any{
- "id": current_user_token.Subject,
- "display_name": current_user_claims["first_name"].(string) + " " + current_user_claims["last_name"].(string),
- "email": current_user_claims["email"],
- },
- "group": map[string]any{
- "id": current_group.ID,
- "name": current_group.Data["name"],
- },
- },
- "motif": r.FormValue("motif"),
- }
-
- datapb, err := structpb.NewStruct(data)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if r.Method == "POST" {
- request := &agenda.DeleteSubscriptionRequest{
- Subscriber: subscribeid,
- Eventid: eventId,
- Data: datapb,
- }
-
- data := map[string]any{
- "motif": r.FormValue("motif"),
- "user": current_user_claims["first_name"].(string) + " " + current_user_claims["last_name"].(string),
- "subscriber": fmt.Sprintf("http://localhost:9000/app/beneficiaries/%s", subscribeid),
- "link": fmt.Sprintf("http://localhost:9000/app/agenda/%s", eventId),
- }
-
- // récupérer l'adresse mail de l'utilisateur qui a créé l'événement
- mail := s_b_email
- fmt.Println(mail)
-
- _, err := h.services.GRPC.Agenda.DeleteSubscription(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if err := h.emailing.Send("delete_subscriber.request", mail, data); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventId), http.StatusFound)
- return
- }
- h.Renderer.AgendaDeleteSubscribeEvent(w, r, eventId)
-}
-
-////////////////////////////////////////////////////////
-// /////////////////////History Event////////////////////////
-func (h *ApplicationHandler) AgendaHistoryEvent(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- eventId := vars["eventid"]
- request := &agenda.GetEventRequest{
- Id: eventId,
- }
-
- resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- grouprequest := &groupsmanagement.GetGroupRequest{
- Id: resp.Event.Owners[0],
- }
-
- groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), grouprequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- subscribers := map[string]any{}
-
- accids := []string{}
- for _, v := range resp.Event.DeletedSubscription {
- accids = append(accids, v.Subscriber)
- }
-
- subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
- context.TODO(),
- &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: accids,
- },
- )
-
- if err == nil {
- for _, sub := range subscriberresp.Accounts {
- subscribers[sub.Id] = sub.ToStorageType()
- }
- }
-
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- group := g.(storage.Group)
-
- accountids := []string{}
- for _, m := range group.Members {
- if !contains(resp.Event.DeletedSubscription, m) {
- accountids = append(accountids, m)
- }
- }
-
- accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
- context.TODO(),
- &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: accountids,
- },
- )
-
- accounts := []any{}
-
- if err == nil {
- for _, acc := range accountresp.Accounts {
- accounts = append(accounts, acc)
- }
- }
-
- h.Renderer.AgendaHistoryEvent(w, r, resp.Event.ToStorageType(), groupresp.Group.ToStorageType(), subscribers, accounts)
-}
diff --git a/handlers/application/beneficiaries.go b/handlers/application/beneficiaries.go
deleted file mode 100644
index bfbc30f..0000000
--- a/handlers/application/beneficiaries.go
+++ /dev/null
@@ -1,413 +0,0 @@
-package application
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "image/png"
- "io"
- "log"
- "net/http"
- "sort"
- "strconv"
- "strings"
- "time"
-
- formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/utils/form-validators"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- profilepictures "git.coopgo.io/coopgo-apps/parcoursmob/utils/profile-pictures"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
- filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
- fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
- 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/gorilla/mux"
- "google.golang.org/protobuf/types/known/structpb"
-)
-
-type BeneficiariesForm struct {
- FirstName string `json:"first_name" validate:"required"`
- LastName string `json:"last_name" validate:"required"`
- Email string `json:"email" validate:"required,email"`
- Birthdate *time.Time `json:"birthdate" validate:"required"`
- PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
- FileNumber string `json:"file_number"`
- Address any `json:"address,omitempty"`
- Gender string `json:"gender"`
-}
-
-func (h *ApplicationHandler) BeneficiariesList(w http.ResponseWriter, r *http.Request) {
-
- accounts, err := h.beneficiaries(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- sort.Sort(sorting.BeneficiariesByName(accounts))
-
- cacheid := uuid.NewString()
- h.cache.PutWithTTL(cacheid, accounts, 1*time.Hour)
- h.Renderer.BeneficiariesList(w, r, accounts, cacheid)
-}
-
-func (h *ApplicationHandler) BeneficiaryCreate(w http.ResponseWriter, r *http.Request) {
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- fmt.Println("Create beneficiary : could not find group")
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(storage.Group)
-
- if r.Method == "POST" {
-
- dataMap, err := parseBeneficiariesForm(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request := &mobilityaccounts.RegisterRequest{
- Account: &mobilityaccounts.Account{
- Namespace: "parcoursmob_beneficiaries",
- Data: data.GetStructValue(),
- },
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.Register(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- subscribe := &groupsmanagement.SubscribeRequest{
- Groupid: group.ID,
- Memberid: resp.Account.Id,
- }
-
- _, err = h.services.GRPC.GroupsManagement.Subscribe(context.TODO(), subscribe)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", resp.Account.Id), http.StatusFound)
-
- return
- }
- h.Renderer.BeneficiaryCreate(w, r)
-}
-
-func (h *ApplicationHandler) BeneficiaryDisplay(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- beneficiaryID := vars["beneficiaryid"]
-
- documents := h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiaryID)
-
- request := &mobilityaccounts.GetAccountRequest{
- Id: beneficiaryID,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- bookingsrequest := &fleets.GetDriverBookingsRequest{
- Driver: beneficiaryID,
- }
- bookingsresp, err := h.services.GRPC.Fleets.GetDriverBookings(context.TODO(), bookingsrequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- bookings := []any{}
-
- for _, b := range bookingsresp.Bookings {
- bookings = append(bookings, b.ToStorageType())
- }
-
- groupsrequest := &groupsmanagement.GetGroupsRequest{
- Namespaces: []string{"parcoursmob_organizations"},
- Member: beneficiaryID,
- }
-
- groupsresp, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), groupsrequest)
- if err != nil {
- fmt.Println(err)
- }
-
- organizations := []any{}
- for _, o := range groupsresp.Groups {
- organizations = append(organizations, o.ToStorageType())
- }
-
- beneficiaries_file_types := h.config.GetStringSlice("modules.beneficiaries.documents_types")
- file_types_map := h.config.GetStringMapString("storage.files.file_types")
-
- h.Renderer.BeneficiaryDisplay(w, r, resp.Account.ToStorageType(), bookings, organizations, beneficiaries_file_types, file_types_map, documents)
-}
-
-func (h *ApplicationHandler) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- beneficiaryID := vars["beneficiaryid"]
-
- if r.Method == "POST" {
-
- dataMap, err := parseBeneficiariesForm(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request := &mobilityaccounts.UpdateDataRequest{
- Account: &mobilityaccounts.Account{
- Id: beneficiaryID,
- Namespace: "parcoursmob_beneficiaries",
- Data: data.GetStructValue(),
- },
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.UpdateData(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", resp.Account.Id), http.StatusFound)
-
- return
- }
-
- request := &mobilityaccounts.GetAccountRequest{
- Id: beneficiaryID,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- //TODO filter namespaces
- //TODO filter groups
-
- h.Renderer.BeneficiaryUpdate(w, r, resp.Account.ToStorageType())
-}
-
-func (h *ApplicationHandler) BeneficiaryPicture(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- beneficiaryID := vars["beneficiaryid"]
-
- request := &mobilityaccounts.GetAccountRequest{
- Id: beneficiaryID,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- account := resp.Account.ToStorageType()
-
- firstName := account.Data["first_name"].(string)
- lastName := account.Data["last_name"].(string)
- picture := profilepictures.DefaultProfilePicture(strings.ToUpper(firstName[0:1] + lastName[0:1]))
-
- buffer := new(bytes.Buffer)
- if err := png.Encode(buffer, picture); err != nil {
- log.Println("unable to encode image.")
- }
-
- w.Header().Set("Content-Type", "image/png")
- w.Header().Set("Content-Length", strconv.Itoa(len(buffer.Bytes())))
- if _, err := w.Write(buffer.Bytes()); err != nil {
- log.Println("unable to write image.")
- }
-}
-
-func (h *ApplicationHandler) BeneficiaryDocuments(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- beneficiaryID := vars["beneficiaryid"]
-
- //r.ParseForm()
- r.ParseMultipartForm(100 * 1024 * 1024)
-
- document_type := r.FormValue("type")
- document_name := r.FormValue("name")
-
- file, header, err := r.FormFile("file-upload")
- if err != nil {
- fmt.Println(err)
- return
- }
- defer file.Close()
-
- fileid := uuid.NewString()
-
- metadata := map[string]string{
- "type": document_type,
- "name": document_name,
- }
-
- if err := h.filestorage.Put(file, filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s_%s", beneficiaryID, fileid, header.Filename), header.Size, metadata); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
-
-}
-
-func (h *ApplicationHandler) BeneficiaryDocumentDownload(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- beneficiaryID := vars["beneficiaryid"]
- document := vars["document"]
-
- file, info, err := h.filestorage.Get(filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s", beneficiaryID, document))
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Content-Type", info.ContentType)
- if _, err = io.Copy(w, file); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
-
-}
-
-func filterAccount(r *http.Request, a *mobilityaccounts.Account) bool {
- searchFilter, ok := r.URL.Query()["search"]
-
- if ok && len(searchFilter[0]) > 0 {
- name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
- if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter[0])) {
- return false
- }
- }
-
- return true
-}
-
-func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]mobilityaccountsstorage.Account, error) {
- var accounts = []mobilityaccountsstorage.Account{}
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- return accounts, errors.New("no group provided")
- }
-
- group := g.(storage.Group)
-
- request := &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: group.Members,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), request)
- if err != nil {
- return accounts, err
- }
-
- for _, account := range resp.Accounts {
- if filterAccount(r, account) {
- a := account.ToStorageType()
- accounts = append(accounts, a)
- }
- }
-
- return accounts, err
-}
-
-func parseBeneficiariesForm(r *http.Request) (map[string]any, error) {
- if err := r.ParseForm(); err != nil {
- return nil, err
- }
-
- var date *time.Time
-
- if r.PostFormValue("birthdate") != "" {
- d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate"))
- if err != nil {
- return nil, err
- }
- date = &d
- }
-
- formData := BeneficiariesForm{
- FirstName: r.PostFormValue("first_name"),
- LastName: r.PostFormValue("last_name"),
- Email: r.PostFormValue("email"),
- Birthdate: date,
- PhoneNumber: r.PostFormValue("phone_number"),
- FileNumber: r.PostFormValue("file_number"),
- Gender: r.PostFormValue("gender"),
- }
-
- if r.PostFormValue("address") != "" {
- var a any
- json.Unmarshal([]byte(r.PostFormValue("address")), &a)
-
- formData.Address = a
- }
-
- validate := formvalidators.New()
- if err := validate.Struct(formData); err != nil {
- return nil, err
- }
-
- d, err := json.Marshal(formData)
- if err != nil {
- return nil, err
- }
-
- var dataMap map[string]any
- err = json.Unmarshal(d, &dataMap)
- if err != nil {
- return nil, err
- }
-
- return dataMap, nil
-}
diff --git a/handlers/application/dashboard.go b/handlers/application/dashboard.go
deleted file mode 100644
index 9a761a5..0000000
--- a/handlers/application/dashboard.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package application
-
-import (
- "context"
- "fmt"
- "net/http"
- "sort"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
- agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
- agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
- "git.coopgo.io/coopgo-platform/groups-management/storage"
- mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
- "google.golang.org/protobuf/types/known/timestamppb"
-)
-
-func (h *ApplicationHandler) Dashboard(w http.ResponseWriter, r *http.Request) {
-
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(storage.Group)
-
- request := &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: group.Members,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var accounts = []any{}
-
- // We only display the 10 last here
- count := len(resp.Accounts)
- min := count - 5
- if min < 0 {
- min = 0
- }
-
- for _, account := range resp.Accounts[min:] {
- if filterAccount(r, account) {
- fmt.Println(account)
- a := account.ToStorageType()
- accounts = append([]any{a}, accounts...)
- }
- }
-
- members, _, err := h.groupmembers(group.ID)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- count_members := len(members)
-
- events := []agendastorage.Event{}
-
- eventsresp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
- Namespaces: []string{"parcoursmob_dispositifs"},
- Mindate: timestamppb.Now(),
- })
-
- for _, e := range eventsresp.Events {
- events = append(events, e.ToStorageType())
- }
-
- sort.Sort(sorting.EventsByStartdate(events))
-
- h.Renderer.Dashboard(w, r, accounts, count, count_members, events)
-
-}
diff --git a/handlers/application/directory.go b/handlers/application/directory.go
deleted file mode 100644
index d58478f..0000000
--- a/handlers/application/directory.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package application
-
-import "net/http"
-
-func (h *ApplicationHandler) DirectoryHome(w http.ResponseWriter, r *http.Request) {
- h.Renderer.DirectoryHome(w, r)
-}
diff --git a/handlers/application/group.go b/handlers/application/group.go
deleted file mode 100644
index 7de3307..0000000
--- a/handlers/application/group.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package application
-
-import (
- "context"
- "crypto/rand"
- "encoding/base64"
- "fmt"
- "io"
- "net/http"
- "time"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- "git.coopgo.io/coopgo-platform/groups-management/storage"
- accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
-)
-
-func (h *ApplicationHandler) GroupSettingsDisplay(w http.ResponseWriter, r *http.Request) {
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(storage.Group)
-
- members, err := h.members()
- if err != nil {
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- }
-
- admins := []any{}
- groupMembers := []any{}
-
- for _, m := range members {
- mm := m.ToStorageType()
- for _, g := range mm.Data["groups"].([]any) {
- if g.(string) == group.ID {
- groupMembers = append(groupMembers, mm)
- }
- if g.(string) == group.ID+":admin" {
- admins = append(admins, mm)
- }
- }
- }
-
- h.Renderer.GroupSettingsDisplay(w, r, group, groupMembers, admins)
-}
-
-func (h *ApplicationHandler) GroupSettingsInviteMember(w http.ResponseWriter, r *http.Request) {
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(storage.Group)
-
- r.ParseForm()
-
- accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &accounts.GetAccountUsernameRequest{
- Username: r.FormValue("username"),
- Namespace: "parcoursmob",
- })
-
- if err == nil {
- // Account already exists : adding the existing account to admin list
- account := accountresp.Account.ToStorageType()
- //account.Data["groups"] = append(account.Data["groups"].([]any), groupid, groupid)
- account.Data["groups"] = append(account.Data["groups"].([]any), group.ID)
-
- as, _ := accounts.AccountFromStorageType(&account)
-
- _, err = h.services.GRPC.MobilityAccounts.UpdateData(
- context.TODO(),
- &accounts.UpdateDataRequest{
- Account: as,
- },
- )
-
- fmt.Println(err)
-
- data := map[string]any{
- "group": group.Data["name"],
- }
-
- if err := h.emailing.Send("onboarding.existing_member", r.FormValue("username"), data); err != nil {
- fmt.Println(err)
- }
-
- http.Redirect(w, r, "/app/group/settings", http.StatusFound)
- return
- } else {
- // Onboard now administrator
- onboarding := map[string]any{
- "username": r.FormValue("username"),
- "group": group.ID,
- "admin": false,
- }
-
- b := make([]byte, 16)
- if _, err := io.ReadFull(rand.Reader, b); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- key := base64.RawURLEncoding.EncodeToString(b)
-
- h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour)
-
- data := map[string]any{
- "group": group.Data["name"],
- "key": key,
- }
-
- if err := h.emailing.Send("onboarding.new_member", r.FormValue("username"), data); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- }
-
- http.Redirect(w, r, "/app/group/settings", http.StatusFound)
- return
-}
diff --git a/handlers/application/group_module.go b/handlers/application/group_module.go
deleted file mode 100644
index e6da0aa..0000000
--- a/handlers/application/group_module.go
+++ /dev/null
@@ -1,221 +0,0 @@
-package application
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "sort"
- "strings"
- "time"
-
- groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
- groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
- mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
- "github.com/google/uuid"
- "github.com/gorilla/mux"
- "google.golang.org/protobuf/types/known/structpb"
-)
-
-var Addres any
-
-type BeneficiariesGroupForm struct {
- FirstName string `json:"first_name" validate:"required"`
- LastName string `json:"last_name" validate:"required"`
- Email string `json:"email" validate:"required,email"`
- Birthdate *time.Time `json:"birthdate"`
- PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
- Address any `json:"address,omitempty"`
- Gender string `json:"gender"`
-}
-
-type GroupsModuleByName []groupstorage.Group
-
-func (a GroupsModuleByName) Len() int { return len(a) }
-func (a GroupsModuleByName) Less(i, j int) bool {
- return strings.Compare(a[i].Data["name"].(string), a[j].Data["name"].(string)) < 0
-}
-func (a GroupsModuleByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-
-func (h *ApplicationHandler) Groups(w http.ResponseWriter, r *http.Request) {
-
- request := &groupsmanagement.GetGroupsRequest{
- Namespaces: []string{"parcoursmob_groups"},
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var groups = []groupstorage.Group{}
-
- for _, group := range resp.Groups {
- g := group.ToStorageType()
- groups = append(groups, g)
- }
-
- sort.Sort(GroupsModuleByName(groups))
-
- h.Renderer.Groups(w, r, groups)
-}
-
-func (h *ApplicationHandler) CreateGroupModule(w http.ResponseWriter, r *http.Request) {
- if r.Method == "POST" {
- if r.PostFormValue("address") != "" {
- var a any
- json.Unmarshal([]byte(r.PostFormValue("address")), &a)
-
- Addres = a
- }
- r.ParseForm()
-
- if r.FormValue("name") == "" {
-
- fmt.Println("invalid name")
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- if r.FormValue("type") == "" {
-
- fmt.Println("invalid type")
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- groupid := uuid.NewString()
-
- dataMap := map[string]any{
- "name": r.FormValue("name"),
- "type": r.FormValue("type"),
- "description": r.FormValue("description"),
- "address": Addres,
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request_organization := &groupsmanagement.AddGroupRequest{
- Group: &groupsmanagement.Group{
- Id: groupid,
- Namespace: "parcoursmob_groups",
- Data: data.GetStructValue(),
- },
- }
-
- _, err = h.services.GRPC.GroupsManagement.AddGroup(context.TODO(), request_organization)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/group_module/groups/%s", groupid), http.StatusFound)
- return
- }
- group_types := h.config.GetStringSlice("modules.groups.group_types")
- h.Renderer.CreateGroupModule(w, r, group_types)
-}
-
-func filterAcccount(r *http.Request, a *mobilityaccounts.Account) bool {
- searchFilter, ok := r.URL.Query()["search"]
-
- if ok && len(searchFilter[0]) > 0 {
- name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
- if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter[0])) {
- return false
- }
- }
-
- return true
-}
-func (h *ApplicationHandler) DisplayGroupModule(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- groupid := vars["groupid"]
-
- request := &groupsmanagement.GetGroupRequest{
- Id: groupid,
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var accounts = []any{}
-
- requesst := &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: resp.Group.Members,
- }
-
- ressp, _ := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), requesst)
- // if err != nil {
- // return err
- // }
- for _, account := range ressp.Accounts {
- if filterAcccount(r, account) {
- a := account.ToStorageType()
- accounts = append(accounts, a)
- }
- }
-
- cacheid := uuid.NewString()
- h.cache.PutWithTTL(cacheid, accounts, 1*time.Hour)
- r.ParseForm()
-
- var beneficiary any
-
- searched := false
-
- // if r.Method == "POST" {
- if r.FormValue("beneficiaryid") != "" {
- // Handler form
- searched = true
-
- requestbeneficiary := &mobilityaccounts.GetAccountRequest{
- Id: r.FormValue("beneficiaryid"),
- }
-
- respbeneficiary, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), requestbeneficiary)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiary = respbeneficiary.Account.ToStorageType()
-
- subscribe := &groupsmanagement.SubscribeRequest{
- Groupid: resp.Group.ToStorageType().ID,
- Memberid: respbeneficiary.Account.Id,
- }
-
- _, err = h.services.GRPC.GroupsManagement.Subscribe(context.TODO(), subscribe)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/group_module/groups/%s", resp.Group.ToStorageType().ID), http.StatusFound)
- return
- }
-
- accountsBeneficaire, err := h.beneficiaries(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- //h.Renderer.BeneficaireSearch(w, r, accounts, searched, beneficiary, resp.Group.ToStorageType())
- h.Renderer.DisplayGroupModule(w, r, resp.Group.ToStorageType().ID, accounts, cacheid, searched, beneficiary, resp.Group.ToStorageType(), accountsBeneficaire)
-}
diff --git a/handlers/application/journeys.go b/handlers/application/journeys.go
deleted file mode 100644
index c8f143b..0000000
--- a/handlers/application/journeys.go
+++ /dev/null
@@ -1,568 +0,0 @@
-package application
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "sort"
- "strconv"
- "strings"
- "time"
-
- fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
- groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
- groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
- mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
- "github.com/google/uuid"
- "github.com/gorilla/mux"
- geojson "github.com/paulmach/go.geojson"
- "gitlab.scity.coop/maas/navitia-golang"
- "gitlab.scity.coop/maas/navitia-golang/types"
- "google.golang.org/protobuf/types/known/structpb"
-)
-
-var Depart any
-var Arrive any
-
-func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
-
- locTime, errTime := time.LoadLocation("Europe/Paris")
- if errTime != nil {
- fmt.Println("Loading timezone location Europe/Paris error : ")
- fmt.Println("Missing zones in container ? ")
- panic(errTime)
- }
-
- departuredate := r.FormValue("departuredate")
- departuretime := r.FormValue("departuretime")
- departuredatetime, _ := time.ParseInLocation("2006-01-02 15:04", fmt.Sprintf("%s %s", departuredate, departuretime), locTime)
-
- departure := r.FormValue("departure")
- destination := r.FormValue("destination")
-
- searched := false
-
- var (
- departuregeo *geojson.Feature
- destinationgeo *geojson.Feature
- journeys *navitia.JourneyResults
- carpoolresults any
- vehicles = []any{}
- )
-
- if departuredate != "" && departuretime != "" && departure != "" && destination != "" {
- searched = true
-
- var err error
-
- departuregeo, err = geojson.UnmarshalFeature([]byte(departure))
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- destinationgeo, err = geojson.UnmarshalFeature([]byte(destination))
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- //TODO make it a library
- session, _ := navitia.NewCustom(
- h.config.GetString("services.navitia.api_key"),
- "https://api.navitia.io/v1",
- &http.Client{})
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- request := navitia.JourneyRequest{
- From: types.ID(fmt.Sprintf("%f", departuregeo.Geometry.Point[0]) + ";" + fmt.Sprintf("%f", departuregeo.Geometry.Point[1])),
- To: types.ID(fmt.Sprintf("%f", destinationgeo.Geometry.Point[0]) + ";" + fmt.Sprintf("%f", destinationgeo.Geometry.Point[1])),
- Date: departuredatetime.Add(-2 * time.Hour),
- DateIsArrival: false, //TODO
- }
-
- journeys, err = session.Journeys(context.Background(), request)
- if err != nil {
- fmt.Println(err)
- // w.WriteHeader(http.StatusBadRequest)
- // return
- }
-
- //CARPOOL
- // carpoolrequest := fmt.Sprintf(
- // "https://api.rdex.ridygo.fr/journeys.json?p[driver][state]=1&frequency=punctual&p[passenger][state]=0&p[from][latitude]=%f&p[from][longitude]=%f&p[to][latitude]=%f&p[to][longitude]=%f&p[outward][mindate]=%s&p[outward][maxdate]=%s",
- // departuregeo.Geometry.Point[1], departuregeo.Geometry.Point[0],
- // destinationgeo.Geometry.Point[1], destinationgeo.Geometry.Point[0],
- // departuredatetime.Format("2006-01-02"), departuredatetime.Add(24*time.Hour).Format("2006-01-02"))
- carpoolrequest := "https://api.rdex.ridygo.fr/journeys.json"
-
- client := &http.Client{}
- req, err := http.NewRequest("GET", carpoolrequest, nil)
- if err != nil {
- fmt.Println(err)
- }
-
- req.URL.RawQuery = fmt.Sprintf(
- "p[driver][state]=1&frequency=punctual&p[passenger][state]=0&p[from][latitude]=%f&p[from][longitude]=%f&p[to][latitude]=%f&p[to][longitude]=%f&p[outward][mindate]=%s&p[outward][maxdate]=%s",
- departuregeo.Geometry.Point[1], departuregeo.Geometry.Point[0],
- destinationgeo.Geometry.Point[1], destinationgeo.Geometry.Point[0],
- departuredatetime.Format("2006-01-02"), departuredatetime.Format("2006-01-02"))
-
- req.Header.Set("X-API-KEY", "123456")
- resp, err := client.Do(req)
- if err != nil {
- fmt.Println(err)
- }
-
- if err == nil && resp.StatusCode == http.StatusOK {
- err = json.NewDecoder(resp.Body).Decode(&carpoolresults)
- if err != nil {
- fmt.Println(err)
- }
-
- if carpoolresults == nil {
- carpoolresults = []any{}
- }
- } else {
- carpoolresults = []any{}
- }
-
- // Vehicles
-
- vehiclerequest := &fleets.GetVehiclesRequest{
- Namespaces: []string{"parcoursmob"},
- }
- vehicleresp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), vehiclerequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- }
-
- for _, vehicle := range vehicleresp.Vehicles {
- v := vehicle.ToStorageType()
- if v.Free(departuredatetime.Add(-24*time.Hour), departuredatetime.Add(168*time.Hour)) {
- vehicles = append(vehicles, v)
- }
- }
- }
-
- h.Renderer.JourneysSearch(w, r, carpoolresults, journeys, vehicles, searched, departuregeo, destinationgeo, departuredate, departuretime)
-}
-
-type GroupsModule []groupstorage.Group
-
-func (a GroupsModule) Len() int { return len(a) }
-func (a GroupsModule) Less(i, j int) bool {
- return strings.Compare(a[i].Data["name"].(string), a[j].Data["name"].(string)) < 0
-}
-func (a GroupsModule) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-
-func (h *ApplicationHandler) GroupsGestion(w http.ResponseWriter, r *http.Request) {
-
- request := &groupsmanagement.GetGroupsRequest{
- Namespaces: []string{"parcoursmob_groups_covoiturage"},
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var groups = []groupstorage.Group{}
-
- for _, group := range resp.Groups {
- g := group.ToStorageType()
- groups = append(groups, g)
- }
-
- cacheid := uuid.NewString()
-
- sort.Sort(GroupsModule(groups))
- h.cache.PutWithTTL(cacheid, groups, 1*time.Hour)
-
- h.Renderer.GroupsGestion(w, r, groups, cacheid)
-}
-func filterAcc(r *http.Request, a *mobilityaccounts.Account) bool {
- searchFilter, ok := r.URL.Query()["search"]
-
- if ok && len(searchFilter[0]) > 0 {
- name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
- if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter[0])) {
- return false
- }
- }
-
- return true
-}
-
-func (h *ApplicationHandler) CreateGroup(w http.ResponseWriter, r *http.Request) {
-
- var beneficiary any
- var (
- departurgeo *geojson.Feature
- dstinationgeo *geojson.Feature
- )
- searched := false
-
- if r.FormValue("beneficiaryid") != "" {
-
- searched = true
-
- requestbeneficiary := &mobilityaccounts.GetAccountRequest{
- Id: r.FormValue("beneficiaryid"),
- }
-
- respbeneficiary, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), requestbeneficiary)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiary = respbeneficiary.Account.ToStorageType()
-
- if r.Method == "POST" {
- departure := r.FormValue("departure")
- destination := r.FormValue("destination")
-
- if departure != "" && destination != "" {
-
- var err error
-
- departurgeo, err = geojson.UnmarshalFeature([]byte(departure))
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- dstinationgeo, err = geojson.UnmarshalFeature([]byte(destination))
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- }
- if r.FormValue("departure") != "" {
- var a any
- json.Unmarshal([]byte(r.FormValue("departure")), &a)
-
- Depart = a
- }
- if r.FormValue("destination") != "" {
- var a any
- json.Unmarshal([]byte(r.FormValue("destination")), &a)
-
- Arrive = a
- }
- r.ParseForm()
-
- if r.FormValue("name") == "" {
-
- fmt.Println("invalid name")
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- if r.FormValue("number") == "" {
-
- fmt.Println("invalid number of personne")
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- planDays := map[string]any{
- "lundi": r.FormValue("lundi") == "on",
- "mardi": r.FormValue("mardi") == "on",
- "mercredi": r.FormValue("mercredi") == "on",
- "jeudi": r.FormValue("jeudi") == "on",
- "vendredi": r.FormValue("vendredi") == "on",
- "samedi": r.FormValue("samedi") == "on",
- "dimanche": r.FormValue("dimanche") == "on",
- }
-
- groupidd := uuid.NewString()
-
- dataMap := map[string]any{
- "name": r.FormValue("name"),
- "number": r.FormValue("number"),
- "driver_first_name": respbeneficiary.Account.ToStorageType().Data["first_name"],
- "driver_last_name": respbeneficiary.Account.ToStorageType().Data["last_name"],
- "depart": Depart,
- "arrive": Arrive,
- "departdate": r.FormValue("departdate"),
- "date": r.FormValue("date"),
- "enddate": r.FormValue("enddate"),
- "departtime": r.FormValue("departtime"),
- "time": r.FormValue("time"),
-
- "planDays": planDays,
- "recurrent": r.FormValue("recurrent"),
- "pontuelle": r.FormValue("ponctuelle"),
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request_organization := &groupsmanagement.AddGroupRequest{
- Group: &groupsmanagement.Group{
- Id: groupidd,
- Namespace: "parcoursmob_groups_covoiturage",
- Data: data.GetStructValue(),
- },
- }
-
- _, err = h.services.GRPC.GroupsManagement.AddGroup(context.TODO(), request_organization)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/journeys/groups_covoiturage/create/%s", request_organization.Group.ToStorageType().ID), http.StatusFound)
-
- return
- }
- }
- accountsBeneficaire, err := h.beneficiaries(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- h.Renderer.CreateGroup(w, r, Depart, Arrive, searched, beneficiary, accountsBeneficaire, departurgeo, dstinationgeo)
-
-}
-
-func (h *ApplicationHandler) DisplayGroupCovoiturage(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- groupid := vars["groupid"]
-
- request := &groupsmanagement.GetGroupRequest{
- Id: groupid,
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var accounts = []any{}
-
- requesst := &mobilityaccounts.GetAccountsBatchRequest{
- Accountids: resp.Group.Members,
- }
-
- ressp, _ := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), requesst)
-
- for _, account := range ressp.Accounts {
- if filterAcc(r, account) {
- a := account.ToStorageType()
- accounts = append(accounts, a)
- }
- }
-
- cacheid := uuid.NewString()
- h.cache.PutWithTTL(cacheid, accounts, 1*time.Hour)
- r.ParseForm()
-
- var beneficiary any
- searched := false
-
- if r.FormValue("beneficiaryid") != "" {
-
- searched = true
-
- requestbeneficiary := &mobilityaccounts.GetAccountRequest{
- Id: r.FormValue("beneficiaryid"),
- }
-
- respbeneficiary, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), requestbeneficiary)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiary = respbeneficiary.Account.ToStorageType()
-
- subscribe := &groupsmanagement.SubscribeRequest{
- Groupid: resp.Group.ToStorageType().ID,
- Memberid: respbeneficiary.Account.Id,
- }
-
- _, err = h.services.GRPC.GroupsManagement.Subscribe(context.TODO(), subscribe)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- /*******************Code to store more information about mermbers groupscovoiturage**************/
- if r.FormValue("departure") != "" {
- var a any
- json.Unmarshal([]byte(r.FormValue("departure")), &a)
-
- Depart = a
- }
- if r.FormValue("destination") != "" {
- var a any
- json.Unmarshal([]byte(r.FormValue("destination")), &a)
-
- Arrive = a
- }
- r.ParseForm()
- dataMap := map[string]any{
-
- "depart": Depart,
- "arrive": Arrive,
- }
- id := uuid.NewString()
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request_organizatio := &groupsmanagement.AddGroupMemberRequest{
- Group: &groupsmanagement.GroupMember{
- Id: id,
- Memberid: respbeneficiary.Account.Id,
- Groupid: resp.Group.ToStorageType().ID,
- Data: data.GetStructValue(),
- },
- }
-
- _, err = h.services.GRPC.GroupsManagement.AddGroupMember(context.TODO(), request_organizatio)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/journeys/groups_covoiturage/create/%s", resp.Group.ToStorageType().ID), http.StatusFound)
- return
- }
- //////////find all groups to store the adresse passenger///////
- // grp := &groupsmanagement.GetGroupsBatchMemberRequest{
-
- // Groupids: []string{resp.Group.ToStorageType().ID},
- // }
- // s, err := h.services.GRPC.GroupsManagement.GetGroupsBatchMember(context.TODO(), grp)
- // if err != nil {
- // fmt.Println(err)
- // w.WriteHeader(http.StatusInternalServerError)
- // return
- // }
- // groups := map[string]any{}
-
- // if err == nil {
- // for _, g := range s.Groups {
- // groups[g.Memberid] = g.ToStorageType()
- // }
- // }
- //////////find all groups to store the adresse passenger///////
- ///////////try to optimise the code ////////////////////////////
- groups, _ := h.services.GetGroupsMemberMap(resp.Group.ToStorageType().ID)
- //fmt.Println(groups)
- var number string = strconv.Itoa(len(resp.Group.Members))
- /////////////////////
- accountsBeneficaire, err := h.beneficiaries(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- h.Renderer.DisplayGroupCovoiturage(w, r, number, resp.Group.ToStorageType().ID, Depart, Arrive, accounts, cacheid, searched, beneficiary, resp.Group.ToStorageType(), accountsBeneficaire, groups)
-}
-
-func (h *ApplicationHandler) UpdateGroupCovoiturage(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- id := vars["id"]
- groupid := vars["groupid"]
- memberid := vars["memberid"]
-
- if r.Method == "POST" {
-
- //////////get groupid covoiturage//////////
- request := &groupsmanagement.GetGroupRequest{
- Id: groupid,
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- //////////////////////////get group member////////////////////////////////
-
- reequest := &groupsmanagement.GetGroupMemberRequest{
- Id: id,
- }
-
- ressp, err := h.services.GRPC.GroupsManagement.GetGroupMember(context.TODO(), reequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- req := &groupsmanagement.UnsubscribeMemberRequest{
- Id: ressp.Group.Id,
- }
-
- _, errr := h.services.GRPC.GroupsManagement.UnsubscribeMember(context.TODO(), req)
- if errr != nil {
- fmt.Println(errr)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- members := resp.Group.Members
- for i := 0; i < len(members); i++ {
- if members[i] == memberid {
- members = append(members[:i], members[(i+1):]...)
- resp.Group.Members = members
- reequest := &groupsmanagement.UnsubscribeRequest{
- Groupid: resp.Group.ToStorageType().ID,
- Memberid: memberid,
- }
-
- _, err := h.services.GRPC.GroupsManagement.Unsubscribe(context.TODO(), reequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- }
-
- }
- http.Redirect(w, r, fmt.Sprintf("/app/journeys/groups_covoiturage/create/%s", groupid), http.StatusFound)
- /*
- I must add "return" to resolve the err
- http: superfluous response.WriteHeader call from git.coopgo.io/coopgo-apps/parcoursmob/renderer.(*Renderer).Render (renderer.go:50)
- */
- return
- }
- h.Renderer.UpdateGroupCovoiturage(w, r, groupid, memberid)
-}
diff --git a/handlers/application/members.go b/handlers/application/members.go
deleted file mode 100644
index 545925f..0000000
--- a/handlers/application/members.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package application
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "strings"
- "time"
-
- formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/utils/form-validators"
- groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
- mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
- "github.com/google/uuid"
- "google.golang.org/protobuf/types/known/structpb"
-)
-
-type UserForm struct {
- FirstName string `json:"first_name" validate:"required"`
- LastName string `json:"last_name" validate:"required"`
- Email string `json:"email" validate:"required,email"`
- PhoneNumber string `json:"phone_number" `
- Address any `json:"address,omitempty"`
- Gender string `json:"gender"`
-}
-
-func (h *ApplicationHandler) MemberDisplay(w http.ResponseWriter, r *http.Request) {
-
- adm := strings.Split(r.URL.Path, "/")
- adminid := adm[3]
-
- request := &mobilityaccounts.GetAccountRequest{
- Id: adminid,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- //////////////////////////////////add organisations/////////////////////////////////////////////////
-
- var allIds []string
- for _, v := range resp.Account.ToStorageType().Data["groups"].([]any) {
- s := fmt.Sprintf("%v", v)
- if !(strings.Contains(s, "admin")) {
- allIds = append(allIds, s)
- }
- }
- reques := &groupsmanagement.GetGroupsBatchRequest{
- Groupids: allIds,
- }
-
- res, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), reques)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var groupsName []string
-
- for _, group := range res.Groups {
- g := fmt.Sprintf("%v", group.ToStorageType().Data["name"])
- groupsName = append(groupsName, g)
- }
-
- h.Renderer.MemberDisplay(w, r, resp.Account.ToStorageType(), groupsName)
-}
-
-func (h *ApplicationHandler) MemberUpdate(w http.ResponseWriter, r *http.Request) {
- adm := strings.Split(r.URL.Path, "/")
- userID := adm[3]
-
- if r.Method == "POST" {
-
- dataMap, err := parseUserForm(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request := &mobilityaccounts.UpdateDataRequest{
- Account: &mobilityaccounts.Account{
- Id: userID,
- Namespace: "parcoursmob",
- Data: data.GetStructValue(),
- },
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.UpdateData(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/members/%s", resp.Account.Id), http.StatusFound)
-
- return
- }
-
- request := &mobilityaccounts.GetAccountRequest{
- Id: userID,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- h.Renderer.MemberUpdate(w, r, resp.Account.ToStorageType())
-}
-
-func parseUserForm(r *http.Request) (map[string]any, error) {
- if err := r.ParseForm(); err != nil {
- return nil, err
- }
-
- formData := UserForm{
- FirstName: r.PostFormValue("first_name"),
- LastName: r.PostFormValue("last_name"),
- Email: r.PostFormValue("email"),
- PhoneNumber: r.PostFormValue("phone_number"),
- Gender: r.PostFormValue("gender"),
- }
-
- validate := formvalidators.New()
- if err := validate.Struct(formData); err != nil {
- return nil, err
- }
-
- d, err := json.Marshal(formData)
- if err != nil {
- return nil, err
- }
-
- var dataMap map[string]any
- err = json.Unmarshal(d, &dataMap)
- if err != nil {
- return nil, err
- }
-
- return dataMap, nil
-}
-
-func (h *ApplicationHandler) MembersList(w http.ResponseWriter, r *http.Request) {
-
- accounts, err := h.services.GetAccounts()
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- var groupsName []string
-
- for _, v := range accounts {
- adminid := v.ID
- request := &mobilityaccounts.GetAccountRequest{
- Id: adminid,
- }
-
- resp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- //////////////////////////////////add organisations/////////////////////////////////////////////////
-
- var allIds []string
- for _, v := range resp.Account.ToStorageType().Data["groups"].([]any) {
- s := fmt.Sprintf("%v", v)
- if !(strings.Contains(s, "admin")) {
- allIds = append(allIds, s)
- }
-
- }
-
- reques := &groupsmanagement.GetGroupsBatchRequest{
- Groupids: allIds,
- }
-
- res, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), reques)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- g := ""
- for _, group := range res.Groups {
- g += fmt.Sprintf("%v", group.ToStorageType().Data["name"]) + " "
- }
- groupsName = append(groupsName, g)
-
- }
- cacheid := uuid.NewString()
- h.cache.PutWithTTL(cacheid, accounts, 1*time.Hour)
-
- h.Renderer.MembersList(w, r, accounts, cacheid, groupsName)
-}
diff --git a/handlers/application/support.go b/handlers/application/support.go
deleted file mode 100644
index 1721500..0000000
--- a/handlers/application/support.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package application
-
-import (
- "fmt"
- "net/http"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
-)
-
-type Message struct {
- Content string
-}
-
-func (h *ApplicationHandler) SupportSend(w http.ResponseWriter, r *http.Request) {
-
- c := r.Context().Value(identification.ClaimsKey)
- if c == nil {
- fmt.Println("no current user claims")
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- current_user_claims := c.(map[string]any)
-
- comment := r.PostFormValue(("comment"))
-
- if r.Method == "POST" {
- data := map[string]any{
- "key": comment,
- "user": current_user_claims["email"],
- }
-
- if err := h.emailing.Send("support.request", "support@parcoursmob.fr", data); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- }
- http.Redirect(w, r, "/app/", http.StatusFound)
- return
- }
- h.Renderer.SupportSend(w, r, comment, current_user_claims)
-}
diff --git a/handlers/application/vehicles-management.go b/handlers/application/vehicles-management.go
deleted file mode 100644
index aca53c9..0000000
--- a/handlers/application/vehicles-management.go
+++ /dev/null
@@ -1,674 +0,0 @@
-package application
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "sort"
- "time"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
- filestorage "git.coopgo.io/coopgo-apps/parcoursmob/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/coreos/go-oidc"
- "github.com/google/uuid"
- "github.com/gorilla/mux"
- "google.golang.org/protobuf/types/known/structpb"
- "google.golang.org/protobuf/types/known/timestamppb"
-)
-
-func (h *ApplicationHandler) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request) {
- //Get Vehicles
- request := &fleets.GetVehiclesRequest{
- Namespaces: []string{"parcoursmob"},
- }
- resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- }
-
- vehicles := []fleetsstorage.Vehicle{}
- bookings := []fleetsstorage.Booking{}
- vehicles_map := map[string]fleetsstorage.Vehicle{}
-
- for _, vehicle := range resp.Vehicles {
- if filterVehicle(r, vehicle) {
- v := vehicle.ToStorageType()
- vehicleBookings := []fleetsstorage.Booking{}
- for _, b := range v.Bookings {
- if b.Status() != fleetsstorage.StatusOld {
- if deleted, ok := b.Data["Deleted"].(bool); !ok && !deleted {
- bookings = append(bookings, b)
- }
- }
- if b.Unavailableto.After(time.Now()) {
- vehicleBookings = append(vehicleBookings, b)
- }
- }
- v.Bookings = vehicleBookings
- vehicles = append(vehicles, v)
- vehicles_map[v.ID] = v
-
- }
- }
-
- sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
- sort.Sort(sorting.BookingsByStartdate(bookings))
- h.Renderer.VehiclesManagementOverview(w, r, vehicles, vehicles_map, bookings)
-}
-
-func (h *ApplicationHandler) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request) {
- //Get Vehicles
- request := &fleets.GetVehiclesRequest{
- Namespaces: []string{"parcoursmob"},
- }
- resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- }
-
- bookings := []fleetsstorage.Booking{}
- vehicles_map := map[string]fleetsstorage.Vehicle{}
-
- for _, vehicle := range resp.Vehicles {
- if filterVehicle(r, vehicle) {
- v := vehicle.ToStorageType()
- vehicles_map[v.ID] = v
- // bookings = append(bookings, v.Bookings...)
- for _, b := range v.Bookings {
- if v, ok := b.Data["administrator_unavailability"].(bool); !ok || !v {
- bookings = append(bookings, b)
- }
- }
- }
- }
-
- sort.Sort(sorting.BookingsByStartdate(bookings))
-
- cacheid := uuid.NewString()
- h.cache.PutWithTTL(cacheid, bookings, 1*time.Hour)
-
- h.Renderer.VehiclesManagementBookingsList(w, r, vehicles_map, bookings, cacheid)
-}
-
-func (h *ApplicationHandler) VehiclesFleetAdd(w http.ResponseWriter, r *http.Request) {
- if r.Method == "POST" {
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(storage.Group)
-
- if err := r.ParseForm(); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- dataMap := map[string]any{}
- if v := r.FormValue("name"); v != "" {
- dataMap["name"] = v
- }
- if v := r.FormValue("address"); v != "" {
- var address map[string]any
- err := json.Unmarshal([]byte(v), &address)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- dataMap["address"] = address
- }
- if v := r.FormValue("informations"); v != "" {
- dataMap["informations"] = v
- }
- if v := r.FormValue("licence_plate"); v != "" {
- dataMap["licence_plate"] = v
- }
- if v := r.FormValue("automatic"); v != "" {
- fmt.Println(v)
- dataMap["automatic"] = (v == "on")
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- vehicle := &fleets.Vehicle{
- Id: uuid.NewString(),
- Namespace: "parcoursmob",
- Type: r.FormValue("type"),
- Administrators: []string{group.ID},
- Data: data.GetStructValue(),
- }
-
- request := &fleets.AddVehicleRequest{
- Vehicle: vehicle,
- }
-
- _, err = h.services.GRPC.Fleets.AddVehicle(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/fleet/%s", vehicle.Id), http.StatusFound)
- return
- }
-
- vehicles_types := h.config.GetStringSlice("modules.fleets.vehicle_types")
- h.Renderer.VehiclesFleetAdd(w, r, vehicles_types)
-}
-
-func (h *ApplicationHandler) VehiclesFleetDisplay(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- vehicleid := vars["vehicleid"]
-
- request := &fleets.GetVehicleRequest{
- Vehicleid: vehicleid,
- }
-
- resp, err := h.services.GRPC.Fleets.GetVehicle(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- h.Renderer.VehiclesFleetDisplay(w, r, resp.Vehicle.ToStorageType())
-}
-
-func filterVehicle(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) VehicleManagementBookingDisplay(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- bookingid := vars["bookingid"]
-
- booking, err := h.services.GetBooking(bookingid)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if r.Method == "POST" {
- r.ParseForm()
-
- newbooking, _ := fleets.BookingFromStorageType(&booking)
-
- startdate := r.FormValue("startdate")
- 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)
- }
- }
-
- enddate := r.FormValue("enddate")
- 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))
- }
- }
-
- unavailablefrom := r.FormValue("unavailablefrom")
- if unavailablefrom != "" {
- newunavailablefrom, _ := time.Parse("2006-01-02", unavailablefrom)
- newbooking.Unavailablefrom = timestamppb.New(newunavailablefrom)
- }
-
- unavailableto := r.FormValue("unavailableto")
- 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(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- booking = newbooking.ToStorageType()
- }
-
- beneficiary := mobilityaccountsstorage.Account{}
-
- if booking.Driver != "" {
- beneficiaryrequest := &mobilityaccounts.GetAccountRequest{
- Id: booking.Driver,
- }
-
- beneficiaryresp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), beneficiaryrequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiary = beneficiaryresp.Account.ToStorageType()
- }
-
- grouprequest := &groupsmanagement.GetGroupRequest{
- Id: booking.Vehicle.Administrators[0],
- }
-
- groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), grouprequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- 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(context.TODO(), alternativerequest)
- if err != nil {
- fmt.Println(err)
- }
-
- alternatives := []any{}
-
- for _, a := range alternativeresp.Vehicles {
- alternatives = append(alternatives, a.ToStorageType())
- }
-
- documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingid)
- file_types_map := h.config.GetStringMapString("storage.files.file_types")
-
- h.Renderer.VehicleManagementBookingDisplay(w, r, booking, booking.Vehicle, beneficiary, groupresp.Group.ToStorageType(), documents, file_types_map, alternatives)
-}
-
-func (h ApplicationHandler) VehicleManagementBookingChangeVehicle(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- bookingid := vars["bookingid"]
-
- r.ParseForm()
-
- newvehicle := r.FormValue("vehicle")
-
- booking, err := h.services.GetBooking(bookingid)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- booking.Vehicleid = newvehicle
-
- b, _ := fleets.BookingFromStorageType(&booking)
-
- request := &fleets.UpdateBookingRequest{
- Booking: b,
- }
-
- _, err = h.services.GRPC.Fleets.UpdateBooking(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/bookings/%s", bookingid), http.StatusFound)
-
-}
-
-func (h ApplicationHandler) VehiclesFleetMakeUnavailable(w http.ResponseWriter, r *http.Request) { // Get Group
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- fmt.Println("no current group")
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- current_group := g.(storage.Group)
-
- // Get current user ID
- u := r.Context().Value(identification.IdtokenKey)
- if u == nil {
- fmt.Println("no current user")
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- current_user_token := u.(*oidc.IDToken)
-
- // Get current user claims
- c := r.Context().Value(identification.ClaimsKey)
- if c == nil {
- fmt.Println("no current user claims")
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- current_user_claims := c.(map[string]any)
-
- vars := mux.Vars(r)
- vehicleid := vars["vehicleid"]
-
- r.ParseForm()
-
- start := r.FormValue("unavailablefrom")
- end := r.FormValue("unavailableto")
- comment := r.FormValue("comment")
-
- unavailablefrom, _ := time.Parse("2006-01-02", start)
- unavailableto, _ := time.Parse("2006-01-02", end)
-
- data := map[string]any{
- "comment": comment,
- "administrator_unavailability": true,
- "booked_by": map[string]any{
- "user": map[string]any{
- "id": current_user_token.Subject,
- "display_name": current_user_claims["display_name"],
- },
- "group": map[string]any{
- "id": current_group.ID,
- "name": current_group.Data["name"],
- },
- },
- }
-
- datapb, err := structpb.NewStruct(data)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- booking := &fleets.Booking{
- Id: uuid.NewString(),
- Vehicleid: vehicleid,
- Unavailablefrom: timestamppb.New(unavailablefrom),
- Unavailableto: timestamppb.New(unavailableto),
- Data: datapb,
- }
-
- request := &fleets.CreateBookingRequest{
- Booking: booking,
- }
-
- _, err = h.services.GRPC.Fleets.CreateBooking(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/fleet/%s", vehicleid), http.StatusFound)
-}
-
-// func (h *ApplicationHandler) UnbookingVehicles(w http.ResponseWriter, r *http.Request) {
-// request := &fleets.GetVehiclesRequest{
-// Namespaces: []string{"parcoursmob"},
-// }
-// resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
-// if err != nil {
-// fmt.Println(err)
-// w.WriteHeader(http.StatusInternalServerError)
-// }
-// vehicles := []fleetsstorage.Vehicle{}
-// fmt.Println(resp.Vehicles[0].Bookings)
-// for i, vehicle := range resp.Vehicles {
-// if len(resp.Vehicles[i].Bookings) == 0 {
-// v := vehicle.ToStorageType()
-// vehicles = append(vehicles, v)
-// }
-// }
-// // if len(resp.Vehicle.ToStorageType().Bookings) == 0 {
-// // h.Renderer.UnbookingVehicles(w, r, resp.Vehicle.ToStorageType())
-// // }
-// // fmt.Println(resp.Vehicle.ToStorageType().Bookings)
-// fmt.Println(vehicles)
-// h.Renderer.UnbookingVehicles(w, r, vehicles)
-// }
-
-func (h *ApplicationHandler) DeleteBooking(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- bookingid := vars["bookingid"]
-
- request := &fleets.DeleteBookingRequest{
- Id: bookingid,
- }
-
- _, err := h.services.GRPC.Fleets.DeleteBooking(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, "/app/vehicles-management/bookings/", http.StatusSeeOther)
-}
-
-func (h *ApplicationHandler) UnbookingVehicle(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- bookingid := vars["bookingid"]
-
- request := &fleets.GetBookingRequest{
- Bookingid: bookingid,
- }
-
- resp, err := h.services.GRPC.Fleets.GetBooking(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- // now := time.Now()
- // date := now.Format("2006-01-02")
-
- date := "1970-01-01"
- unavailableto, _ := time.Parse("2006-01-02", date)
- unavailablefrom := unavailableto
-
- current_group, err := h.currentGroup(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- current_user_token, current_user_claims, err := h.currentUser(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- booked_by_id := resp.Booking.Data.Fields["booked_by"].GetStructValue().Fields["user"].GetStructValue().Fields["id"].GetStringValue()
- booked_by_name := resp.Booking.Data.Fields["booked_by"].GetStructValue().Fields["user"].GetStructValue().Fields["display_name"].GetStringValue()
- booked_by_email := resp.Booking.Data.Fields["booked_by"].GetStructValue().Fields["user"].GetStructValue().Fields["email"].GetStringValue()
- booked_by_group_id := resp.Booking.Data.Fields["booked_by"].GetStructValue().Fields["group"].GetStructValue().Fields["id"].GetStringValue()
- booked_by_group_name := resp.Booking.Data.Fields["booked_by"].GetStructValue().Fields["group"].GetStructValue().Fields["name"].GetStringValue()
-
- data := map[string]any{
- "booked_by": map[string]any{
- "user": map[string]any{
- "id": booked_by_id,
- "display_name": booked_by_name,
- "email": booked_by_email,
- },
- "group": map[string]any{
- "id": booked_by_group_id,
- "name": booked_by_group_name,
- },
- },
- "unbooked_by": map[string]any{
- "user": map[string]any{
- "id": current_user_token.Subject,
- "display_name": current_user_claims["first_name"].(string) + " " + current_user_claims["last_name"].(string),
- "email": current_user_claims["email"],
- },
- "group": map[string]any{
- "id": current_group.ID,
- "name": current_group.Data["name"],
- },
- },
- "Deleted": true,
- "motif": r.FormValue("motif"),
- }
-
- datapb, err := structpb.NewStruct(data)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if r.Method == "POST" {
-
- request := &fleets.UpdateBookingRequest{
- Booking: &fleets.Booking{
- Id: resp.Booking.Id,
- Vehicleid: resp.Booking.Vehicleid,
- Driver: resp.Booking.Driver,
- Startdate: resp.Booking.Startdate,
- Enddate: resp.Booking.Enddate,
- Unavailablefrom: timestamppb.New(unavailablefrom),
- Unavailableto: timestamppb.New(unavailableto),
- Data: datapb,
- },
- }
-
- _, err := h.services.GRPC.Fleets.UpdateBooking(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, "/app/vehicles-management/", http.StatusFound)
- return
- }
- h.Renderer.UnbookingVehicle(w, r, resp.Booking.ToStorageType())
-}
-
-////////////////////////UpdateVehicle///////////////////////
-
-func (h *ApplicationHandler) VehiclesFleetUpdate(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- vehicleID := vars["vehicleid"]
- request := &fleets.GetVehicleRequest{
- Vehicleid: vehicleID,
- }
-
- resp, err := h.services.GRPC.Fleets.GetVehicle(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- namespaceV := resp.Vehicle.Namespace
- //typeV := resp.Vehicle.Type
- administratorsV := resp.Vehicle.Administrators
-
- if r.Method == "POST" {
- fmt.Print(r.FormValue("vehicle_type"))
- if err := r.ParseForm(); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- dataMap := map[string]any{}
- if v := r.FormValue("name"); v != "" {
- dataMap["name"] = v
- }
- if v := r.FormValue("address"); v != "" {
- var address map[string]any
- err := json.Unmarshal([]byte(v), &address)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- dataMap["address"] = address
- }
- if v := r.FormValue("informations"); v != "" {
- dataMap["informations"] = v
- }
- if v := r.FormValue("licence_plate"); v != "" {
- dataMap["licence_plate"] = v
- }
- if v := r.FormValue("automatic"); v != "" {
- fmt.Println(v)
- dataMap["automatic"] = (v == "on")
- }
-
- data, err := structpb.NewValue(dataMap)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request := &fleets.UpdateVehicleRequest{
- Vehicle: &fleets.Vehicle{
- Id: vehicleID,
- Namespace: namespaceV,
- Type: r.FormValue("type"),
- Administrators: administratorsV,
- Data: data.GetStructValue(),
- },
- }
-
- resp, err := h.services.GRPC.Fleets.UpdateVehicle(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/fleet/%s", resp.Vehicle.Id), http.StatusFound)
- return
- }
- vehicles_types := h.config.GetStringSlice("modules.fleets.vehicle_types")
- h.Renderer.VehiclesFleetUpdate(w, r, resp.Vehicle.ToStorageType(), vehicles_types)
-}
diff --git a/handlers/application/vehicles.go b/handlers/application/vehicles.go
deleted file mode 100644
index eec2147..0000000
--- a/handlers/application/vehicles.go
+++ /dev/null
@@ -1,378 +0,0 @@
-package application
-
-import (
- "context"
- "fmt"
- "io"
- "net/http"
- "sort"
- "strings"
- "time"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
- filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
- fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
- "git.coopgo.io/coopgo-platform/fleets/storage"
- groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
- groupsmanagementstorage "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/gorilla/mux"
- "google.golang.org/protobuf/types/known/structpb"
- "google.golang.org/protobuf/types/known/timestamppb"
-)
-
-func (h ApplicationHandler) VehiclesSearch(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
-
- var beneficiary mobilityaccountsstorage.Account
-
- beneficiarydocuments := []filestorage.FileInfo{}
-
- vehicles := []any{}
- searched := false
- start := r.FormValue("startdate")
- end := r.FormValue("enddate")
-
- startdate, _ := time.Parse("2006-01-02", start)
- enddate, _ := time.Parse("2006-01-02", end)
- automatic := (r.FormValue("automatic") == "on")
-
- administrators := []string{}
-
- if r.FormValue("beneficiaryid") != "" {
- // Handler form
- searched = true
-
- requestbeneficiary := &mobilityaccounts.GetAccountRequest{
- Id: r.FormValue("beneficiaryid"),
- }
-
- respbeneficiary, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), requestbeneficiary)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiary = respbeneficiary.Account.ToStorageType()
-
- request := &fleets.GetVehiclesRequest{
- Namespaces: []string{"parcoursmob"},
- AvailabilityFrom: timestamppb.New(startdate),
- AvailabilityTo: timestamppb.New(enddate),
- }
-
- if r.FormValue("type") != "" {
- request.Types = []string{r.FormValue("type")}
- }
-
- resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- }
-
- for _, vehicle := range resp.Vehicles {
- v := vehicle.ToStorageType()
-
- if r.FormValue("type") == "Voiture" && automatic {
- fmt.Println(v.Data["automatic"])
- if auto, ok := v.Data["automatic"].(bool); !ok || !auto {
- fmt.Println(v.Data["automatic"])
- continue
- }
- }
-
- adminfound := false
- for _, a := range administrators {
- if a == v.Administrators[0] {
- adminfound = true
- break
- }
- }
- if !adminfound {
- administrators = append(administrators, v.Administrators[0])
- }
-
- vehicles = append(vehicles, v)
- }
-
- beneficiarydocuments = h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiary.ID)
- }
-
- accounts, err := h.beneficiaries(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- groups := map[string]any{}
-
- if len(administrators) > 0 {
- admingroups, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
- Groupids: administrators,
- })
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- for _, g := range admingroups.Groups {
- groups[g.Id] = g.ToStorageType()
- }
-
- }
- sort.Sort(sorting.BeneficiariesByName(accounts))
-
- mandatory_documents := h.config.GetStringSlice("modules.fleets.booking_documents.mandatory")
- file_types_map := h.config.GetStringMapString("storage.files.file_types")
- vehicles_types := h.config.GetStringSlice("modules.fleets.vehicle_types")
-
- h.Renderer.VehiclesSearch(w, r, accounts, searched, vehicles, beneficiary, r.FormValue("startdate"), r.FormValue("enddate"), mandatory_documents, file_types_map, beneficiarydocuments, r.FormValue("type"), automatic, vehicles_types, groups)
-}
-
-func (h ApplicationHandler) Book(w http.ResponseWriter, r *http.Request) {
- fmt.Println("Book")
- current_group, err := h.currentGroup(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- current_user_token, current_user_claims, err := h.currentUser(r)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- vars := mux.Vars(r)
- vehicleid := vars["vehicleid"]
- beneficiaryid := vars["beneficiaryid"]
-
- vehicle, err := h.services.GRPC.Fleets.GetVehicle(context.TODO(), &fleets.GetVehicleRequest{
- Vehicleid: vehicleid,
- })
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte("Vehicle not found"))
- w.Write([]byte(err.Error()))
- return
- }
-
- r.ParseMultipartForm(100 * 1024 * 1024)
-
- start := r.FormValue("startdate")
- end := r.FormValue("enddate")
-
- startdate, _ := time.Parse("2006-01-02", start)
- enddate, _ := time.Parse("2006-01-02", end)
-
- data := map[string]any{
- "booked_by": map[string]any{
- "user": map[string]any{
- "id": current_user_token.Subject,
- "display_name": fmt.Sprintf("%s %s", current_user_claims["first_name"], current_user_claims["last_name"]),
- },
- "group": map[string]any{
- "id": current_group.ID,
- "name": current_group.Data["name"],
- },
- },
- }
- datapb, err := structpb.NewStruct(data)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- booking := &fleets.Booking{
- Id: uuid.NewString(),
- Vehicleid: vehicleid,
- Driver: beneficiaryid,
- Startdate: timestamppb.New(startdate),
- Enddate: timestamppb.New(enddate),
- Unavailablefrom: timestamppb.New(startdate),
- Unavailableto: timestamppb.New(enddate.Add(72 * time.Hour)),
- Data: datapb,
- }
-
- request := &fleets.CreateBookingRequest{
- Booking: booking,
- }
-
- for _, v := range h.config.GetStringSlice("modules.fleets.booking_documents.mandatory") {
- existing_file := r.FormValue("type-" + v)
- if existing_file == "" {
- file, header, err := r.FormFile("doc-" + v)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Document manquant : " + v))
- return
- }
- defer file.Close()
-
- fileid := uuid.NewString()
-
- metadata := map[string]string{
- "type": v,
- "name": header.Filename,
- }
-
- if err := h.filestorage.Put(file, filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s_%s", booking.Id, fileid, header.Filename), header.Size, metadata); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- } else {
- path := strings.Split(existing_file, "/")
-
- if err := h.filestorage.Copy(existing_file, fmt.Sprintf("%s/%s/%s", filestorage.PREFIX_BOOKINGS, booking.Id, path[len(path)-1])); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- }
- }
-
- _, err = h.services.GRPC.Fleets.CreateBooking(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- //NOTIFY GROUP MEMBERS
- members, _, err := h.groupmembers(vehicle.Vehicle.Administrators[0])
- if err != nil {
- fmt.Println(err)
- } else {
- for _, m := range members {
- if email, ok := m.Data["email"].(string); ok {
- h.emailing.Send("fleets.bookings.creation_admin_alert", email, map[string]string{
- "bookingid": booking.Id,
- })
- }
- }
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/vehicles/bookings/%s", booking.Id), http.StatusFound)
-
-}
-
-func (h ApplicationHandler) VehicleBookingDisplay(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- bookingid := vars["bookingid"]
-
- request := &fleets.GetBookingRequest{
- Bookingid: bookingid,
- }
- resp, err := h.services.GRPC.Fleets.GetBooking(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- booking := resp.Booking.ToStorageType()
-
- beneficiaryrequest := &mobilityaccounts.GetAccountRequest{
- Id: booking.Driver,
- }
-
- beneficiaryresp, err := h.services.GRPC.MobilityAccounts.GetAccount(context.TODO(), beneficiaryrequest)
- if err != nil {
- beneficiaryresp = &mobilityaccounts.GetAccountResponse{
- Account: &mobilityaccounts.Account{},
- }
- }
-
- grouprequest := &groupsmanagement.GetGroupRequest{
- Id: booking.Vehicle.Administrators[0],
- }
-
- groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), grouprequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingid)
- file_types_map := h.config.GetStringMapString("storage.files.file_types")
-
- h.Renderer.VehicleBookingDisplay(w, r, booking, booking.Vehicle, beneficiaryresp.Account.ToStorageType(), groupresp.Group.ToStorageType(), documents, file_types_map)
-}
-
-func (h ApplicationHandler) VehiclesBookingsList(w http.ResponseWriter, r *http.Request) {
-
- g := r.Context().Value(identification.GroupKey)
- if g == nil {
- w.WriteHeader(http.StatusBadRequest)
- return
- }
-
- group := g.(groupsmanagementstorage.Group)
-
- request := &fleets.GetBookingsRequest{}
- resp, err := h.services.GRPC.Fleets.GetBookings(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusNotFound)
- return
- }
-
- bookings := []storage.Booking{}
-
- for _, b := range resp.Bookings {
- booking := b.ToStorageType()
- if b1, ok := booking.Data["booked_by"].(map[string]any); ok {
- if b2, ok := b1["group"].(map[string]any); ok {
- if b2["id"] == group.ID {
- bookings = append(bookings, booking)
- }
- }
- }
-
- }
-
- sort.Sort(sorting.BookingsByStartdate(bookings))
-
- vehicles, _ := h.services.GetVehiclesMap()
- groups, _ := h.services.GetGroupsMap()
-
- h.Renderer.VehicleBookingsList(w, r, bookings, vehicles, groups)
-}
-
-func (h *ApplicationHandler) BookingDocumentDownload(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- bookingid := vars["bookingid"]
- document := vars["document"]
-
- fmt.Println(fmt.Sprintf("%s/%s", bookingid, document))
-
- file, info, err := h.filestorage.Get(filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s", bookingid, document))
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Content-Type", info.ContentType)
- if _, err = io.Copy(w, file); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, fmt.Sprintf("/app/vehicles/bookings/%s", bookingid), http.StatusFound)
-
-}
diff --git a/handlers/auth/auth.go b/handlers/auth/auth.go
deleted file mode 100644
index 43d42bd..0000000
--- a/handlers/auth/auth.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package auth
-
-import (
- "git.coopgo.io/coopgo-apps/parcoursmob/renderer"
- "git.coopgo.io/coopgo-apps/parcoursmob/services"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
- "git.coopgo.io/coopgo-platform/emailing"
- "github.com/spf13/viper"
-)
-
-type AuthHandler struct {
- idp *identification.IdentificationProvider
- config *viper.Viper
- services *services.ServicesHandler
- Renderer *renderer.Renderer
- cache cache.CacheHandler
- emailing *emailing.Mailer
-}
-
-func NewAuthHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache cache.CacheHandler, emailing *emailing.Mailer) (*AuthHandler, error) {
- templates_root := cfg.GetString("templates.root")
- renderer := renderer.NewRenderer(cfg, templates_root)
- return &AuthHandler{
- idp: idp,
- config: cfg,
- services: svc,
- Renderer: renderer,
- cache: cache,
- emailing: emailing,
- }, nil
-}
diff --git a/handlers/auth/groups.go b/handlers/auth/groups.go
deleted file mode 100644
index 9a7578e..0000000
--- a/handlers/auth/groups.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package auth
-
-import (
- "context"
- "fmt"
- "net/http"
-
- groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
-)
-
-func (h *AuthHandler) Groups(w http.ResponseWriter, r *http.Request) {
- session, _ := h.idp.SessionsStore.Get(r, "parcoursmob_session")
-
- if r.Method == "POST" {
- r.ParseForm()
-
- groupid := r.FormValue("group")
-
- session.Values["organization"] = groupid
- session.Save(r, w)
-
- http.Redirect(w, r, "/app/", http.StatusFound)
-
- return
- }
-
- tokenstring, ok := session.Values["idtoken"]
-
- if !ok {
- http.Redirect(w, r, "/app/", http.StatusFound)
- return
- }
-
- idtoken, err := h.idp.TokenVerifier.Verify(context.Background(), tokenstring.(string))
- if err != nil {
- delete(session.Values, "idtoken")
- http.Redirect(w, r, "/app/", http.StatusFound)
- return
- }
-
- var claims map[string]any
-
- err = idtoken.Claims(&claims)
- if err != nil {
- fmt.Println(err)
- }
-
- g := claims["groups"]
-
- groups_interface, ok := g.([]any)
- if !ok {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- groups := []string{}
-
- for _, v := range groups_interface {
- groups = append(groups, v.(string))
- }
-
- request := &groupsmanagement.GetGroupsBatchRequest{
- Groupids: groups,
- }
-
- resp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), request)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- var groupsresponse = []any{}
-
- for _, group := range resp.Groups {
- if group.Namespace != "parcoursmob_organizations" {
- continue
- }
- g := group.ToStorageType()
- groupsresponse = append(groupsresponse, g)
- }
-
- h.Renderer.AuthGroups(w, r, groupsresponse)
-}
-
-func (h *AuthHandler) GroupSwitch(w http.ResponseWriter, r *http.Request) {
- session, _ := h.idp.SessionsStore.Get(r, "parcoursmob_session")
- delete(session.Values, "organization")
- session.Save(r, w)
- http.Redirect(w, r, "/app/", http.StatusFound)
-}
diff --git a/handlers/auth/lost_password.go b/handlers/auth/lost_password.go
deleted file mode 100644
index 9647513..0000000
--- a/handlers/auth/lost_password.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package auth
-
-import (
- "context"
- "crypto/rand"
- "encoding/base64"
- "fmt"
- "io"
- "net/http"
- "time"
-
- "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
-)
-
-func (h *AuthHandler) LostPasswordInit(w http.ResponseWriter, r *http.Request) {
- if r.Method == "POST" {
- r.ParseForm()
- email := r.FormValue("email")
- if email != "" {
- account, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &grpcapi.GetAccountUsernameRequest{
- Username: email,
- Namespace: "parcoursmob",
- })
- if err != nil {
- fmt.Println(err)
- http.Redirect(w, r, "/app/", http.StatusFound)
- return
- }
-
- b := make([]byte, 16)
- if _, err := io.ReadFull(rand.Reader, b); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- key := base64.RawURLEncoding.EncodeToString(b)
-
- passwordretrieval := map[string]any{
- "username": email,
- "account_id": account.Account.Id,
- "key": key,
- }
-
- h.cache.PutWithTTL("retrieve-password/"+key, passwordretrieval, 72*time.Hour)
-
- if err := h.emailing.Send("auth.retrieve_password", email, passwordretrieval); err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- http.Redirect(w, r, "/app/", http.StatusFound)
- }
- }
- h.Renderer.LostPasswordInit(w, r)
-
-}
-
-func (h *AuthHandler) LostPasswordRecover(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
-
- key := r.FormValue("key")
- recover, err := h.cache.Get("retrieve-password/" + key)
- if err != nil {
- fmt.Println(err)
- h.Renderer.LostPasswordRecoverKO(w, r, key)
- return
- }
-
- if r.Method == "POST" {
- newpassword := r.FormValue("password")
- if newpassword == "" {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte("Password is empty"))
- return
- }
-
- _, err := h.services.GRPC.MobilityAccounts.ChangePassword(context.TODO(), &grpcapi.ChangePasswordRequest{
- Id: recover.(map[string]any)["account_id"].(string),
- Password: newpassword,
- })
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- }
-
- err = h.cache.Delete("retrieve-password/" + key)
- if err != nil {
- fmt.Println(err)
- }
-
- http.Redirect(w, r, "/app/", http.StatusFound)
-
- }
- h.Renderer.LostPasswordRecover(w, r, recover)
-}
diff --git a/handlers/auth/onboarding.go b/handlers/auth/onboarding.go
deleted file mode 100644
index 20a3982..0000000
--- a/handlers/auth/onboarding.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package auth
-
-import (
- "context"
- "fmt"
- "net/http"
-
- mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
- ma "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
-)
-
-func (h *AuthHandler) Onboarding(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
-
- key := r.FormValue("key")
- onboarding, err := h.cache.Get("onboarding/" + key)
- if err != nil {
- fmt.Println(err)
- h.Renderer.AuthOnboardingKO(w, r, key)
- return
- }
-
- onboardingmap := onboarding.(map[string]any)
-
- if r.Method == "POST" {
- if r.FormValue("password") == "" {
- fmt.Println("password is empty !")
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- groups := []string{
- onboardingmap["group"].(string),
- //onboardingmap["group"].(string) + ":admin",
- }
-
- if onboardingmap["admin"].(bool) {
- groups = append(groups, onboardingmap["group"].(string)+":admin")
- }
- display_name := fmt.Sprint(r.FormValue("first_name")) + " " + fmt.Sprint(r.FormValue("last_name"))
- account := &ma.Account{
- Authentication: ma.AccountAuth{
- Local: ma.LocalAuth{
- Username: onboardingmap["username"].(string),
- Password: r.FormValue("password"),
- },
- },
- Namespace: "parcoursmob",
-
- Data: map[string]any{
- "display_name": display_name,
- "first_name": r.FormValue("first_name"),
- "last_name": r.FormValue("last_name"),
- "email": onboardingmap["username"],
- "groups": groups,
- },
- }
-
- acc, err := mobilityaccounts.AccountFromStorageType(account)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- request := &mobilityaccounts.RegisterRequest{
- Account: acc,
- }
-
- _, err = h.services.GRPC.MobilityAccounts.Register(context.TODO(), request)
-
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- err = h.cache.Delete("onboarding/" + key)
- if err != nil {
- fmt.Println(err)
- }
-
- http.Redirect(w, r, "/app/", http.StatusFound)
- }
-
- h.Renderer.AuthOnboarding(w, r, key, onboarding)
-}
diff --git a/handlers/exports/agenda.go b/handlers/exports/agenda.go
deleted file mode 100644
index b21a13d..0000000
--- a/handlers/exports/agenda.go
+++ /dev/null
@@ -1,204 +0,0 @@
-package exports
-
-import (
- "context"
- "fmt"
- "net/http"
- "sort"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
- agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
- agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
- groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
- groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
- accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
- accountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
- "github.com/gorilla/mux"
- "github.com/xuri/excelize/v2"
-)
-
-func (h *ExportsHandler) Agenda(filter string) func(w http.ResponseWriter, r *http.Request) {
- switch filter {
- case "allEvents":
- return func(w http.ResponseWriter, r *http.Request) {
- resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
- Namespaces: []string{"parcoursmob_dispositifs"},
- })
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- events := []agendastorage.Event{}
-
- groupids := []string{}
- beneficiaries_ids := []string{}
- for _, e := range resp.Events {
- groupids = append(groupids, e.Owners...)
- events = append(events, e.ToStorageType())
-
- for _, subscriptions := range e.Subscriptions {
- beneficiaries_ids = append(beneficiaries_ids, subscriptions.Subscriber)
- }
- }
-
- sort.Sort(sorting.EventsByStartdate(events))
-
- groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
- Groupids: groupids,
- })
- groups := map[string]groupsstorage.Group{}
-
- if err == nil {
- for _, g := range groupsresp.Groups {
- groups[g.Id] = g.ToStorageType()
- }
- }
-
- beneficiaries, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), &accounts.GetAccountsBatchRequest{
- Accountids: beneficiaries_ids,
- })
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiaries_map := map[string]accountsstorage.Account{}
- for _, ben := range beneficiaries.Accounts {
- beneficiaries_map[ben.Id] = ben.ToStorageType()
- }
-
- f := h.generateExcel(events, groups, beneficiaries_map)
-
- h.writeFileResponse(f, w)
- }
-
- case "oneEvent":
- return func(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- eventId := vars["eventid"]
- resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), &agenda.GetEventRequest{
- Id: eventId,
- })
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- groupids := []string{}
- beneficiaries_ids := []string{}
- groupids = append(groupids, resp.Event.Owners...)
- for _, subscriptions := range resp.Event.Subscriptions {
- beneficiaries_ids = append(beneficiaries_ids, subscriptions.Subscriber)
- }
- groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
- Groupids: groupids,
- })
- groups := map[string]groupsstorage.Group{}
-
- if err == nil {
- for _, g := range groupsresp.Groups {
- groups[g.Id] = g.ToStorageType()
- }
- }
-
- beneficiaries, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), &accounts.GetAccountsBatchRequest{
- Accountids: beneficiaries_ids,
- })
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiaries_map := map[string]accountsstorage.Account{}
- for _, ben := range beneficiaries.Accounts {
- beneficiaries_map[ben.Id] = ben.ToStorageType()
- }
-
- f := h.generateExcel([]agendastorage.Event{resp.Event.ToStorageType()}, groups, beneficiaries_map)
- h.writeFileResponse(f, w)
-
- }
- }
- return nil
-}
-
-func (h *ExportsHandler) generateExcel(events []agendastorage.Event, groups map[string]groupsstorage.Group,
- beneficiaries_map map[string]accountsstorage.Account) *excelize.File {
- f := excelize.NewFile()
- defer func() {
- if err := f.Close(); err != nil {
- fmt.Println(err)
- }
- }()
- f.SetCellValue("Sheet1", "A1", "Evénement")
- f.SetCellValue("Sheet1", "B1", "Date de début")
- f.SetCellValue("Sheet1", "C1", "Date de fin")
- f.SetCellValue("Sheet1", "D1", "Nom bénéficiaire")
- f.SetCellValue("Sheet1", "E1", "Prenom bénéficiaire")
- f.SetCellValue("Sheet1", "F1", "Numéro allocataire / Pole emploi")
- f.SetCellValue("Sheet1", "G1", "Prescipteur")
- f.SetCellValue("Sheet1", "H1", "Prescipteur Nom")
- f.SetCellValue("Sheet1", "I1", "Prescipteur Email")
- f.SetCellValue("Sheet1", "J1", "Gestionnaire événement")
- // f.SetCellValue("Sheet1", "I1", "Prescripteur téléphone")
-
- i := 2
- for _, e := range events {
- if len(e.Owners) == 0 {
- continue
- }
- admin := groups[e.Owners[0]]
-
- for _, s := range e.Subscriptions {
- subscribedbygroup := ""
- subscribedbyuser := ""
- subscribedbyemail := ""
- if v, ok := s.Data["subscribed_by"].(map[string]any); ok {
- if v2, ok := v["group"].(map[string]any); ok {
- if v3, ok := v2["name"].(string); ok {
- subscribedbygroup = v3
- }
-
- }
- if v4, ok := v["user"].(map[string]any); ok {
- if v5, ok := v4["display_name"].(string); ok {
- subscribedbyuser = v5
- }
- if v6, ok := v4["email"].(string); ok {
- subscribedbyemail = v6
- }
-
- }
- }
-
- beneficiary := beneficiaries_map[s.Subscriber]
-
- f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i), e.Name)
- f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i), e.Startdate.Format("2006-01-02"))
- f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i), e.Enddate.Format("2006-01-02"))
- f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i), beneficiary.Data["last_name"])
- f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i), beneficiary.Data["first_name"])
- f.SetCellValue("Sheet1", fmt.Sprintf("F%d", i), beneficiary.Data["file_number"])
- f.SetCellValue("Sheet1", fmt.Sprintf("G%d", i), subscribedbygroup)
- f.SetCellValue("Sheet1", fmt.Sprintf("H%d", i), subscribedbyuser)
- f.SetCellValue("Sheet1", fmt.Sprintf("I%d", i), subscribedbyemail)
- f.SetCellValue("Sheet1", fmt.Sprintf("J%d", i), admin.Data["name"])
- i = i + 1
- }
-
- }
- return f
-
-}
-
-func (h *ExportsHandler) writeFileResponse(file *excelize.File, w http.ResponseWriter) {
- w.Header().Set("Content-Type", "application/octet-stream")
- w.Header().Set("Content-Disposition", "attachment; filename="+"Workbook.xlsx")
- w.Header().Set("Content-Transfer-Encoding", "binary")
- w.Header().Set("Expires", "0")
- file.Write(w)
-}
diff --git a/handlers/exports/exports.go b/handlers/exports/exports.go
deleted file mode 100644
index c3eec40..0000000
--- a/handlers/exports/exports.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package exports
-
-import (
- "git.coopgo.io/coopgo-apps/parcoursmob/services"
- "git.coopgo.io/coopgo-platform/emailing"
- "github.com/spf13/viper"
-)
-
-type ExportsHandler struct {
- config *viper.Viper
- services *services.ServicesHandler
- emailing *emailing.Mailer
-}
-
-func NewExportsHandler(cfg *viper.Viper, svc *services.ServicesHandler, emailing *emailing.Mailer) (*ExportsHandler, error) {
- return &ExportsHandler{
- config: cfg,
- services: svc,
- emailing: emailing,
- }, nil
-}
diff --git a/handlers/exports/fleets.go b/handlers/exports/fleets.go
deleted file mode 100644
index 8c2361a..0000000
--- a/handlers/exports/fleets.go
+++ /dev/null
@@ -1,174 +0,0 @@
-package exports
-
-import (
- "context"
- "fmt"
- "net/http"
-
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- 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"
- groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
- accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
- accountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
- "github.com/xuri/excelize/v2"
-)
-
-func (h *ExportsHandler) Bookings(w http.ResponseWriter, r *http.Request) {
-
- vehicles := map[string]fleetsstorage.Vehicle{}
- bookings := []fleetsstorage.Booking{}
- reequest := &fleets.GetVehiclesRequest{
- Namespaces: []string{"parcoursmob"},
- }
- reesp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), reequest)
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiaries_ids := []string{}
-
- for _, vehicle := range reesp.Vehicles {
-
- v := vehicle.ToStorageType()
- fmt.Println(v)
-
- for _, b := range v.Bookings {
- bookings = append(bookings, b)
- beneficiaries_ids = append(beneficiaries_ids, b.Driver)
- }
-
- vehicles[vehicle.Id] = v
-
- }
-
- groups := map[string]groupsstorage.Group{}
-
- admingroups, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), &groupsmanagement.GetGroupsRequest{
- Namespaces: []string{"parcoursmob_organizations"},
- })
- if err != nil {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- 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 {
- fmt.Println(err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- beneficiaries_map := map[string]accountsstorage.Account{}
- for _, ben := range beneficiaries.Accounts {
- beneficiaries_map[ben.Id] = ben.ToStorageType()
- }
-
- /////////////// Generate file
-
- f := excelize.NewFile()
- defer func() {
- if err := f.Close(); err != nil {
- fmt.Println(err)
- }
- }()
-
- f.SetCellValue("Sheet1", "A1", "Numéro")
- f.SetCellValue("Sheet1", "B1", "Type")
- f.SetCellValue("Sheet1", "C1", "Gestionnaire")
- f.SetCellValue("Sheet1", "D1", "Prescripteur")
- f.SetCellValue("Sheet1", "E1", "Bénéficiaire")
- f.SetCellValue("Sheet1", "F1", "Numéro allocataire / Pole emploi")
- f.SetCellValue("Sheet1", "G1", "Début de Mise à disposition")
- f.SetCellValue("Sheet1", "H1", "Fin de mise à disposition")
- f.SetCellValue("Sheet1", "I1", "Début indisponibilité")
- f.SetCellValue("Sheet1", "J1", "Fin indisponibilité")
- f.SetCellValue("Sheet1", "K1", "Véhicule retiré")
- f.SetCellValue("Sheet1", "L1", "Commentaire - Retrait véhicule")
- f.SetCellValue("Sheet1", "M1", "Réservation supprimée")
- f.SetCellValue("Sheet1", "N1", "Motif de la suppression")
-
- i := 2
- for _, b := range bookings {
- vehicle := vehicles[b.Vehicleid]
- if len(vehicle.Administrators) == 0 {
- continue
- }
- admin := groups[vehicle.Administrators[0]]
-
- bookedby := ""
- if v, ok := b.Data["booked_by"].(map[string]any); ok {
- if v2, ok := v["user"].(map[string]any); ok {
- if v3, ok := v2["display_name"].(string); ok {
- bookedby = v3
- }
-
- }
- }
-
- bookedbygroup := ""
- if v4, ok := b.Data["booked_by"].(map[string]any); ok {
- if v5, ok := v4["group"].(map[string]any); ok {
- if v6, ok := v5["id"].(string); ok {
- bookedbygroup = v6
- }
- }
- }
-
- // filter by group
- g := r.Context().Value(identification.GroupKey)
- group := g.(groupsstorage.Group)
-
- if bookedbygroup != group.ID {
- continue
- }
-
- beneficiary := beneficiaries_map[b.Driver]
- adminunavailability := false
-
- if av, ok := b.Data["administrator_unavailability"].(bool); ok && av {
- adminunavailability = true
- }
-
- deleted := ""
- v, ok := b.Data["Deleted"]
- fmt.Println(v)
- fmt.Println(ok)
- if b.Deleted || (ok && v.(bool)) {
- deleted = "DELETED"
- }
-
- f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i), vehicle.Data["licence_plate"])
- f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i), vehicle.Type)
- f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i), admin.Data["name"])
- f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i), bookedby)
- f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i), fmt.Sprintf("%v %v", beneficiary.Data["first_name"], beneficiary.Data["last_name"]))
- f.SetCellValue("Sheet1", fmt.Sprintf("F%d", i), beneficiary.Data["file_number"])
- f.SetCellValue("Sheet1", fmt.Sprintf("G%d", i), b.Startdate.Format("2006-01-02"))
- f.SetCellValue("Sheet1", fmt.Sprintf("H%d", i), b.Enddate.Format("2006-01-02"))
- f.SetCellValue("Sheet1", fmt.Sprintf("I%d", i), b.Unavailablefrom.Format("2006-01-02"))
- f.SetCellValue("Sheet1", fmt.Sprintf("J%d", i), b.Unavailableto.Format("2006-01-02"))
- f.SetCellValue("Sheet1", fmt.Sprintf("K%d", i), adminunavailability)
- f.SetCellValue("Sheet1", fmt.Sprintf("L%d", i), b.Data["comment"])
- f.SetCellValue("Sheet1", fmt.Sprintf("M%d", i), deleted)
- f.SetCellValue("Sheet1", fmt.Sprintf("N%d", i), b.Data["motif"])
- i = i + 1
- }
-
- w.Header().Set("Content-Type", "application/octet-stream")
- w.Header().Set("Content-Disposition", "attachment; filename="+"Workbook.xlsx")
- w.Header().Set("Content-Transfer-Encoding", "binary")
- w.Header().Set("Expires", "0")
- f.Write(w)
-
-}
diff --git a/main.go b/main.go
old mode 100644
new mode 100755
index f45ad57..350f072
--- a/main.go
+++ b/main.go
@@ -1,190 +1,94 @@
package main
import (
- "fmt"
- "log"
- "net/http"
- "time"
+ "os"
+ "sync"
- "git.coopgo.io/coopgo-apps/parcoursmob/handlers/api"
- "git.coopgo.io/coopgo-apps/parcoursmob/handlers/application"
- "git.coopgo.io/coopgo-apps/parcoursmob/handlers/auth"
- "git.coopgo.io/coopgo-apps/parcoursmob/handlers/exports"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
+ "git.coopgo.io/coopgo-apps/parcoursmob/servers/mcp"
+ "git.coopgo.io/coopgo-apps/parcoursmob/servers/publicweb"
+ "git.coopgo.io/coopgo-apps/parcoursmob/servers/web"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
- cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
- "github.com/gorilla/mux"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
)
func main() {
cfg, err := ReadConfig()
if err != nil {
- panic(err)
+ log.Fatal().Err(err).Msg("cannot read config!")
+ return
}
var (
- address = cfg.GetString("server.listen")
- service_name = cfg.GetString("service_name")
- templates_public_dir = cfg.GetString("templates.public_dir")
- dev_env = cfg.GetBool("dev_env")
+ dev_env = cfg.GetBool("dev_env")
+ webEnabled = cfg.GetBool("server.web.enabled")
+ mcpEnabled = cfg.GetBool("server.mcp.enabled")
+ publicwebEnabled = cfg.GetBool("server.publicweb.enabled")
)
+ if dev_env {
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+ } else {
+ zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
+ }
+
svc, err := services.NewServicesHandler(cfg)
if err != nil {
- panic(err)
+ log.Panic().Err(err).Msg("Error creating services handler")
}
kv, err := cache.NewKVHandler(cfg)
if err != nil {
- panic(err)
+ log.Panic().Err(err).Msg("Error creating KV handler")
}
-
- filestorage, err := cache.NewFileStorage(cfg)
+ filestorage, _ := cache.NewFileStorage(cfg)
idp, err := identification.NewIdentificationProvider(cfg, svc, kv)
if err != nil {
- panic(err)
+ log.Panic().Err(err).Msg("Error creating identification provider")
}
emailing, err := renderer.NewEmailingHandler(cfg)
if err != nil {
- panic(err)
+ log.Panic().Err(err).Msg("Error creating emailing handler")
}
- apiHandler, _ := api.NewAPIHandler(cfg, idp, svc, kv)
- applicationHandler, _ := application.NewApplicationHandler(cfg, svc, kv, filestorage, emailing)
- exportsHandler, _ := exports.NewExportsHandler(cfg, svc, emailing)
- authHandler, _ := auth.NewAuthHandler(cfg, idp, svc, kv, emailing)
+ // Create renderer for web server
+ templates_root := cfg.GetString("templates.root")
+ webRenderer := renderer.NewRenderer(cfg, templates_root, filestorage)
- fmt.Println("Running", service_name, ":")
- if dev_env {
- fmt.Printf("\033]0;%s\007", service_name)
+ applicationHandler, _ := application.NewApplicationHandler(cfg, svc, kv, filestorage, emailing, idp)
+
+ var wg sync.WaitGroup
+
+ if webEnabled {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ web.Run(cfg, svc, webRenderer, applicationHandler, idp, kv, filestorage)
+ }()
}
- r := mux.NewRouter()
-
- r.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir(templates_public_dir))))
-
- r.HandleFunc("/auth/onboarding", authHandler.Onboarding)
- r.HandleFunc("/auth/disconnect", authHandler.Disconnect)
- r.HandleFunc("/auth/lost-password", authHandler.LostPasswordInit)
- r.HandleFunc("/auth/lost-password/recover", authHandler.LostPasswordRecover)
- r.HandleFunc("/auth/groups/", authHandler.Groups)
- r.HandleFunc("/auth/groups/switch", authHandler.GroupSwitch)
- r.HandleFunc("/", redirectApp)
-
- api_router := r.PathPrefix("/api").Subrouter()
- api_router.HandleFunc("/", apiHandler.NotFound)
- api_router.HandleFunc("/geo/autocomplete", apiHandler.GeoAutocomplete)
- api_router.HandleFunc("/cache/{cacheid}", apiHandler.GetCache)
- api_router.HandleFunc("/cache/{cacheid}/export", apiHandler.CacheExport)
- api_router.HandleFunc("/oauth2/callback", apiHandler.OAuth2Callback)
-
- application := r.PathPrefix("/app").Subrouter()
- application.HandleFunc("/", applicationHandler.Dashboard)
- application.HandleFunc("/beneficiaries/", applicationHandler.BeneficiariesList)
- application.HandleFunc("/beneficiaries/create", applicationHandler.BeneficiaryCreate)
- application.HandleFunc("/beneficiaries/{beneficiaryid}", applicationHandler.BeneficiaryDisplay)
- application.HandleFunc("/beneficiaries/{beneficiaryid}/update", applicationHandler.BeneficiaryUpdate)
- application.HandleFunc("/beneficiaries/{beneficiaryid}/documents", applicationHandler.BeneficiaryDocuments)
- application.HandleFunc("/beneficiaries/{beneficiaryid}/documents/{document}", applicationHandler.BeneficiaryDocumentDownload)
- application.HandleFunc("/beneficiaries/{beneficiaryid}/picture", applicationHandler.BeneficiaryPicture)
- application.HandleFunc("/members/{beneficiaryid}/picture", applicationHandler.BeneficiaryPicture)
- application.HandleFunc("/members/{adminid}", applicationHandler.MemberDisplay)
- application.HandleFunc("/members/{adminid}/update", applicationHandler.MemberUpdate)
- application.HandleFunc("/members/", applicationHandler.MembersList)
- application.HandleFunc("/journeys/", applicationHandler.JourneysSearch)
- application.HandleFunc("/vehicles/", applicationHandler.VehiclesSearch)
- application.HandleFunc("/vehicles/bookings/", applicationHandler.VehiclesBookingsList)
- application.HandleFunc("/vehicles/bookings/{bookingid}", applicationHandler.VehicleBookingDisplay)
- application.HandleFunc("/vehicles/v/{vehicleid}/b/{beneficiaryid}", applicationHandler.Book)
- application.HandleFunc("/vehicles/bookings/{bookingid}/documents/{document}", applicationHandler.BookingDocumentDownload)
- application.HandleFunc("/vehicles-management/", applicationHandler.VehiclesManagementOverview)
- application.HandleFunc("/vehicles-management/fleet/add", applicationHandler.VehiclesFleetAdd)
- application.HandleFunc("/vehicles-management/fleet/{vehicleid}", applicationHandler.VehiclesFleetDisplay)
- application.HandleFunc("/vehicles-management/fleet/{vehicleid}/unavailability", applicationHandler.VehiclesFleetMakeUnavailable)
- application.HandleFunc("/vehicles-management/fleet/{vehicleid}/update", applicationHandler.VehiclesFleetUpdate)
- application.HandleFunc("/vehicles-management/bookings/", applicationHandler.VehiclesManagementBookingsList)
- application.HandleFunc("/vehicles-management/bookings/{bookingid}", applicationHandler.VehicleManagementBookingDisplay)
- application.HandleFunc("/vehicles-management/bookings/{bookingid}/change-vehicle", applicationHandler.VehicleManagementBookingChangeVehicle)
- /////////////////////////////////////Remove booking vehicle/////////////////////////////////////////
- application.HandleFunc("/vehicles-management/bookings/{bookingid}/delete", applicationHandler.UnbookingVehicle)
- // application.HandleFunc("/vehicles-management/bookings/{bookingid}/delete", applicationHandler.DeleteBooking)
- ////////////////////////////////////////////////////////////////////////////////////////////////////
- application.HandleFunc("/vehicles-management/bookings/{bookingid}/documents/{document}", applicationHandler.BookingDocumentDownload)
- application.HandleFunc("/agenda/", applicationHandler.AgendaHome)
- application.HandleFunc("/agenda/history", applicationHandler.AgendaHistory)
- application.HandleFunc("/agenda/create-event", applicationHandler.AgendaCreateEvent)
- application.HandleFunc("/agenda/{eventid}", applicationHandler.AgendaDisplayEvent)
- ///////////////////////////////Code to modify event///////////////////////
- application.HandleFunc("/agenda/{eventid}/update", applicationHandler.AgendaUpdateEvent)
- application.HandleFunc("/agenda/{eventid}/delete", applicationHandler.AgendaDeleteEvent)
-
- /////////////////////////////////////////////////////
- application.HandleFunc("/agenda/{eventid}/subscribe", applicationHandler.AgendaSubscribeEvent)
- application.HandleFunc("/directory/", applicationHandler.DirectoryHome)
-
- application.HandleFunc("/group/settings", applicationHandler.GroupSettingsDisplay)
- application.HandleFunc("/group/settings/invite-member", applicationHandler.GroupSettingsInviteMember)
-
- /****************************Groupe Déplacement ************************************/
- application.HandleFunc("/journeys/groups_covoiturage", applicationHandler.GroupsGestion)
- application.HandleFunc("/journeys/groups_covoiturage/create", applicationHandler.CreateGroup)
- application.HandleFunc("/journeys/groups_covoiturage/create/{groupid}", applicationHandler.DisplayGroupCovoiturage)
- application.HandleFunc("/journeys/groups_covoiturage/create/{id}/{groupid}/{memberid}", applicationHandler.UpdateGroupCovoiturage)
- /****************************************************************/
-
- /********************Code Supprt Emailing************************/
- application.HandleFunc("/support/", applicationHandler.SupportSend)
-
- /*********************** CODE GROUP **************************/
- appGroup := application.PathPrefix("/group_module").Subrouter()
- appGroup.HandleFunc("/", applicationHandler.Groups)
- appGroup.HandleFunc("/groups", applicationHandler.CreateGroupModule)
- appGroup.HandleFunc("/groups/{groupid}", applicationHandler.DisplayGroupModule)
-
- // TODO Subrouters with middlewares checking security for each module ?
- application.Use(idp.Middleware)
- application.Use(idp.GroupsMiddleware)
-
- appAdmin := application.PathPrefix("/administration").Subrouter()
- appAdmin.HandleFunc("/", applicationHandler.Administration)
- appAdmin.HandleFunc("/groups/", applicationHandler.AdministrationCreateGroup)
- appAdmin.HandleFunc("/groups/{groupid}", applicationHandler.AdministrationGroupDisplay)
- appAdmin.HandleFunc("/groups/{groupid}/invite-admin", applicationHandler.AdministrationGroupInviteAdmin)
- appAdmin.HandleFunc("/groups/{groupid}/invite-member", applicationHandler.AdministrationGroupInviteMember)
- // add statistiques
- appAdmin.HandleFunc("/stats/vehicles", applicationHandler.AdminStatVehicles)
- appAdmin.HandleFunc("/stats/bookings", applicationHandler.AdminStatBookings)
- appAdmin.HandleFunc("/stats/beneficaires", applicationHandler.AdminStatBeneficaires)
- appAdmin.HandleFunc("/stats/events", applicationHandler.AdminStatEvents)
-
- /////////////////////////////////////Delete subscriber///////////////////////////////////////////////
- application.HandleFunc("/agenda/{eventid}/{subscribeid}/delete", applicationHandler.AgendaDeleteSubscribeEvent)
- application.HandleFunc("/agenda/{eventid}/history", applicationHandler.AgendaHistoryEvent)
- /////////////////////////////////////////////////////////////////////////////////////////////////////
-
- export := r.PathPrefix("/exports").Subrouter()
- export.HandleFunc("/fleets/bookings", exportsHandler.Bookings)
- export.HandleFunc("/fleets/bookings/{groupid}", exportsHandler.Bookings)
- export.HandleFunc("/agenda/subscriptions", exportsHandler.Agenda("allEvents"))
- export.HandleFunc("/agenda/{eventid}", exportsHandler.Agenda("oneEvent"))
- export.Use(idp.Middleware)
- export.Use(idp.GroupsMiddleware)
-
- fmt.Println("-> HTTP server listening on", address)
-
- srv := &http.Server{
- Handler: r,
- Addr: address,
- WriteTimeout: 15 * time.Second,
- ReadTimeout: 15 * time.Second,
+ if mcpEnabled {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ mcp.Run(cfg, svc, applicationHandler, kv, filestorage)
+ }()
}
- log.Fatal(srv.ListenAndServe())
-}
-
-func redirectApp(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "/app/", http.StatusFound)
+ if publicwebEnabled {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ publicweb.Run(cfg, svc, applicationHandler, kv, filestorage)
+ }()
+ }
+
+ wg.Wait()
}
diff --git a/renderer/administration.go b/renderer/administration.go
old mode 100644
new mode 100755
index aaa41ff..4c7d105
--- a/renderer/administration.go
+++ b/renderer/administration.go
@@ -100,7 +100,7 @@ func (renderer *Renderer) AdminStatVehicles(w http.ResponseWriter, r *http.Reque
renderer.Render("vehicles_state", w, r, files, state)
}
-func (renderer *Renderer) AdminStatBookings(w http.ResponseWriter, r *http.Request, vehicles map[string]fleetsstorage.Vehicle, bookings []fleetsstorage.Booking, admingroups map[string]any, beneficiaries map[string]any) {
+func (renderer *Renderer) AdminStatBookings(w http.ResponseWriter, r *http.Request, vehicles map[string]fleetsstorage.Vehicle, bookings []fleetsstorage.Booking, admingroups map[string]any, beneficiaries map[string]any, filters map[string]string) {
files := renderer.ThemeConfig.GetStringSlice("views.administration.bookings_list.files")
state := NewState(r, renderer.ThemeConfig, administrationMenu)
state.ViewState = map[string]any{
@@ -108,6 +108,7 @@ func (renderer *Renderer) AdminStatBookings(w http.ResponseWriter, r *http.Reque
"bookings": bookings,
"admingroups": admingroups,
"beneficiaries_map": beneficiaries,
+ "filters": filters,
}
renderer.Render("bookings_stats", w, r, files, state)
diff --git a/renderer/agenda.go b/renderer/agenda.go
old mode 100644
new mode 100755
index 09f41f3..bc65c43
--- a/renderer/agenda.go
+++ b/renderer/agenda.go
@@ -30,22 +30,31 @@ func (renderer *Renderer) AgendaHistory(w http.ResponseWriter, r *http.Request,
renderer.Render("agenda history", w, r, files, state)
}
-func (renderer *Renderer) AgendaCreateEvent(w http.ResponseWriter, r *http.Request) {
+func (renderer *Renderer) AgendaCreateEvent(w http.ResponseWriter, r *http.Request, events_file_types []string, file_types_map map[string]string, documents any) {
files := renderer.ThemeConfig.GetStringSlice("views.agenda.create_event.files")
state := NewState(r, renderer.ThemeConfig, agendaMenu)
+ state.ViewState = map[string]any{
+ "events_file_types": events_file_types,
+ "file_types_map": file_types_map,
+ "documents": documents,
+ }
+
renderer.Render("agenda create event", w, r, files, state)
}
-func (renderer *Renderer) AgendaDisplayEvent(w http.ResponseWriter, r *http.Request, event any, group any, subscribers map[string]any, beneficiaries any) {
+func (renderer *Renderer) AgendaDisplayEvent(w http.ResponseWriter, r *http.Request, event any, group any, events_file_types []string, file_types_map map[string]string, documents any, subscribers map[string]any, beneficiaries any) {
files := renderer.ThemeConfig.GetStringSlice("views.agenda.display_event.files")
state := NewState(r, renderer.ThemeConfig, agendaMenu)
state.ViewState = map[string]any{
- "event": event,
- "group": group,
- "subscribers": subscribers,
- "beneficiaries": beneficiaries,
+ "event": event,
+ "group": group,
+ "events_file_types": events_file_types,
+ "file_types_map": file_types_map,
+ "documents": documents,
+ "subscribers": subscribers,
+ "beneficiaries": beneficiaries,
}
renderer.Render("agenda create event", w, r, files, state)
@@ -100,3 +109,18 @@ func (renderer *Renderer) AgendaDeleteEvent(w http.ResponseWriter, r *http.Reque
renderer.Render("event_deleteEvent", w, r, files, state)
}
+
+////////Event documents////////////////////////
+// func (renderer *Renderer) EventDocuments(w http.ResponseWriter, r *http.Request, event any, documents []any) {
+// files := renderer.ThemeConfig.GetStringSlice("views.agenda.event_files.files")
+// state := NewState(r, renderer.ThemeConfig, agendaMenu)
+
+// state.ViewState = map[string]any{
+// "event": event,
+// "documents": documents,
+// "eventid": event.(map[string]any)["id"],
+// "eventtitle": event.(map[string]any)["title"],
+// }
+
+// renderer.Render("event_files", w, r, files, state)
+// }
\ No newline at end of file
diff --git a/renderer/auth.go b/renderer/auth.go
old mode 100644
new mode 100755
diff --git a/renderer/beneficiaries.go b/renderer/beneficiaries.go
old mode 100644
new mode 100755
index 51a9ca9..ffb7361
--- a/renderer/beneficiaries.go
+++ b/renderer/beneficiaries.go
@@ -5,6 +5,7 @@ import (
"html/template"
"net/http"
+ fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
@@ -14,6 +15,7 @@ type BeneficiariesListState struct {
Count int `json:"count"`
CacheId string `json:"cache_id"`
Beneficiaries []mobilityaccountsstorage.Account `json:"beneficiaries"`
+ Archived bool `json:"archived"`
}
func (s BeneficiariesListState) JSON() template.JS {
@@ -28,7 +30,7 @@ func (s BeneficiariesListState) JSONWithLimits(a int, b int) template.JS {
return s.JSON()
}
-func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string) {
+func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string, archived bool) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.list.files")
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
@@ -36,6 +38,7 @@ func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Reque
Count: len(accounts),
CacheId: cacheid,
Beneficiaries: accounts,
+ Archived: archived,
}
renderer.Render("beneficiaries_list", w, r, files, state)
@@ -43,7 +46,12 @@ func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Reque
func (renderer *Renderer) BeneficiaryCreate(w http.ResponseWriter, r *http.Request) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.create.files")
+ profileFields := renderer.GlobalConfig.Get("modules.beneficiaries.profile_optional_fields")
+
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
+ state.ViewState = map[string]any{
+ "profile_optional_fields": profileFields,
+ }
renderer.Render("beneficiaries_create", w, r, files, state)
}
@@ -52,25 +60,42 @@ type BeneficiariesDisplayState struct {
Beneficiary any
}
-func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Request, beneficiary any, bookings []any, organizations []any, beneficiaries_file_types []string, file_types_map map[string]string, documents any) {
+func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Request, beneficiary any, bookings []fleetsstorage.Booking, organizations []any, beneficiaries_file_types []string, file_types_map map[string]string, documents any, event interface{}, solidarityTransportStats any, solidarityTransportBookings any, solidarityDriversMap any, organizedCarpoolStats any, organizedCarpoolBookings any, organizedCarpoolDriversMap any, walletBalance float64, tab string) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.display.files")
+ profileFields := renderer.GlobalConfig.Get("modules.beneficiaries.profile_optional_fields")
+
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
state.ViewState = map[string]any{
- "beneficiary": beneficiary,
- "bookings": bookings,
- "beneficiaries_file_types": beneficiaries_file_types,
- "file_types_map": file_types_map,
- "documents": documents,
- "organizations": organizations,
+ "beneficiary": beneficiary,
+ "bookings": bookings,
+ "beneficiaries_file_types": beneficiaries_file_types,
+ "file_types_map": file_types_map,
+ "documents": documents,
+ "organizations": organizations,
+ "event": event,
+ "solidarity_transport_stats": solidarityTransportStats,
+ "solidarity_transport_bookings": solidarityTransportBookings,
+ "solidarity_transport_drivers_map": solidarityDriversMap,
+ "organized_carpool_stats": organizedCarpoolStats,
+ "organized_carpool_bookings": organizedCarpoolBookings,
+ "organized_carpool_drivers_map": organizedCarpoolDriversMap,
+ "profile_optional_fields": profileFields,
+ "wallet_balance": walletBalance,
+ "tab": tab,
+ "search_view": renderer.GlobalConfig.GetString("modules.journeys.search_view"),
}
-
renderer.Render("beneficiaries_display", w, r, files, state)
}
func (renderer *Renderer) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request, beneficiary any) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.update.files")
+ profileFields := renderer.GlobalConfig.Get("modules.beneficiaries.profile_optional_fields")
+
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
- state.ViewState = beneficiary
+ state.ViewState = map[string]any{
+ "beneficiary": beneficiary,
+ "profile_optional_fields": profileFields,
+ }
renderer.Render("beneficiaries_update", w, r, files, state)
}
diff --git a/renderer/dashboard.go b/renderer/dashboard.go
old mode 100644
new mode 100755
index cd88997..0680e68
--- a/renderer/dashboard.go
+++ b/renderer/dashboard.go
@@ -4,11 +4,13 @@ import (
"net/http"
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
+ fleetstorage "git.coopgo.io/coopgo-platform/fleets/storage"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
const dashboardMenu = "dashboard"
-func (renderer *Renderer) Dashboard(w http.ResponseWriter, r *http.Request, accounts []any, nbaccounts int, count_members int, events []agendastorage.Event) {
+func (renderer *Renderer) Dashboard(w http.ResponseWriter, r *http.Request, accounts any, nbaccounts int, count_members int, events []agendastorage.Event, fleets []fleetstorage.Booking, solidarityDrivers []mobilityaccountsstorage.Account, organizedCarpoolDrivers []mobilityaccountsstorage.Account, driverAddressGeo string, enrichedGeoFilters []map[string]string) {
files := renderer.ThemeConfig.GetStringSlice("views.dashboard.files")
state := NewState(r, renderer.ThemeConfig, dashboardMenu)
state.ViewState = map[string]any{
@@ -16,8 +18,16 @@ func (renderer *Renderer) Dashboard(w http.ResponseWriter, r *http.Request, acco
"count": nbaccounts,
"latest": accounts,
},
- "count_members": count_members,
- "events": events,
+ "count_members": count_members,
+ "events": events,
+ "fleets": fleets,
+ "solidarity_drivers": solidarityDrivers,
+ "organized_carpool_drivers": organizedCarpoolDrivers,
+ "geography_filters_enabled": renderer.GlobalConfig.GetBool("geography.filters.enabled"),
+ "geography_filters_list": enrichedGeoFilters,
+ "filters": map[string]any{
+ "driver_address_geo": driverAddressGeo,
+ },
}
renderer.Render("dashboard", w, r, files, state)
diff --git a/renderer/directory.go b/renderer/directory.go
old mode 100644
new mode 100755
diff --git a/renderer/func-maps.go b/renderer/func-maps.go
old mode 100644
new mode 100755
index 3c5b005..9a4902f
--- a/renderer/func-maps.go
+++ b/renderer/func-maps.go
@@ -5,22 +5,51 @@ import (
"encoding/json"
"fmt"
"html/template"
+ "math"
+ "reflect"
"strings"
"time"
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ validatedprofile "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/validated-profile"
+ groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/viper"
"gitlab.scity.coop/maas/navitia-golang/types"
)
-func TimeFrom(d any) *time.Time {
+func ModuleAvailable(group groupsstorage.Group, configmodules *viper.Viper) func(string) bool {
+ return func(module string) bool {
+ if module == "dashboard" {
+ return true
+ }
+ groupmodules := group.Data["modules"].(map[string]any)
+ modAvailable, ok := groupmodules[module].(bool)
+ if ok && modAvailable && configmodules.GetBool(fmt.Sprintf("modules.%s.enabled", module)) {
+ return true
+ }
+ return false
+ }
+}
+func TimeFrom(d any) *time.Time {
+ paris, err := time.LoadLocation("Europe/Paris")
if date, ok := d.(time.Time); ok {
- return &date
+ if err != nil {
+ return &date
+ }
+ nd := date.In(paris)
+ return &nd
} else if date, ok := d.(string); ok {
datetime, err := time.Parse("2006-01-02T15:04:05Z", date)
if err != nil {
- panic(err)
+ datetime, err = time.Parse("2006-01-02", date)
+ if err != nil {
+ log.Error().Err(err).Msg("cannot parse date")
+ }
}
- return &datetime
+ dt := datetime.In(paris)
+ return &dt
}
return nil
}
@@ -78,7 +107,6 @@ func RawJSON(v any) string {
return ""
}
return strings.TrimSuffix(buf.String(), "\n")
-
}
func UnescapeHTML(s string) template.HTML {
@@ -124,6 +152,106 @@ func strval(v interface{}) string {
}
}
+// JSEscape escapes a string for safe use in JavaScript
+func JSEscape(s string) template.JS {
+ return template.JS(template.JSEscapeString(s))
+}
+
+// IsGuaranteedTripMotivation checks if a motivation is a guaranteed trip
+func IsGuaranteedTripMotivation(globalConfig *viper.Viper) func(string) bool {
+ return func(motivation string) bool {
+ guaranteedMotivations := globalConfig.GetStringSlice("modules.solidarity_transport.guaranteed_trip_motivations")
+ for _, m := range guaranteedMotivations {
+ if m == motivation {
+ return true
+ }
+ }
+ 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{
+ "moduleAvailable": ModuleAvailable(group, globalConfig),
+ "timeFrom": TimeFrom,
+ "timeFormat": TimeFormat,
+ "genderISO5218": GenderISO5218,
+ "dict": Dict,
+ "json": JSON,
+ "rawjson": RawJSON,
+ "unescapeHTML": UnescapeHTML,
+ "jsEscape": JSEscape,
+ "walkingLength": WalkingLength,
+ "divideFloat64": Divide[float64],
+ "divideInt": Divide[int],
+ "typeOf": reflect.TypeOf,
+ "shortDuration": ShortDuration,
+ "round2": Round2,
+ "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")),
+ "isGuaranteedTripMotivation": IsGuaranteedTripMotivation(globalConfig),
+ "beneficiaryDocuments": func(id string) []filestorage.FileInfo {
+ return fileStorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + id)
+ },
+ "solidarityDocuments": func(id string) []filestorage.FileInfo {
+ return fileStorage.List(filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS + "/" + id)
+ },
+ "carpoolDocuments": func(id string) []filestorage.FileInfo {
+ return fileStorage.List(filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS + "/" + id)
+ },
+ }
+}
+
func Divide[V int | float64](a, b V) V {
return a / b
}
+
+func ShortDuration(d interface{}) string {
+ var duration time.Duration
+
+ switch v := d.(type) {
+ case time.Duration:
+ duration = v
+ case int:
+ duration = time.Duration(v) * time.Second
+ case int64:
+ duration = time.Duration(v) * time.Second
+ case float64:
+ duration = time.Duration(v) * time.Second
+ default:
+ return ""
+ }
+
+ s := duration.String()
+ if strings.HasSuffix(s, "m0s") {
+ s = s[:len(s)-2]
+ }
+ if strings.HasSuffix(s, "h0m") {
+ s = s[:len(s)-2]
+ }
+ return s
+}
+
+// Round2 rounds a float64 to 2 decimal places to avoid floating point issues
+func Round2(value interface{}) float64 {
+ var f float64
+ switch v := value.(type) {
+ case float64:
+ f = v
+ case *float64:
+ if v != nil {
+ f = *v
+ }
+ case float32:
+ f = float64(v)
+ case int:
+ f = float64(v)
+ case int64:
+ f = float64(v)
+ default:
+ return 0
+ }
+ return math.Round(f*100) / 100
+}
diff --git a/renderer/group.go b/renderer/group.go
old mode 100644
new mode 100755
diff --git a/renderer/group_module.go b/renderer/group_module.go
old mode 100644
new mode 100755
diff --git a/renderer/journeys.go b/renderer/journeys.go
old mode 100644
new mode 100755
index accf061..250d70a
--- a/renderer/journeys.go
+++ b/renderer/journeys.go
@@ -7,6 +7,7 @@ import (
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ geojson "github.com/paulmach/orb/geojson"
)
const journeysMenu = "journeys"
@@ -35,18 +36,30 @@ func (s BeneficiariesCovoiturage) JSONWithLimits(a int, b int) template.JS {
return s.JSON()
}
-func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request, carpools any, transitjourneys any, vehicles []any, searched bool, departure any, destination any, departuredate string, departuretime string) {
+func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request, carpools []*geojson.FeatureCollection, transitjourneys any, vehicles any, searched bool, departure any, destination any, departuredate string, departuretime string, driverJourneys any, solidarityDrivers any, organizedCarpools any, beneficiaries any, kbData any, passengerid string, savedSearches any, beneficiariesMap any) {
files := renderer.ThemeConfig.GetStringSlice("views.journeys.search.files")
state := NewState(r, renderer.ThemeConfig, journeysMenu)
+ journeyTabs := renderer.ThemeConfig.Get("journey_tabs")
state.ViewState = map[string]any{
- "searched": searched,
- "departuredate": departuredate,
- "departuretime": departuretime,
- "departure": departure,
- "destination": destination,
- "journeys": transitjourneys,
- "carpools": carpools,
- "vehicles": vehicles,
+ "searched": searched,
+ "departuredate": departuredate,
+ "departuretime": departuretime,
+ "departure": departure,
+ "destination": destination,
+ "journeys": transitjourneys,
+ "carpools": carpools,
+ "organized_carpools": organizedCarpools,
+ "vehicles": vehicles,
+ "driver_journeys": driverJourneys,
+ "solidarity_drivers": solidarityDrivers,
+ "querystring": r.URL.RawQuery,
+ "beneficiaries": beneficiariesMap,
+ "beneficiaries_list": beneficiaries,
+ "kb_data": kbData,
+ "passengerid": passengerid,
+ "journey_tabs": journeyTabs,
+ "saved_searches": savedSearches,
+ "search_view": renderer.GlobalConfig.GetString("modules.journeys.search_view"),
}
renderer.Render("journeys", w, r, files, state)
@@ -69,6 +82,7 @@ func (s BeneficiariesListstate) JSONWithLimits(a int, b int) template.JS {
}
return s.JSON()
}
+
func (renderer *Renderer) GroupsGestion(w http.ResponseWriter, r *http.Request, groups []groupstorage.Group, cacheid string) {
files := renderer.ThemeConfig.GetStringSlice("views.journeys.list.files")
state := NewState(r, renderer.ThemeConfig, journeysMenu)
@@ -98,7 +112,6 @@ func (renderer *Renderer) CreateGroup(w http.ResponseWriter, r *http.Request, de
viewstate["search"] = map[string]any{
"beneficiary": beneficiary,
}
-
}
state.ViewState = viewstate
@@ -130,7 +143,6 @@ func (renderer *Renderer) DisplayGroupCovoiturage(w http.ResponseWriter, r *http
viewstate["search"] = map[string]any{
"beneficiary": beneficiary,
}
-
}
state.ViewState = viewstate
@@ -146,5 +158,29 @@ func (renderer *Renderer) UpdateGroupCovoiturage(w http.ResponseWriter, r *http.
"memberid": memberid,
}
renderer.Render("journeys", w, r, files, state)
-
+}
+
+func (renderer *Renderer) JourneysSearchCompact(w http.ResponseWriter, r *http.Request, carpools []*geojson.FeatureCollection, transitjourneys any, vehicles any, searched bool, departure any, destination any, departuredate string, departuretime string, driverJourneys any, solidarityDrivers any, organizedCarpools any, kbData any, passengerid string) {
+ files := renderer.ThemeConfig.GetStringSlice("views.journeys.search_compact.files")
+ vehicleOptionalFields := renderer.GlobalConfig.Get("modules.fleets.vehicle_optional_fields")
+
+ state := NewState(r, renderer.ThemeConfig, journeysMenu)
+ state.ViewState = map[string]any{
+ "searched": searched,
+ "departuredate": departuredate,
+ "departuretime": departuretime,
+ "departure": departure,
+ "destination": destination,
+ "journeys": transitjourneys,
+ "carpools": carpools,
+ "organized_carpools": organizedCarpools,
+ "vehicles": vehicles,
+ "vehicle_optional_fields": vehicleOptionalFields,
+ "driver_journeys": driverJourneys,
+ "solidarity_drivers": solidarityDrivers,
+ "kb_data": kbData,
+ "passengerid": passengerid,
+ }
+
+ renderer.Render("journeys", w, r, files, state)
}
diff --git a/renderer/layout.go b/renderer/layout.go
old mode 100644
new mode 100755
index 27ecffc..b723eed
--- a/renderer/layout.go
+++ b/renderer/layout.go
@@ -2,9 +2,14 @@ package renderer
type LayoutState struct {
AdministrationState AdministrationState
- MenuItems []MenuItem
+ Menu any
+ ActiveMenu string
+
+ // DEPRECATED
+ MenuItems []MenuItem
}
+// DEPRECATED
type MenuItem struct {
Title string
Link string
diff --git a/renderer/mailer.go b/renderer/mailer.go
old mode 100644
new mode 100755
diff --git a/renderer/members.go b/renderer/members.go
old mode 100644
new mode 100755
index ad96005..91fa11f
--- a/renderer/members.go
+++ b/renderer/members.go
@@ -12,19 +12,26 @@ const membersMenu = "members"
func (renderer *Renderer) MemberDisplay(w http.ResponseWriter, r *http.Request, admins any, groups []string) {
files := renderer.ThemeConfig.GetStringSlice("views.members.display.files")
+ profileFields := renderer.GlobalConfig.Get("modules.members.profile_optional_fields")
state := NewState(r, renderer.ThemeConfig, membersMenu)
state.ViewState = map[string]any{
- "admins": admins,
- "groups": groups,
+ "admins": admins,
+ "groups": groups,
+ "profile_optional_fields": profileFields,
}
renderer.Render("members_list", w, r, files, state)
}
func (renderer *Renderer) MemberUpdate(w http.ResponseWriter, r *http.Request, user any) {
files := renderer.ThemeConfig.GetStringSlice("views.members.update.files")
+ profileFields := renderer.GlobalConfig.Get("modules.members.profile_optional_fields")
+
state := NewState(r, renderer.ThemeConfig, membersMenu)
- state.ViewState = user
+ state.ViewState = map[string]any{
+ "user": user,
+ "profile_optional_fields": profileFields,
+ }
renderer.Render("members_update", w, r, files, state)
}
@@ -53,7 +60,6 @@ func (renderer *Renderer) MembersList(w http.ResponseWriter, r *http.Request, ac
state := NewState(r, renderer.ThemeConfig, membersMenu)
state.ViewState = map[string]any{
-
"list": MembersListState{
Count: len(accounts),
CacheId: cacheid,
diff --git a/renderer/organized-carpool.go b/renderer/organized-carpool.go
new file mode 100644
index 0000000..3b760d5
--- /dev/null
+++ b/renderer/organized-carpool.go
@@ -0,0 +1,110 @@
+package renderer
+
+import (
+ "net/http"
+
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "git.coopgo.io/coopgo-platform/payments/pricing"
+)
+
+const organizedCarpoolMenu = "organized_carpool"
+
+func (renderer *Renderer) OrganizedCarpoolOverview(w http.ResponseWriter, r *http.Request, drivers any, driversMap any, passengersMap any, bookings any, bookingsHistory any, filters map[string]any, histFilters map[string]any, tab string, enrichedGeoFilters []map[string]string, archived bool) {
+ files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.overview.files")
+ tripsItemsPerPage := renderer.GlobalConfig.GetInt("modules.organized_carpool.pagination.trips_items_per_page")
+ driversItemsPerPage := renderer.GlobalConfig.GetInt("modules.organized_carpool.pagination.drivers_items_per_page")
+
+ geoFiltersEnabled := len(enrichedGeoFilters) > 0
+
+ state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
+ state.ViewState = map[string]any{
+ "drivers": drivers,
+ "drivers_map": driversMap,
+ "passengers_map": passengersMap,
+ "bookings": bookings,
+ "bookings_history": bookingsHistory,
+ "filters": filters,
+ "hist_filters": histFilters,
+ "tab": tab,
+ "trips_items_per_page": tripsItemsPerPage,
+ "drivers_items_per_page": driversItemsPerPage,
+ "geography_filters_enabled": geoFiltersEnabled,
+ "geography_filters_list": enrichedGeoFilters,
+ "archived": archived,
+ }
+
+ renderer.Render("organized carpool overview", w, r, files, state)
+}
+
+func (renderer *Renderer) OrganizedCarpoolCreateDriver(w http.ResponseWriter, r *http.Request) {
+ files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.driver_create.files")
+ state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
+ state.ViewState = map[string]any{}
+
+ renderer.Render("organized carpool driver creation", w, r, files, state)
+}
+
+func (renderer *Renderer) OrganizedCarpoolUpdateDriver(w http.ResponseWriter, r *http.Request, driver any) {
+ files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.driver_update.files")
+ state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
+ state.ViewState = map[string]any{
+ "driver": driver,
+ }
+
+ renderer.Render("organized carpool driver update", w, r, files, state)
+}
+
+func (renderer *Renderer) OrganizedCarpoolDriverDisplay(w http.ResponseWriter, r *http.Request, driver mobilityaccountsstorage.Account, trips any, documents any, bookings any, beneficiariesMap any, stats map[string]any, walletBalance float64, tab string) {
+ files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.driver_display.files")
+ state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
+
+ drivers_file_types := renderer.GlobalConfig.GetStringSlice("modules.organized_carpool.drivers.documents_types")
+ file_types_map := renderer.GlobalConfig.GetStringMapString("storage.files.file_types")
+
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "trips": trips,
+ "documents": documents,
+ "bookings": bookings,
+ "beneficiaries_map": beneficiariesMap,
+ "stats": stats,
+ "drivers_file_types": drivers_file_types,
+ "file_types_map": file_types_map,
+ "wallet_balance": walletBalance,
+ "tab": tab,
+ }
+
+ renderer.Render("organized carpool driver display", w, r, files, state)
+}
+
+func (renderer *Renderer) OrganizedCarpoolJourney(w http.ResponseWriter, r *http.Request, journey any, driver any, passenger any, beneficiaries any, passengerWalletBalance float64, pricingResult map[string]pricing.Price) {
+ files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.journey.files")
+ bookingMotivations := renderer.GlobalConfig.Get("modules.organized_carpool.booking_motivations")
+ state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "passenger": passenger,
+ "beneficiaries": beneficiaries,
+ "journey": journey,
+ "config": renderer.GlobalConfig,
+ "booking_motivations": bookingMotivations,
+ "passenger_wallet_balance": passengerWalletBalance,
+ "pricing_result": pricingResult,
+ }
+
+ renderer.Render("organized carpool journey", w, r, files, state)
+}
+
+func (renderer *Renderer) OrganizedCarpoolBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, driverDepartureAddress, driverArrivalAddress string) {
+ files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.booking_display.files")
+ state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "passenger": passenger,
+ "booking": booking,
+ "driverDepartureAddress": driverDepartureAddress,
+ "driverArrivalAddress": driverArrivalAddress,
+ }
+
+ renderer.Render("organized carpool booking display", w, r, files, state)
+}
diff --git a/renderer/renderer.go b/renderer/renderer.go
old mode 100644
new mode 100755
index aae7262..606806e
--- a/renderer/renderer.go
+++ b/renderer/renderer.go
@@ -5,11 +5,14 @@ import (
"html/template"
"net/http"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/icons"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/icons"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ xlsxrenderer "git.coopgo.io/coopgo-apps/parcoursmob/renderer/xlsx"
"git.coopgo.io/coopgo-platform/emailing"
"git.coopgo.io/coopgo-platform/groups-management/storage"
- "github.com/coreos/go-oidc"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
@@ -18,9 +21,11 @@ type Renderer struct {
GlobalConfig *viper.Viper
ThemeConfig *viper.Viper
Mailer *emailing.Mailer
+ FileStorage cache.FileStorage
+ XLSX *xlsxrenderer.XLSXRenderer
}
-func NewRenderer(global *viper.Viper, templates_dir string) *Renderer {
+func NewRenderer(global *viper.Viper, templates_dir string, filestorage cache.FileStorage) *Renderer {
theme := viper.New()
theme.SetConfigName("config")
theme.AddConfigPath(templates_dir)
@@ -32,11 +37,12 @@ func NewRenderer(global *viper.Viper, templates_dir string) *Renderer {
TemplatesDir: templates_dir,
GlobalConfig: global,
ThemeConfig: theme,
+ FileStorage: filestorage,
+ XLSX: xlsxrenderer.NewXLSXRenderer(global),
}
}
func (renderer *Renderer) Render(name string, w http.ResponseWriter, r *http.Request, files []string, state RenderState) {
-
genericFiles := renderer.ThemeConfig.GetStringSlice("views.generic.files")
prefixed_files := []string{}
@@ -48,54 +54,28 @@ func (renderer *Renderer) Render(name string, w http.ResponseWriter, r *http.Req
}
w.WriteHeader(http.StatusOK)
- t := template.New(name).Funcs(
- template.FuncMap{
- "timeFrom": TimeFrom,
- "timeFormat": TimeFormat,
- "genderISO5218": GenderISO5218,
- "dict": Dict,
- "json": JSON,
- "rawjson": RawJSON,
- "unescapeHTML": UnescapeHTML,
- "walkingLength": WalkingLength,
- "divideFloat64": Divide[float64],
- "divideInt": Divide[int],
- },
- )
+ t := template.New(name).Funcs(GetTemplateFuncMap(state.Group, renderer.GlobalConfig, renderer.FileStorage))
t = template.Must(t.ParseFiles(prefixed_files...))
err := t.ExecuteTemplate(w, "main", state)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("issue executing template")
}
}
func (renderer *Renderer) RenderNoLayout(name string, w http.ResponseWriter, r *http.Request, files []string, state RenderState) {
-
prefixed_files := []string{}
for _, f := range files {
prefixed_files = append(prefixed_files, renderer.templateFile(f))
}
w.WriteHeader(http.StatusOK)
- t := template.New(name).Funcs(
- template.FuncMap{
- "timeFrom": TimeFrom,
- "timeFormat": TimeFormat,
- "genderISO5218": GenderISO5218,
- "dict": Dict,
- "json": JSON,
- "rawjson": RawJSON,
- "unsescapeHTML": UnescapeHTML,
- "divideFloat64": Divide[float64],
- "divideInt": Divide[int],
- },
- )
+ t := template.New(name).Funcs(GetTemplateFuncMap(state.Group, renderer.GlobalConfig, renderer.FileStorage))
t = template.Must(t.ParseFiles(prefixed_files...))
err := t.ExecuteTemplate(w, "main", state)
if err != nil {
- fmt.Println(err)
+ log.Error().Err(err).Msg("issue executing template")
}
}
@@ -115,6 +95,7 @@ type RenderState struct {
func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) RenderState {
iconset := themeConfig.GetStringMapString("icons.svg")
+ menu := themeConfig.Get("menu_items")
// Get State elements from Request
var userid string
@@ -160,6 +141,10 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
Active: menuState == administrationMenu,
},
+ Menu: menu,
+ ActiveMenu: menuState,
+
+ // DEPRECATED
MenuItems: []MenuItem{
{
Title: "Tableau de bord",
@@ -170,6 +155,7 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
},
}
+ // DEPRECATED
if modules["beneficiaries"] != nil && modules["beneficiaries"].(bool) {
ls.MenuItems = append(ls.MenuItems, MenuItem{
Title: "Bénéficiaires",
@@ -188,6 +174,15 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
})
}
+ if modules["solidarity_transport"] != nil && modules["solidarity_transport"].(bool) {
+ ls.MenuItems = append(ls.MenuItems, MenuItem{
+ Title: "Transport solidaire",
+ Link: "/app/solidarity-transport/",
+ Active: menuState == solidarityTransportMenu,
+ Icon: "tabler-icons:car",
+ })
+ }
+
if modules["vehicles"] != nil && modules["vehicles"].(bool) {
ls.MenuItems = append(ls.MenuItems, MenuItem{
Title: "Véhicules partagés",
@@ -222,7 +217,6 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
Active: menuState == groupMenu,
Icon: "hero:outline/group_module",
})
-
}
if modules["support"] != nil && modules["support"].(bool) {
@@ -232,7 +226,6 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
Active: menuState == commentMenu,
Icon: "hero:outline/support",
})
-
}
if modules["directory"] != nil && modules["directory"].(bool) {
@@ -243,14 +236,7 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
Icon: "hero:outline/document-text",
})
}
- if modules["conseillers"] != nil && modules["conseillers"].(bool) {
- ls.MenuItems = append(ls.MenuItems, MenuItem{
- Title: "Conseillers",
- Link: "/app/conseillers/",
- Active: menuState == membersMenu,
- Icon: "hero:outline/user-group",
- })
- }
+
return RenderState{
IconSet: icons.NewIconSet(iconset),
Group: group,
diff --git a/renderer/solidarity-transport-ext.go b/renderer/solidarity-transport-ext.go
new file mode 100644
index 0000000..315af40
--- /dev/null
+++ b/renderer/solidarity-transport-ext.go
@@ -0,0 +1,16 @@
+package renderer
+
+import "net/http"
+
+func (renderer *Renderer) SolidarityTransportExternalBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any) {
+ files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.ext.booking_proposal.files")
+ state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "passenger": passenger,
+ "booking": booking,
+ "config": renderer.GlobalConfig,
+ }
+
+ renderer.RenderNoLayout("booking display", w, r, files, state)
+}
diff --git a/renderer/solidarity-transport.go b/renderer/solidarity-transport.go
new file mode 100644
index 0000000..1860d9a
--- /dev/null
+++ b/renderer/solidarity-transport.go
@@ -0,0 +1,130 @@
+package renderer
+
+import (
+ "net/http"
+
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "git.coopgo.io/coopgo-platform/payments/pricing"
+)
+
+const solidarityTransportMenu = "solidarity_transport"
+
+func (renderer *Renderer) SolidarityTransportOverview(w http.ResponseWriter, r *http.Request, drivers any, driversMap any, passengersMap any, bookings any, bookingsHistory any, filters any, hist_filters any, tab string, enrichedGeoFilters []map[string]string, archived bool) {
+ files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.overview.files")
+ tripsItemsPerPage := renderer.GlobalConfig.GetInt("modules.solidarity_transport.pagination.trips_items_per_page")
+ driversItemsPerPage := renderer.GlobalConfig.GetInt("modules.solidarity_transport.pagination.drivers_items_per_page")
+
+ guaranteedMotivations := renderer.GlobalConfig.GetStringSlice("modules.solidarity_transport.guaranteed_trip_motivations")
+
+ // Geography filters
+ geoFiltersEnabled := len(enrichedGeoFilters) > 0
+
+ state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
+ state.ViewState = map[string]any{
+ "drivers": drivers,
+ "drivers_map": driversMap,
+ "passengers_map": passengersMap,
+ "bookings": bookings,
+ "bookings_history": bookingsHistory,
+ "filters": filters,
+ "hist_filters": hist_filters,
+ "tab": tab,
+ "trips_items_per_page": tripsItemsPerPage,
+ "drivers_items_per_page": driversItemsPerPage,
+ "guaranteed_trip_motivations": guaranteedMotivations,
+ "geography_filters_enabled": geoFiltersEnabled,
+ "geography_filters_list": enrichedGeoFilters,
+ "archived": archived,
+ }
+
+ renderer.Render("solidarity transport overview", w, r, files, state)
+}
+
+func (renderer *Renderer) SolidarityTransportCreateDriver(w http.ResponseWriter, r *http.Request) {
+ files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.driver_create.files")
+ profileFields := renderer.GlobalConfig.Get("modules.solidarity_transport.drivers.profile_optional_fields")
+
+ state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
+ state.ViewState = map[string]any{
+ "profile_optional_fields": profileFields,
+ }
+
+ renderer.Render("solidarity transport driver creation", w, r, files, state)
+}
+
+func (renderer *Renderer) SolidarityTransportUpdateDriver(w http.ResponseWriter, r *http.Request, driver any) {
+ files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.driver_update.files")
+ profileFields := renderer.GlobalConfig.Get("modules.solidarity_transport.drivers.profile_optional_fields")
+
+ state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "profile_optional_fields": profileFields,
+ }
+
+ renderer.Render("solidarity transport driver update", w, r, files, state)
+}
+
+func (renderer *Renderer) SolidarityTransportDriverDisplay(w http.ResponseWriter, r *http.Request, driver mobilityaccountsstorage.Account, availabilities any, documents any, bookings any, beneficiariesMap any, stats map[string]any, walletBalance float64, tab string) {
+ files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.driver_display.files")
+ profileFields := renderer.GlobalConfig.Get("modules.solidarity_transport.drivers.profile_optional_fields")
+ state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
+
+ drivers_file_types := renderer.GlobalConfig.GetStringSlice("modules.solidarity_transport.drivers.documents_types")
+ file_types_map := renderer.GlobalConfig.GetStringMapString("storage.files.file_types")
+
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "availabilities": availabilities,
+ "bookings": bookings,
+ "beneficiaries_map": beneficiariesMap,
+ "documents": documents,
+ "drivers_file_types": drivers_file_types,
+ "file_types_map": file_types_map,
+ "stats": stats,
+ "profile_optional_fields": profileFields,
+ "wallet_balance": walletBalance,
+ "tab": tab,
+ }
+
+ renderer.Render("solidarity transport driver creation", w, r, files, state)
+}
+
+func (renderer *Renderer) SolidarityTransportDriverJourney(w http.ResponseWriter, r *http.Request, driverJourney any, driver any, passenger any, beneficiaries any, passengerWalletBalance float64, pricingResult map[string]pricing.Price, replacesBookingID string) {
+ files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.driver_journey.files")
+ bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
+ state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "passenger": passenger,
+ "beneficiaries": beneficiaries,
+ "driver_journey": driverJourney,
+ "config": renderer.GlobalConfig,
+ "passenger_wallet_balance": passengerWalletBalance,
+ "pricing_result": pricingResult,
+ "booking_motivations": bookingMotivations,
+ "replaces_booking_id": replacesBookingID,
+ }
+
+ renderer.Render("solidarity transport driver creation", w, r, files, state)
+}
+
+func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, passengerWalletBalance float64, replacementDrivers any, replacementDriversMap any, replacementPricing any, replacementLocations any) {
+ files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.booking_display.files")
+ bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
+ state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
+ state.ViewState = map[string]any{
+ "driver": driver,
+ "passenger": passenger,
+ "booking": booking,
+ "config": renderer.GlobalConfig,
+ "passenger_wallet_balance": passengerWalletBalance,
+ "booking_motivations": bookingMotivations,
+ "replacement_drivers": replacementDrivers,
+ "replacement_drivers_map": replacementDriversMap,
+ "replacement_pricing": replacementPricing,
+ "replacement_locations": replacementLocations,
+ }
+
+ renderer.Render("booking display", w, r, files, state)
+}
diff --git a/renderer/support.go b/renderer/support.go
old mode 100644
new mode 100755
diff --git a/renderer/vehicle-management.go b/renderer/vehicle-management.go
old mode 100644
new mode 100755
index eda9d35..25e62f1
--- a/renderer/vehicle-management.go
+++ b/renderer/vehicle-management.go
@@ -3,31 +3,35 @@ package renderer
import (
"net/http"
- filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
const vehiclesmanagementMenu = "vehicles_management"
-func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request, vehicles []fleetsstorage.Vehicle, vehicles_map map[string]fleetsstorage.Vehicle, 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) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.overview.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
"vehicles": vehicles,
"bookings": bookings,
"vehicles_map": vehicles_map,
+ "drivers_map": driversMap,
}
renderer.Render("fleet overview", w, r, files, state)
}
-func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request, vehicles_map map[string]fleetsstorage.Vehicle, bookings []fleetsstorage.Booking, cacheid 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) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.bookings_list.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
"bookings": bookings,
- "vehicles_map": vehicles_map,
+ "vehicles_map": vehiclesMap,
+ "drivers_map": driversMap,
"cacheid": cacheid,
+ "filters": filters,
}
renderer.Render("fleet overview", w, r, files, state)
@@ -35,19 +39,26 @@ func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter,
func (renderer *Renderer) VehiclesFleetAdd(w http.ResponseWriter, r *http.Request, vehicle_types []string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.fleet_add.files")
+ vehicleOptionalFields := renderer.GlobalConfig.Get("modules.fleets.vehicle_optional_fields")
+
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
- "vehicle_types": vehicle_types,
+ "vehicle_types": vehicle_types,
+ "vehicle_optional_fields": vehicleOptionalFields,
}
renderer.Render("fleet add vehicle", w, r, files, state)
}
-func (renderer *Renderer) VehiclesFleetDisplay(w http.ResponseWriter, r *http.Request, vehicle any) {
+func (renderer *Renderer) VehiclesFleetDisplay(w http.ResponseWriter, r *http.Request, vehicle any, beneficiaries any) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.fleet_display.files")
+ vehicleOptionalFields := renderer.GlobalConfig.Get("modules.fleets.vehicle_optional_fields")
+
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
- "vehicle": vehicle,
+ "vehicle": vehicle,
+ "beneficiaries": beneficiaries,
+ "vehicle_optional_fields": vehicleOptionalFields,
}
renderer.Render("fleet display vehicle", w, r, files, state)
@@ -55,10 +66,13 @@ func (renderer *Renderer) VehiclesFleetDisplay(w http.ResponseWriter, r *http.Re
func (renderer *Renderer) VehiclesFleetUpdate(w http.ResponseWriter, r *http.Request, vehicle any, vehicle_types []string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.fleet_update.files")
+ vehicleOptionalFields := renderer.GlobalConfig.Get("modules.fleets.vehicle_optional_fields")
+
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
- "vehicle": vehicle,
- "vehicle_types": vehicle_types,
+ "vehicle": vehicle,
+ "vehicle_types": vehicle_types,
+ "vehicle_optional_fields": vehicleOptionalFields,
}
renderer.Render("fleet display vehicle", w, r, files, state)
@@ -84,9 +98,8 @@ func (renderer *Renderer) UnbookingVehicle(w http.ResponseWriter, r *http.Reques
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.delete_booking.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
- "booking": booking,
+ "booking": booking,
}
renderer.Render("vehicule unbooking", w, r, files, state)
-
-}
\ No newline at end of file
+}
diff --git a/renderer/vehicles.go b/renderer/vehicles.go
old mode 100644
new mode 100755
index 09faa62..b4d1529
--- a/renderer/vehicles.go
+++ b/renderer/vehicles.go
@@ -3,8 +3,9 @@ package renderer
import (
"net/http"
- filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
+ filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
"git.coopgo.io/coopgo-platform/fleets/storage"
+ fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
@@ -22,7 +23,7 @@ func selectDocumentsDefaults(beneficiarydocuments []filestorage.FileInfo, mandat
return res
}
-func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []any, beneficiary any, startdate any, enddate any, mandatory_documents []string, file_types_map map[string]string, beneficiarydocuments []filestorage.FileInfo, selected_type string, automatic bool, vehicles_types []string, admingroups map[string]any) {
+func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []fleetsstorage.Vehicle, beneficiary any, startdate any, enddate any, mandatory_documents []string, file_types_map map[string]string, beneficiarydocuments []filestorage.FileInfo, selected_type string, automatic bool, vehicles_types []string, admingroups map[string]any) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles.search.files")
state := NewState(r, renderer.ThemeConfig, vehiclesMenu)
viewstate := map[string]any{
diff --git a/renderer/xlsx/organized-carpool.go b/renderer/xlsx/organized-carpool.go
new file mode 100644
index 0000000..c07f1bc
--- /dev/null
+++ b/renderer/xlsx/organized-carpool.go
@@ -0,0 +1,273 @@
+package xlsx
+
+import (
+ "fmt"
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/gender"
+ "github.com/rs/zerolog/log"
+)
+
+func (r *XLSXRenderer) OrganizedCarpoolBookings(w http.ResponseWriter, result *application.OrganizedCarpoolBookingsResult) {
+ // Create Excel spreadsheet
+ spreadsheet := r.NewSpreadsheet("Covoiturage solidaire")
+
+ // Build headers dynamically based on configuration
+ headers := []string{
+ "ID Réservation",
+ "Statut",
+ "Motif de réservation",
+ "Date de prise en charge",
+ "Heure de prise en charge",
+ }
+
+ // Add driver fields from config
+ driverOptionalFields := r.Config.Get("modules.organized_carpool.profile_optional_fields")
+ driverFields := []string{"last_name", "first_name", "email", "phone_number"}
+ driverHeaders := []string{"Conducteur - Nom", "Conducteur - Prénom", "Conducteur - Email", "Conducteur - Téléphone"}
+
+ if driverOptionalFieldsList, ok := driverOptionalFields.([]interface{}); ok {
+ for _, field := range driverOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ driverFields = append(driverFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ driverHeaders = append(driverHeaders, fmt.Sprintf("Conducteur - %s", label))
+ }
+ }
+ }
+ }
+ headers = append(headers, driverHeaders...)
+
+ // Add beneficiary fields from config
+ beneficiaryOptionalFields := r.Config.Get("modules.beneficiaries.profile_optional_fields")
+ beneficiaryFields := []string{"last_name", "first_name", "email", "phone_number"}
+ beneficiaryHeaders := []string{"Bénéficiaire - Nom", "Bénéficiaire - Prénom", "Bénéficiaire - Email", "Bénéficiaire - Téléphone"}
+
+ if beneficiaryOptionalFieldsList, ok := beneficiaryOptionalFields.([]interface{}); ok {
+ for _, field := range beneficiaryOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ beneficiaryFields = append(beneficiaryFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ beneficiaryHeaders = append(beneficiaryHeaders, fmt.Sprintf("Bénéficiaire - %s", label))
+ }
+ }
+ }
+ }
+ headers = append(headers, beneficiaryHeaders...)
+
+ // Add journey information headers
+ headers = append(headers,
+ "Lieu de départ - Adresse",
+ "Destination - Adresse",
+ "Distance passager (km)",
+ "Durée trajet (minutes)",
+ "Prix passager",
+ "Devise prix passager",
+ "Compensation conducteur",
+ "Devise compensation",
+ )
+
+ spreadsheet.SetHeaders(headers)
+
+ // Add data rows
+ for _, booking := range result.Bookings {
+ driver := result.DriversMap[booking.Driver.Id]
+ beneficiary := result.BeneficiariesMap[booking.Passenger.Id]
+
+ row := []interface{}{}
+
+ // Booking information
+ row = append(row, booking.Id)
+ row = append(row, booking.Status.String())
+
+ // Motivation
+ motivation := ""
+ if booking.Motivation != nil {
+ motivation = *booking.Motivation
+ }
+ row = append(row, motivation)
+
+ // Journey date and time
+ row = append(row, booking.PassengerPickupDate.AsTime().Format("2006-01-02"))
+ row = append(row, booking.PassengerPickupDate.AsTime().Format("15:04"))
+
+ // Driver data
+ for _, field := range driverFields {
+ row = append(row, getAccountFieldValue(driver.Data, field))
+ }
+
+ // Beneficiary data
+ for _, field := range beneficiaryFields {
+ row = append(row, getAccountFieldValue(beneficiary.Data, field))
+ }
+
+ // Journey information
+ if booking.PassengerPickupAddress != nil {
+ row = append(row, *booking.PassengerPickupAddress)
+ } else {
+ row = append(row, "")
+ }
+
+ if booking.PassengerDropAddress != nil {
+ row = append(row, *booking.PassengerDropAddress)
+ } else {
+ row = append(row, "")
+ }
+
+ // Distance
+ if booking.Distance != nil {
+ row = append(row, *booking.Distance)
+ } else {
+ row = append(row, "")
+ }
+
+ // Duration
+ if booking.Duration != nil {
+ row = append(row, *booking.Duration)
+ } else {
+ row = append(row, "")
+ }
+
+ // Pricing
+ if booking.Price != nil && booking.Price.Amount != nil {
+ row = append(row, fmt.Sprintf("%.2f", *booking.Price.Amount))
+ if booking.Price.Currency != nil {
+ row = append(row, *booking.Price.Currency)
+ } else {
+ row = append(row, "")
+ }
+ } else {
+ row = append(row, "", "")
+ }
+
+ // Driver compensation
+ if booking.DriverCompensationAmount != nil {
+ row = append(row, fmt.Sprintf("%.2f", *booking.DriverCompensationAmount))
+ if booking.DriverCompensationCurrency != nil {
+ row = append(row, *booking.DriverCompensationCurrency)
+ } else {
+ row = append(row, "")
+ }
+ } else {
+ row = append(row, "", "")
+ }
+
+ spreadsheet.AddRow(row)
+ }
+
+ // Write Excel to response
+ w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ w.Header().Set("Content-Disposition", "attachment; filename=\"export-covoiturage-solidaire.xlsx\"")
+
+ if err := spreadsheet.GetFile().Write(w); err != nil {
+ log.Error().Err(err).Msg("Error generating Excel file")
+ http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
+ return
+ }
+}
+
+func (r *XLSXRenderer) OrganizedCarpoolDrivers(w http.ResponseWriter, result *application.OrganizedCarpoolOverviewResult) {
+ // Create Excel spreadsheet
+ spreadsheet := r.NewSpreadsheet("Covoitureurs solidaires")
+
+ // Build headers dynamically based on configuration
+ driverOptionalFields := r.Config.Get("modules.organized_carpool.drivers.profile_optional_fields")
+ driverFields := []string{"last_name", "first_name", "email", "phone_number", "birthdate", "gender", "file_number"}
+ headers := []string{"ID", "Nom", "Prénom", "Email", "Téléphone", "Date de naissance", "Genre", "Numéro de dossier"}
+
+ if driverOptionalFieldsList, ok := driverOptionalFields.([]interface{}); ok {
+ for _, field := range driverOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ driverFields = append(driverFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ headers = append(headers, label)
+ }
+ }
+ }
+ }
+
+ // Add address columns
+ headers = append(headers, "Adresse départ", "Adresse destination", "Archivé")
+
+ spreadsheet.SetHeaders(headers)
+
+ // Add data rows
+ for _, driver := range result.Accounts {
+ row := []interface{}{}
+
+ // Driver ID
+ row = append(row, driver.ID)
+
+ // Driver data
+ for _, field := range driverFields {
+ value := getAccountFieldValue(driver.Data, field)
+ // Convert gender code to text
+ if field == "gender" && value != "" {
+ value = gender.ISO5218ToString(value)
+ }
+ row = append(row, value)
+ }
+
+ // Address departure
+ addressDeparture := ""
+ if addr, ok := driver.Data["address"]; ok {
+ if addrMap, ok := addr.(map[string]interface{}); ok {
+ if props, ok := addrMap["properties"]; ok {
+ if propsMap, ok := props.(map[string]interface{}); ok {
+ if label, ok := propsMap["label"].(string); ok {
+ addressDeparture = label
+ }
+ }
+ }
+ }
+ }
+ row = append(row, addressDeparture)
+
+ // Address destination
+ addressDestination := ""
+ if addr, ok := driver.Data["address_destination"]; ok {
+ if addrMap, ok := addr.(map[string]interface{}); ok {
+ if props, ok := addrMap["properties"]; ok {
+ if propsMap, ok := props.(map[string]interface{}); ok {
+ if label, ok := propsMap["label"].(string); ok {
+ addressDestination = label
+ }
+ }
+ }
+ }
+ }
+ row = append(row, addressDestination)
+
+ // Archived status
+ archived := "Non"
+ if archivedVal, ok := driver.Data["archived"].(bool); ok && archivedVal {
+ archived = "Oui"
+ }
+ row = append(row, archived)
+
+ spreadsheet.AddRow(row)
+ }
+
+ // Write Excel to response
+ w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ w.Header().Set("Content-Disposition", "attachment; filename=\"export-covoitureurs-solidaires.xlsx\"")
+
+ if err := spreadsheet.GetFile().Write(w); err != nil {
+ log.Error().Err(err).Msg("Error generating Excel file")
+ http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/renderer/xlsx/solidarity-transport.go b/renderer/xlsx/solidarity-transport.go
new file mode 100644
index 0000000..37c72ff
--- /dev/null
+++ b/renderer/xlsx/solidarity-transport.go
@@ -0,0 +1,332 @@
+package xlsx
+
+import (
+ "fmt"
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/gender"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+)
+
+func (r *XLSXRenderer) SolidarityTransportBookings(w http.ResponseWriter, result *application.SolidarityTransportBookingsResult) {
+ // Create Excel spreadsheet
+ spreadsheet := r.NewSpreadsheet("Transport solidaire")
+
+ // Build headers dynamically based on configuration
+ headers := []string{
+ "ID Réservation",
+ "ID Groupe",
+ "Statut",
+ "Motif de réservation",
+ "Raison d'annulation",
+ "Remplacé par (ID)",
+ "Date de prise en charge",
+ "Heure de prise en charge",
+ }
+
+ // Add driver fields from config
+ driverOptionalFields := r.Config.Get("modules.solidarity_transport.profile_optional_fields")
+ driverFields := []string{"last_name", "first_name", "email", "phone_number"}
+ driverHeaders := []string{"Conducteur - Nom", "Conducteur - Prénom", "Conducteur - Email", "Conducteur - Téléphone"}
+
+ if driverOptionalFieldsList, ok := driverOptionalFields.([]interface{}); ok {
+ for _, field := range driverOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ driverFields = append(driverFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ driverHeaders = append(driverHeaders, fmt.Sprintf("Conducteur - %s", label))
+ }
+ }
+ }
+ }
+ headers = append(headers, driverHeaders...)
+
+ // Add beneficiary fields from config
+ beneficiaryOptionalFields := r.Config.Get("modules.beneficiaries.profile_optional_fields")
+ beneficiaryFields := []string{"last_name", "first_name", "email", "phone_number"}
+ beneficiaryHeaders := []string{"Bénéficiaire - Nom", "Bénéficiaire - Prénom", "Bénéficiaire - Email", "Bénéficiaire - Téléphone"}
+
+ if beneficiaryOptionalFieldsList, ok := beneficiaryOptionalFields.([]interface{}); ok {
+ for _, field := range beneficiaryOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ beneficiaryFields = append(beneficiaryFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ beneficiaryHeaders = append(beneficiaryHeaders, fmt.Sprintf("Bénéficiaire - %s", label))
+ }
+ }
+ }
+ }
+ headers = append(headers, beneficiaryHeaders...)
+
+ // Add journey information headers
+ headers = append(headers,
+ "Lieu de départ - Adresse",
+ "Lieu de départ - Latitude",
+ "Lieu de départ - Longitude",
+ "Destination - Adresse",
+ "Destination - Latitude",
+ "Destination - Longitude",
+ "Distance passager (km)",
+ "Distance conducteur totale (km)",
+ "Durée trajet (minutes)",
+ "Prix passager",
+ "Devise prix passager",
+ "Compensation conducteur",
+ "Devise compensation",
+ "Temps d'attente retour",
+ "Aller simple",
+ "Départ conducteur - Adresse",
+ "Départ conducteur - Latitude",
+ "Départ conducteur - Longitude",
+ "Arrivée conducteur - Adresse",
+ "Arrivée conducteur - Latitude",
+ "Arrivée conducteur - Longitude",
+ )
+
+ spreadsheet.SetHeaders(headers)
+
+ // Add data rows
+ for _, booking := range result.Bookings {
+ driver := result.DriversMap[booking.DriverId]
+ beneficiary := result.BeneficiariesMap[booking.PassengerId]
+
+ row := []interface{}{}
+
+ // Booking information
+ row = append(row, booking.Id)
+ row = append(row, booking.GroupId)
+ row = append(row, booking.Status)
+
+ // Motivation (from booking.Data)
+ motivation := ""
+ if booking.Data != nil {
+ if motivationVal, ok := booking.Data["motivation"]; ok && motivationVal != nil {
+ motivation = fmt.Sprint(motivationVal)
+ }
+ }
+ row = append(row, motivation)
+
+ // Cancellation reason (from booking.Data)
+ cancellationReason := ""
+ if booking.Data != nil {
+ if reasonVal, ok := booking.Data["reason"]; ok && reasonVal != nil {
+ cancellationReason = fmt.Sprint(reasonVal)
+ }
+ }
+ row = append(row, cancellationReason)
+
+ // Replaced by (from booking.Data)
+ replacedBy := ""
+ if booking.Data != nil {
+ if replacedByVal, ok := booking.Data["replaced_by"]; ok && replacedByVal != nil {
+ replacedBy = fmt.Sprint(replacedByVal)
+ }
+ }
+ row = append(row, replacedBy)
+
+ // Journey date and time
+ if booking.Journey != nil {
+ row = append(row, booking.Journey.PassengerPickupDate.Format("2006-01-02"))
+ row = append(row, booking.Journey.PassengerPickupDate.Format("15:04"))
+ } else {
+ row = append(row, "", "")
+ }
+
+ // Driver data
+ for _, field := range driverFields {
+ row = append(row, getAccountFieldValue(driver.Data, field))
+ }
+
+ // Beneficiary data
+ for _, field := range beneficiaryFields {
+ row = append(row, getAccountFieldValue(beneficiary.Data, field))
+ }
+
+ // Journey locations and details
+ if booking.Journey != nil {
+ // Passenger pickup
+ pickupAddr, pickupLat, pickupLon := getLocationData(booking.Journey.PassengerPickup)
+ row = append(row, pickupAddr, pickupLat, pickupLon)
+
+ // Passenger drop
+ dropAddr, dropLat, dropLon := getLocationData(booking.Journey.PassengerDrop)
+ row = append(row, dropAddr, dropLat, dropLon)
+
+ // Distances and duration
+ row = append(row, booking.Journey.PassengerDistance)
+ row = append(row, booking.Journey.DriverDistance)
+ row = append(row, int64(booking.Journey.Duration.Minutes()))
+
+ // Pricing
+ row = append(row, fmt.Sprintf("%.2f", booking.Journey.Price.Amount))
+ row = append(row, booking.Journey.Price.Currency)
+
+ // Driver compensation
+ if booking.DriverCompensationAmount > 0 {
+ row = append(row, fmt.Sprintf("%.2f", booking.DriverCompensationAmount))
+ row = append(row, booking.DriverCompensationCurrency)
+ } else {
+ row = append(row, "", "")
+ }
+
+ // Return wait time
+ row = append(row, "")
+
+ // One way trip (Noreturn field)
+ if booking.Journey.Noreturn {
+ row = append(row, "Oui")
+ } else {
+ row = append(row, "Non")
+ }
+
+ // Driver departure
+ driverDepartAddr, driverDepartLat, driverDepartLon := getLocationData(booking.Journey.DriverDeparture)
+ row = append(row, driverDepartAddr, driverDepartLat, driverDepartLon)
+
+ // Driver arrival
+ driverArrivalAddr, driverArrivalLat, driverArrivalLon := getLocationData(booking.Journey.DriverArrival)
+ row = append(row, driverArrivalAddr, driverArrivalLat, driverArrivalLon)
+ } else {
+ // No journey data - fill with empty values
+ for i := 0; i < 21; i++ {
+ row = append(row, "")
+ }
+ }
+
+ spreadsheet.AddRow(row)
+ }
+
+ // Write Excel to response
+ w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ w.Header().Set("Content-Disposition", "attachment; filename=\"export-transport-solidaire.xlsx\"")
+
+ if err := spreadsheet.GetFile().Write(w); err != nil {
+ log.Error().Err(err).Msg("Error generating Excel file")
+ http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
+ return
+ }
+}
+
+func getAccountFieldValue(data map[string]interface{}, field string) string {
+ // First check direct field
+ if val, ok := data[field]; ok && val != nil {
+ return fmt.Sprint(val)
+ }
+
+ // Check in other_properties
+ if otherProps, ok := data["other_properties"]; ok {
+ if otherPropsMap, ok := otherProps.(map[string]interface{}); ok {
+ if val, ok := otherPropsMap[field]; ok && val != nil {
+ return fmt.Sprint(val)
+ }
+ }
+ }
+
+ return ""
+}
+
+func getLocationData(feature *geojson.Feature) (address string, lat interface{}, lon interface{}) {
+ if feature != nil && feature.Properties != nil {
+ if label, ok := feature.Properties["label"].(string); ok {
+ address = label
+ }
+ if feature.Geometry != nil {
+ coords := feature.Geometry.Bound().Center()
+ lat = fmt.Sprintf("%.6f", coords.Lat())
+ lon = fmt.Sprintf("%.6f", coords.Lon())
+ }
+ }
+ return
+}
+
+func (r *XLSXRenderer) SolidarityTransportDrivers(w http.ResponseWriter, result *application.SolidarityTransportOverviewResult) {
+ // Create Excel spreadsheet
+ spreadsheet := r.NewSpreadsheet("Conducteurs solidaires")
+
+ // Build headers dynamically based on configuration
+ driverOptionalFields := r.Config.Get("modules.solidarity_transport.drivers.profile_optional_fields")
+ driverFields := []string{"last_name", "first_name", "email", "phone_number", "birthdate", "gender", "file_number"}
+ headers := []string{"ID", "Nom", "Prénom", "Email", "Téléphone", "Date de naissance", "Genre", "Numéro de dossier"}
+
+ if driverOptionalFieldsList, ok := driverOptionalFields.([]interface{}); ok {
+ for _, field := range driverOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ driverFields = append(driverFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ headers = append(headers, label)
+ }
+ }
+ }
+ }
+
+ // Add address and archived columns
+ headers = append(headers, "Adresse", "Archivé")
+
+ spreadsheet.SetHeaders(headers)
+
+ // Add data rows
+ for _, driver := range result.Accounts {
+ row := []interface{}{}
+
+ // Driver ID
+ row = append(row, driver.ID)
+
+ // Driver data
+ for _, field := range driverFields {
+ value := getAccountFieldValue(driver.Data, field)
+ // Convert gender code to text
+ if field == "gender" && value != "" {
+ value = gender.ISO5218ToString(value)
+ }
+ row = append(row, value)
+ }
+
+ // Address
+ address := ""
+ if addr, ok := driver.Data["address"]; ok {
+ if addrMap, ok := addr.(map[string]interface{}); ok {
+ if props, ok := addrMap["properties"]; ok {
+ if propsMap, ok := props.(map[string]interface{}); ok {
+ if label, ok := propsMap["label"].(string); ok {
+ address = label
+ }
+ }
+ }
+ }
+ }
+ row = append(row, address)
+
+ // Archived status
+ archived := "Non"
+ if archivedVal, ok := driver.Data["archived"].(bool); ok && archivedVal {
+ archived = "Oui"
+ }
+ row = append(row, archived)
+
+ spreadsheet.AddRow(row)
+ }
+
+ // Write Excel to response
+ w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ w.Header().Set("Content-Disposition", "attachment; filename=\"export-conducteurs-solidaires.xlsx\"")
+
+ if err := spreadsheet.GetFile().Write(w); err != nil {
+ log.Error().Err(err).Msg("Error generating Excel file")
+ http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/renderer/xlsx/vehicle-bookings.go b/renderer/xlsx/vehicle-bookings.go
new file mode 100644
index 0000000..8263d84
--- /dev/null
+++ b/renderer/xlsx/vehicle-bookings.go
@@ -0,0 +1,350 @@
+package xlsx
+
+import (
+ "fmt"
+ "net/http"
+
+ fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
+ groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "github.com/rs/zerolog/log"
+)
+
+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")
+
+ // Build headers
+ headers := []string{
+ "ID Réservation",
+ "Statut",
+ "Type de véhicule",
+ "Nom du véhicule",
+ "Immatriculation",
+ }
+
+ // Add beneficiary fields from config
+ beneficiaryOptionalFields := r.Config.Get("modules.beneficiaries.profile_optional_fields")
+ beneficiaryFields := []string{"last_name", "first_name", "email", "phone_number"}
+ beneficiaryHeaders := []string{"Bénéficiaire - Nom", "Bénéficiaire - Prénom", "Bénéficiaire - Email", "Bénéficiaire - Téléphone"}
+
+ if beneficiaryOptionalFieldsList, ok := beneficiaryOptionalFields.([]interface{}); ok {
+ for _, field := range beneficiaryOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ beneficiaryFields = append(beneficiaryFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ beneficiaryHeaders = append(beneficiaryHeaders, fmt.Sprintf("Bénéficiaire - %s", label))
+ }
+ }
+ }
+ }
+ headers = append(headers, beneficiaryHeaders...)
+
+ // Add booking date headers
+ headers = append(headers,
+ "Date de début",
+ "Date de fin",
+ "Durée (jours)",
+ "Commentaire",
+ "Raison d'annulation",
+ )
+
+ spreadsheet.SetHeaders(headers)
+
+ // Add data rows
+ for _, booking := range bookings {
+ vehicle := vehiclesMap[booking.Vehicleid]
+ beneficiary := driversMap[booking.Driver]
+
+ row := []interface{}{}
+
+ // Booking information
+ row = append(row, booking.ID)
+
+ // Status
+ status := ""
+ if booking.Deleted {
+ status = "Annulé"
+ } else {
+ switch booking.Status() {
+ case 1:
+ status = "A venir"
+ case 0:
+ status = "En cours"
+ case -1:
+ status = "Terminé"
+ }
+ }
+ row = append(row, status)
+
+ // Vehicle information
+ row = append(row, vehicle.Type)
+ if vehicleName, ok := vehicle.Data["name"].(string); ok {
+ row = append(row, vehicleName)
+ } else {
+ row = append(row, "")
+ }
+ if licencePlate, ok := vehicle.Data["licence_plate"].(string); ok {
+ row = append(row, licencePlate)
+ } else {
+ row = append(row, "")
+ }
+
+ // Beneficiary data (including other_properties)
+ for _, field := range beneficiaryFields {
+ value := ""
+ // First check direct field
+ if val, ok := beneficiary.Data[field]; ok && val != nil {
+ value = fmt.Sprint(val)
+ } else {
+ // Check in other_properties
+ if otherProps, ok := beneficiary.Data["other_properties"]; ok {
+ if otherPropsMap, ok := otherProps.(map[string]interface{}); ok {
+ if val, ok := otherPropsMap[field]; ok && val != nil {
+ value = fmt.Sprint(val)
+ }
+ }
+ }
+ }
+ row = append(row, value)
+ }
+
+ // Booking dates
+ row = append(row, booking.Startdate.Format("2006-01-02"))
+ row = append(row, booking.Enddate.Format("2006-01-02"))
+
+ // Duration in days
+ duration := booking.Enddate.Sub(booking.Startdate).Hours() / 24
+ row = append(row, fmt.Sprintf("%.0f", duration))
+
+ // Comment
+ comment := ""
+ if booking.Data != nil {
+ if commentVal, ok := booking.Data["comment"]; ok && commentVal != nil {
+ comment = fmt.Sprint(commentVal)
+ }
+ }
+ row = append(row, comment)
+
+ // Cancellation reason
+ cancellationReason := ""
+ if booking.Deleted && booking.Data != nil {
+ if reasonVal, ok := booking.Data["reason"]; ok && reasonVal != nil {
+ cancellationReason = fmt.Sprint(reasonVal)
+ }
+ }
+ row = append(row, cancellationReason)
+
+ spreadsheet.AddRow(row)
+ }
+
+ // Write Excel to response
+ w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ w.Header().Set("Content-Disposition", "attachment; filename=\"export-reservations-vehicules.xlsx\"")
+
+ if err := spreadsheet.GetFile().Write(w); err != nil {
+ log.Error().Err(err).Msg("Error generating Excel file")
+ http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
+ return
+ }
+}
+
+func (r *XLSXRenderer) VehicleBookingsAdmin(w http.ResponseWriter, bookings []fleetsstorage.Booking, vehiclesMap map[string]interface{}, driversMap map[string]interface{}, groupsMap map[string]any) {
+ // Create Excel spreadsheet
+ spreadsheet := r.NewSpreadsheet("Réservations véhicules")
+
+ // Build headers
+ headers := []string{
+ "ID Réservation",
+ "Statut",
+ "Date de début",
+ "Date de fin",
+ "Durée (jours)",
+ "Commentaire",
+ "Raison d'annulation",
+ "Type de véhicule",
+ "Nom du véhicule",
+ "Immatriculation",
+ "Gestionnaire véhicule",
+ }
+
+ // Add vehicle optional fields from config
+ vehicleOptionalFields := r.Config.Get("modules.fleets.vehicle_optional_fields")
+ vehicleFields := []string{}
+ vehicleHeaders := []string{}
+
+ if vehicleOptionalFieldsList, ok := vehicleOptionalFields.([]interface{}); ok {
+ for _, field := range vehicleOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ vehicleFields = append(vehicleFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ vehicleHeaders = append(vehicleHeaders, fmt.Sprintf("Véhicule - %s", label))
+ }
+ }
+ }
+ }
+ headers = append(headers, vehicleHeaders...)
+
+ // Add beneficiary fields from config
+ beneficiaryOptionalFields := r.Config.Get("modules.beneficiaries.profile_optional_fields")
+ beneficiaryFields := []string{"last_name", "first_name", "email", "phone_number"}
+ beneficiaryHeaders := []string{"Bénéficiaire - Nom", "Bénéficiaire - Prénom", "Bénéficiaire - Email", "Bénéficiaire - Téléphone"}
+
+ if beneficiaryOptionalFieldsList, ok := beneficiaryOptionalFields.([]interface{}); ok {
+ for _, field := range beneficiaryOptionalFieldsList {
+ if fieldMap, ok := field.(map[string]interface{}); ok {
+ if name, ok := fieldMap["name"].(string); ok {
+ beneficiaryFields = append(beneficiaryFields, name)
+ label := name
+ if labelVal, ok := fieldMap["label"].(string); ok {
+ label = labelVal
+ }
+ beneficiaryHeaders = append(beneficiaryHeaders, fmt.Sprintf("Bénéficiaire - %s", label))
+ }
+ }
+ }
+ }
+ headers = append(headers, beneficiaryHeaders...)
+
+ spreadsheet.SetHeaders(headers)
+
+ // Add data rows
+ for _, booking := range bookings {
+ // Get vehicle from map
+ var vehicle fleetsstorage.Vehicle
+ if v, ok := vehiclesMap[booking.Vehicleid]; ok {
+ if vTyped, ok := v.(fleetsstorage.Vehicle); ok {
+ vehicle = vTyped
+ }
+ }
+
+ // Get beneficiary from map
+ var beneficiary mobilityaccountsstorage.Account
+ if d, ok := driversMap[booking.Driver]; ok {
+ if dTyped, ok := d.(mobilityaccountsstorage.Account); ok {
+ beneficiary = dTyped
+ }
+ }
+
+ row := []interface{}{}
+
+ // Booking information
+ row = append(row, booking.ID)
+
+ // Status
+ status := ""
+ if booking.Deleted {
+ status = "Annulé"
+ } else {
+ switch booking.Status() {
+ case 1:
+ status = "A venir"
+ case 0:
+ status = "En cours"
+ case -1:
+ status = "Terminé"
+ }
+ }
+ row = append(row, status)
+
+ // Booking dates
+ row = append(row, booking.Startdate.Format("2006-01-02"))
+ row = append(row, booking.Enddate.Format("2006-01-02"))
+
+ // Duration in days
+ duration := booking.Enddate.Sub(booking.Startdate).Hours() / 24
+ row = append(row, fmt.Sprintf("%.0f", duration))
+
+ // Comment
+ comment := ""
+ if booking.Data != nil {
+ if commentVal, ok := booking.Data["comment"]; ok && commentVal != nil {
+ comment = fmt.Sprint(commentVal)
+ }
+ }
+ row = append(row, comment)
+
+ // Cancellation reason
+ cancellationReason := ""
+ if booking.Deleted && booking.Data != nil {
+ if reasonVal, ok := booking.Data["reason"]; ok && reasonVal != nil {
+ cancellationReason = fmt.Sprint(reasonVal)
+ }
+ }
+ row = append(row, cancellationReason)
+
+ // Vehicle information
+ row = append(row, vehicle.Type)
+ if vehicleName, ok := vehicle.Data["name"].(string); ok {
+ row = append(row, vehicleName)
+ } else {
+ row = append(row, "")
+ }
+ if licencePlate, ok := vehicle.Data["licence_plate"].(string); ok {
+ row = append(row, licencePlate)
+ } else {
+ row = append(row, "")
+ }
+
+ // Vehicle administrator (group name)
+ administratorName := ""
+ if len(vehicle.Administrators) > 0 {
+ if group, ok := groupsMap[vehicle.Administrators[0]]; ok {
+ if groupTyped, ok := group.(groupsstorage.Group); ok {
+ if name, ok := groupTyped.Data["name"]; ok {
+ administratorName = fmt.Sprint(name)
+ }
+ }
+ }
+ }
+ row = append(row, administratorName)
+
+ // Vehicle optional fields
+ for _, field := range vehicleFields {
+ value := ""
+ if val, ok := vehicle.Data[field]; ok && val != nil {
+ value = fmt.Sprint(val)
+ }
+ row = append(row, value)
+ }
+
+ // Beneficiary data (including other_properties)
+ for _, field := range beneficiaryFields {
+ value := ""
+ // First check direct field
+ if val, ok := beneficiary.Data[field]; ok && val != nil {
+ value = fmt.Sprint(val)
+ } else {
+ // Check in other_properties
+ if otherProps, ok := beneficiary.Data["other_properties"]; ok {
+ if otherPropsMap, ok := otherProps.(map[string]interface{}); ok {
+ if val, ok := otherPropsMap[field]; ok && val != nil {
+ value = fmt.Sprint(val)
+ }
+ }
+ }
+ }
+ row = append(row, value)
+ }
+
+ spreadsheet.AddRow(row)
+ }
+
+ // Write Excel to response
+ w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ w.Header().Set("Content-Disposition", "attachment; filename=\"all_bookings.xlsx\"")
+
+ if err := spreadsheet.GetFile().Write(w); err != nil {
+ log.Error().Err(err).Msg("Error generating Excel file")
+ http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/renderer/xlsx/xlsx.go b/renderer/xlsx/xlsx.go
new file mode 100644
index 0000000..2bee13d
--- /dev/null
+++ b/renderer/xlsx/xlsx.go
@@ -0,0 +1,60 @@
+package xlsx
+
+import (
+ "github.com/spf13/viper"
+ "github.com/xuri/excelize/v2"
+)
+
+// XLSXRenderer handles rendering data to Excel XLSX format
+type XLSXRenderer struct {
+ Config *viper.Viper
+}
+
+func NewXLSXRenderer(config *viper.Viper) *XLSXRenderer {
+ return &XLSXRenderer{
+ Config: config,
+ }
+}
+
+// Spreadsheet represents an Excel spreadsheet
+type Spreadsheet struct {
+ file *excelize.File
+ sheetName string
+ rowIndex int
+}
+
+// NewSpreadsheet creates a new Excel spreadsheet
+func (r *XLSXRenderer) NewSpreadsheet(sheetName string) *Spreadsheet {
+ f := excelize.NewFile()
+ // Rename default sheet
+ f.SetSheetName("Sheet1", sheetName)
+
+ return &Spreadsheet{
+ file: f,
+ sheetName: sheetName,
+ rowIndex: 1,
+ }
+}
+
+// SetHeaders sets the header row
+func (s *Spreadsheet) SetHeaders(headers []string) {
+ for i, header := range headers {
+ cell, _ := excelize.CoordinatesToCellName(i+1, s.rowIndex)
+ s.file.SetCellValue(s.sheetName, cell, header)
+ }
+ s.rowIndex++
+}
+
+// AddRow adds a data row
+func (s *Spreadsheet) AddRow(values []interface{}) {
+ for i, value := range values {
+ cell, _ := excelize.CoordinatesToCellName(i+1, s.rowIndex)
+ s.file.SetCellValue(s.sheetName, cell, value)
+ }
+ s.rowIndex++
+}
+
+// GetFile returns the underlying excelize File
+func (s *Spreadsheet) GetFile() *excelize.File {
+ return s.file
+}
diff --git a/servers/mcp/README.md b/servers/mcp/README.md
new file mode 100644
index 0000000..9c2b484
--- /dev/null
+++ b/servers/mcp/README.md
@@ -0,0 +1,221 @@
+# MCP Server for PARCOURSMOB
+
+This package implements a Model Context Protocol (MCP) HTTP server for the PARCOURSMOB application, exposing journey search functionality as an MCP tool.
+
+## Overview
+
+The MCP server provides a standardized interface for AI assistants to search for multimodal journeys, including:
+- Public transit routes
+- Carpooling solutions (via operators like Mobicoop)
+- Solidarity transport
+- Organized carpools
+- Fleet vehicles
+- Local knowledge base solutions
+
+## Configuration
+
+Enable the MCP server in your config file:
+
+```yaml
+server:
+ mcp:
+ enabled: true
+ listen: "0.0.0.0:8081"
+```
+
+Or via environment variables:
+```bash
+export SERVER_MCP_ENABLED=true
+export SERVER_MCP_LISTEN="0.0.0.0:8081"
+```
+
+## API Endpoints
+
+### Initialize
+```http
+POST /mcp/v1/initialize
+```
+
+Returns server capabilities and protocol version.
+
+### List Tools
+```http
+GET /mcp/v1/tools/list
+```
+
+Returns available MCP tools.
+
+### Call Tool
+```http
+POST /mcp/v1/tools/call
+Content-Type: application/json
+
+{
+ "name": "search_journeys",
+ "arguments": {
+ "departure": "123 Main St, Paris, France",
+ "destination": "456 Oak Ave, Lyon, France",
+ "departure_date": "2025-01-20",
+ "departure_time": "14:30"
+ }
+}
+```
+
+### Health Check
+```http
+GET /health
+```
+
+## Available Tools
+
+### search_journeys
+
+Searches for multimodal journey options between two locations.
+
+**Parameters:**
+- `departure` (string, required): Departure address as text
+- `destination` (string, required): Destination address as text
+- `departure_date` (string, required): Date in YYYY-MM-DD format
+- `departure_time` (string, required): Time in HH:MM format (24-hour)
+- `passenger_id` (string, optional): Passenger ID to retrieve address from account
+- `exclude_driver_ids` (array, optional): List of driver IDs to exclude from solidarity transport results
+
+**Example Request:**
+```bash
+curl -X POST http://localhost:8081/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "search_journeys",
+ "arguments": {
+ "departure": "Gare de Lyon, Paris",
+ "destination": "Part-Dieu, Lyon",
+ "departure_date": "2025-01-20",
+ "departure_time": "09:00"
+ }
+ }'
+```
+
+**Response Format:**
+```json
+{
+ "search_parameters": {
+ "departure": {
+ "label": "Gare de Lyon, Paris, France",
+ "coordinates": { "type": "Point", "coordinates": [2.3736, 48.8443] }
+ },
+ "destination": {
+ "label": "Part-Dieu, Lyon, France",
+ "coordinates": { "type": "Point", "coordinates": [4.8575, 45.7605] }
+ },
+ "departure_date": "2025-01-20",
+ "departure_time": "09:00"
+ },
+ "results": {
+ "CarpoolResults": [...],
+ "TransitResults": [...],
+ "VehicleResults": [...],
+ "DriverJourneys": [...],
+ "OrganizedCarpools": [...],
+ "KnowledgeBaseResults": [...]
+ }
+}
+```
+
+## Architecture
+
+### Package Structure
+
+- `mcp.go`: HTTP server and request routing
+- `tools.go`: Tool registration and execution
+- `journey_search.go`: Journey search tool implementation
+
+### Flow
+
+1. HTTP request received at MCP endpoint
+2. Tool name and arguments extracted
+3. Addresses geocoded using Pelias geocoding service
+4. Journey search executed via ApplicationHandler
+5. Results formatted and returned as JSON
+
+### Dependencies
+
+The MCP server uses:
+- Pelias geocoding service (configured via `geo.pelias.url`)
+- ApplicationHandler for journey search business logic
+- All backend services (GRPC): solidarity transport, carpool service, transit routing, fleets, etc.
+
+## Integration with AI Assistants
+
+The MCP server follows the Model Context Protocol specification, making it compatible with AI assistants that support MCP, such as Claude Desktop or other MCP-enabled tools.
+
+Example Claude Desktop configuration:
+```json
+{
+ "mcpServers": {
+ "parcoursmob": {
+ "url": "http://localhost:8081/mcp/v1"
+ }
+ }
+}
+```
+
+## Development
+
+### Testing
+
+Test the health endpoint:
+```bash
+curl http://localhost:8081/health
+```
+
+List available tools:
+```bash
+curl http://localhost:8081/mcp/v1/tools/list
+```
+
+Test journey search:
+```bash
+curl -X POST http://localhost:8081/mcp/v1/tools/call \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "search_journeys",
+ "arguments": {
+ "departure": "Paris",
+ "destination": "Lyon",
+ "departure_date": "2025-01-20",
+ "departure_time": "10:00"
+ }
+ }'
+```
+
+### Adding New Tools
+
+To add a new MCP tool:
+
+1. Define the tool in `tools.go`:
+```go
+func (h *ToolsHandler) registerNewTool() {
+ tool := &Tool{
+ Name: "tool_name",
+ Description: "Tool description",
+ InputSchema: map[string]any{...},
+ }
+ h.tools["tool_name"] = tool
+}
+```
+
+2. Implement the handler:
+```go
+func (h *ToolsHandler) handleNewTool(ctx context.Context, arguments map[string]any) (any, error) {
+ // Implementation
+}
+```
+
+3. Add to CallTool switch statement in `tools.go`
+
+## Notes
+
+- All times are handled in Europe/Paris timezone
+- Geocoding uses the first result from Pelias
+- Journey search runs multiple transport mode queries in parallel
+- Results include all available transport options for the requested route
diff --git a/servers/mcp/journey_tool.go b/servers/mcp/journey_tool.go
new file mode 100644
index 0000000..77f60c4
--- /dev/null
+++ b/servers/mcp/journey_tool.go
@@ -0,0 +1,158 @@
+package mcp
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+)
+
+// JourneySearchInput represents the input for journey search
+type JourneySearchInput struct {
+ Departure string `json:"departure" jsonschema:"Departure address as text (e.g. Paris or Gare de Lyon Paris)"`
+ Destination string `json:"destination" jsonschema:"Destination address as text (e.g. Lyon or Part-Dieu Lyon)"`
+ DepartureDate string `json:"departure_date" jsonschema:"Departure date in YYYY-MM-DD format"`
+ DepartureTime string `json:"departure_time" jsonschema:"Departure time in HH:MM format (24-hour)"`
+ PassengerID string `json:"passenger_id,omitempty" jsonschema:"Optional passenger ID to retrieve address from account"`
+ ExcludeDriverIDs []string `json:"exclude_driver_ids,omitempty" jsonschema:"Optional list of driver IDs to exclude from solidarity transport results"`
+}
+
+// JourneySearchOutput represents the output of journey search
+type JourneySearchOutput struct {
+ SearchParameters map[string]any `json:"search_parameters"`
+ Results any `json:"results"`
+}
+
+// registerJourneySearchTool registers the journey search tool with the MCP server
+func (s *MCPServer) registerJourneySearchTool() {
+ mcpsdk.AddTool(
+ s.mcpServer,
+ &mcpsdk.Tool{
+ Name: "search_journeys",
+ Description: "Search for multimodal journeys including transit, carpooling, solidarity transport, organized carpool, and local solutions. Accepts departure and destination as text addresses that will be geocoded automatically.",
+ },
+ s.handleJourneySearch,
+ )
+}
+
+// handleJourneySearch handles the journey search tool execution
+func (s *MCPServer) handleJourneySearch(ctx context.Context, req *mcpsdk.CallToolRequest, input JourneySearchInput) (*mcpsdk.CallToolResult, *JourneySearchOutput, error) {
+ // Geocode departure address using French government API
+ log.Info().Str("address", input.Departure).Msg("Geocoding departure address")
+ departureFeature, err := s.geocodeAddress(input.Departure)
+ if err != nil {
+ log.Error().Err(err).Msg("failed to geocode departure address")
+ return nil, nil, fmt.Errorf("failed to geocode departure address: %w", err)
+ }
+
+ // Geocode destination address using French government API
+ log.Info().Str("address", input.Destination).Msg("Geocoding destination address")
+ destinationFeature, err := s.geocodeAddress(input.Destination)
+ if err != nil {
+ log.Error().Err(err).Msg("failed to geocode destination address")
+ return nil, nil, fmt.Errorf("failed to geocode destination address: %w", err)
+ }
+
+ // Parse date and time
+ parisLoc, err := time.LoadLocation("Europe/Paris")
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to load Paris timezone: %w", err)
+ }
+
+ departureDateTime, err := time.ParseInLocation("2006-01-02 15:04", fmt.Sprintf("%s %s", input.DepartureDate, input.DepartureTime), parisLoc)
+ if err != nil {
+ log.Error().Err(err).Msg("failed to parse date/time")
+ return nil, nil, fmt.Errorf("failed to parse departure date/time: %w", err)
+ }
+
+ // Convert to UTC for the search
+ departureDateTime = departureDateTime.UTC()
+
+ log.Info().
+ Str("departure", input.Departure).
+ Str("destination", input.Destination).
+ Time("departure_datetime", departureDateTime).
+ Msg("Executing journey search")
+
+ // Prepare exclude driver ID (only first one if provided)
+ excludeDriverID := ""
+ if len(input.ExcludeDriverIDs) > 0 {
+ excludeDriverID = input.ExcludeDriverIDs[0]
+ }
+
+ // Prepare search options - disable transit for MCP requests
+ searchOptions := &application.SearchJourneyOptions{
+ DisableTransit: true,
+ }
+
+ // Call the journey search from application handler
+ searchResult, err := s.applicationHandler.SearchJourneys(
+ ctx,
+ departureDateTime,
+ departureFeature,
+ destinationFeature,
+ input.PassengerID,
+ excludeDriverID,
+ "", // solidarityExcludeGroupId - not used in MCP context
+ searchOptions,
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("journey search failed: %w", err)
+ }
+
+ // Format the results for MCP response
+ response := &JourneySearchOutput{
+ SearchParameters: map[string]any{
+ "departure": map[string]any{
+ "label": getFeatureLabel(departureFeature),
+ "coordinates": departureFeature.Geometry,
+ },
+ "destination": map[string]any{
+ "label": getFeatureLabel(destinationFeature),
+ "coordinates": destinationFeature.Geometry,
+ },
+ "departure_date": input.DepartureDate,
+ "departure_time": input.DepartureTime,
+ },
+ Results: searchResult,
+ }
+
+ return &mcpsdk.CallToolResult{}, response, nil
+}
+
+// geocodeAddress uses the geo service helper to geocode an address
+func (s *MCPServer) geocodeAddress(address string) (*geojson.Feature, error) {
+ // Use the geo service to get autocomplete results
+ featureCollection, err := s.geoService.Autocomplete(address)
+ if err != nil {
+ return nil, fmt.Errorf("geocoding request failed: %w", err)
+ }
+
+ if len(featureCollection.Features) == 0 {
+ return nil, fmt.Errorf("no results found for address: %s", address)
+ }
+
+ // Return the first feature directly
+ return featureCollection.Features[0], nil
+}
+
+// getFeatureLabel extracts a human-readable label from a GeoJSON Feature
+func getFeatureLabel(feature *geojson.Feature) string {
+ if feature.Properties == nil {
+ return ""
+ }
+
+ // Try common label fields
+ if label, ok := feature.Properties["label"].(string); ok {
+ return label
+ }
+ if name, ok := feature.Properties["name"].(string); ok {
+ return name
+ }
+
+ return ""
+}
diff --git a/servers/mcp/mcp.go b/servers/mcp/mcp.go
new file mode 100644
index 0000000..12f89b4
--- /dev/null
+++ b/servers/mcp/mcp.go
@@ -0,0 +1,109 @@
+package mcp
+
+import (
+ "net/http"
+ "time"
+
+ mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/viper"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/geo"
+ cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "git.coopgo.io/coopgo-apps/parcoursmob/services"
+)
+
+// MCPServer represents the MCP HTTP server
+type MCPServer struct {
+ cfg *viper.Viper
+ services *services.ServicesHandler
+ kv cache.KVHandler
+ filestorage cache.FileStorage
+ applicationHandler *application.ApplicationHandler
+ mcpServer *mcpsdk.Server
+ geoService *geo.GeoService
+}
+
+// NewMCPServer creates a new MCP server instance
+func NewMCPServer(
+ cfg *viper.Viper,
+ svc *services.ServicesHandler,
+ applicationHandler *application.ApplicationHandler,
+ kv cache.KVHandler,
+ filestorage cache.FileStorage,
+) *MCPServer {
+ // Initialize geocoding service
+ geoType := cfg.GetString("geo.type")
+ baseURL := cfg.GetString("geo." + geoType + ".url")
+ autocompleteEndpoint := cfg.GetString("geo." + geoType + ".autocomplete")
+ geoService := geo.NewGeoService(geoType, baseURL, autocompleteEndpoint)
+
+ server := &MCPServer{
+ cfg: cfg,
+ services: svc,
+ kv: kv,
+ filestorage: filestorage,
+ applicationHandler: applicationHandler,
+ geoService: geoService,
+ }
+
+ // Create MCP server with implementation info
+ server.mcpServer = mcpsdk.NewServer(&mcpsdk.Implementation{
+ Name: "parcoursmob-mcp-server",
+ Version: "1.0.0",
+ }, nil)
+
+ // Register journey search tool
+ server.registerJourneySearchTool()
+
+ return server
+}
+
+// Run starts the MCP HTTP server with SSE transport
+func Run(
+ cfg *viper.Viper,
+ svc *services.ServicesHandler,
+ applicationHandler *application.ApplicationHandler,
+ kv cache.KVHandler,
+ filestorage cache.FileStorage,
+) {
+ address := cfg.GetString("server.mcp.listen")
+ service_name := cfg.GetString("service_name")
+
+ mcpServer := NewMCPServer(cfg, svc, applicationHandler, kv, filestorage)
+
+ // Create HTTP server with SSE transport
+ mux := http.NewServeMux()
+
+ // Health check endpoint
+ mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`{"status":"healthy"}`))
+ })
+
+ // MCP Streamable HTTP endpoint (preferred over SSE as of 2025-03-26 spec)
+ streamHandler := mcpsdk.NewStreamableHTTPHandler(func(r *http.Request) *mcpsdk.Server {
+ return mcpServer.mcpServer
+ }, nil)
+ mux.Handle("/", streamHandler)
+
+ // Also support legacy SSE endpoint for backwards compatibility
+ sseHandler := mcpsdk.NewSSEHandler(func(r *http.Request) *mcpsdk.Server {
+ return mcpServer.mcpServer
+ }, nil)
+ mux.Handle("/sse", sseHandler)
+
+ srv := &http.Server{
+ Handler: mux,
+ Addr: address,
+ WriteTimeout: 60 * time.Second,
+ ReadTimeout: 30 * time.Second,
+ }
+
+ log.Info().Str("service_name", service_name).Str("address", address).Msg("Running MCP HTTP server with SSE transport")
+
+ err := srv.ListenAndServe()
+ log.Error().Err(err).Msg("MCP server error")
+}
diff --git a/servers/publicweb/journeys.go b/servers/publicweb/journeys.go
new file mode 100644
index 0000000..481cb73
--- /dev/null
+++ b/servers/publicweb/journeys.go
@@ -0,0 +1,142 @@
+package publicweb
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+)
+
+// JourneySearchResponse represents the search results for hydration
+type JourneySearchResponse struct {
+ Searched bool `json:"searched"`
+ DepartureDate string `json:"departure_date,omitempty"`
+ DepartureTime string `json:"departure_time,omitempty"`
+ Departure any `json:"departure,omitempty"`
+ Destination any `json:"destination,omitempty"`
+ Error string `json:"error,omitempty"`
+ Results struct {
+ SolidarityDrivers struct {
+ Number int `json:"number"`
+ } `json:"solidarity_drivers"`
+ OrganizedCarpools struct {
+ Number int `json:"number"`
+ } `json:"organized_carpools"`
+ Carpools struct {
+ Number int `json:"number"`
+ Results any `json:"results,omitempty"`
+ } `json:"carpools"`
+ PublicTransit struct {
+ Number int `json:"number"`
+ Results any `json:"results,omitempty"`
+ } `json:"public_transit"`
+ Vehicles struct {
+ Number int `json:"number"`
+ Results any `json:"results,omitempty"`
+ } `json:"vehicles"`
+ LocalSolutions struct {
+ Number int `json:"number"`
+ Results any `json:"results,omitempty"`
+ } `json:"local_solutions"`
+ } `json:"results"`
+}
+
+// journeySearchDataProvider provides data for the journey search page
+func (s *PublicWebServer) journeySearchDataProvider(r *http.Request) (any, error) {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ return JourneySearchResponse{Error: "invalid request"}, nil
+ }
+
+ departureDate := r.FormValue("departuredate")
+ departureTime := r.FormValue("departuretime")
+ departure := r.FormValue("departure")
+ destination := r.FormValue("destination")
+
+ response := JourneySearchResponse{
+ DepartureDate: departureDate,
+ DepartureTime: departureTime,
+ }
+
+ // If no search parameters, return empty response
+ if departure == "" || destination == "" || departureDate == "" || departureTime == "" {
+ return response, nil
+ }
+
+ // Parse timezone and datetime
+ locTime, err := time.LoadLocation("Europe/Paris")
+ if err != nil {
+ log.Error().Err(err).Msg("timezone error")
+ response.Error = "internal error"
+ return response, nil
+ }
+
+ departureDateTime, err := time.ParseInLocation("2006-01-02 15:04", departureDate+" "+departureTime, locTime)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing datetime")
+ response.Error = "invalid date/time format"
+ return response, nil
+ }
+
+ // Parse departure location
+ departureGeo, err := geojson.UnmarshalFeature([]byte(departure))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling departure")
+ response.Error = "invalid departure location"
+ return response, nil
+ }
+ response.Departure = departureGeo
+
+ // Parse destination location
+ destinationGeo, err := geojson.UnmarshalFeature([]byte(destination))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling destination")
+ response.Error = "invalid destination location"
+ return response, nil
+ }
+ response.Destination = destinationGeo
+
+ // Call business logic
+ result, err := s.applicationHandler.SearchJourneys(
+ r.Context(),
+ departureDateTime,
+ departureGeo,
+ destinationGeo,
+ "", // passengerID
+ "", // solidarityTransportExcludeDriver
+ "", // solidarityExcludeGroupId
+ nil, // options - use defaults
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error in journey search")
+ response.Error = "search failed"
+ return response, nil
+ }
+
+ response.Searched = result.Searched
+
+ // Solidarity drivers
+ response.Results.SolidarityDrivers.Number = len(result.DriverJourneys)
+
+ // Organized carpools
+ response.Results.OrganizedCarpools.Number = len(result.OrganizedCarpools)
+
+ // Carpools (from external operators like Movici)
+ response.Results.Carpools.Number = len(result.CarpoolResults)
+ response.Results.Carpools.Results = result.CarpoolResults
+
+ // Public transit
+ response.Results.PublicTransit.Number = len(result.TransitResults)
+ response.Results.PublicTransit.Results = result.TransitResults
+
+ // Fleet vehicles
+ response.Results.Vehicles.Number = len(result.VehicleResults)
+ response.Results.Vehicles.Results = result.VehicleResults
+
+ // Knowledge base / local solutions
+ response.Results.LocalSolutions.Number = len(result.KnowledgeBaseResults)
+ response.Results.LocalSolutions.Results = result.KnowledgeBaseResults
+
+ return response, nil
+}
diff --git a/servers/publicweb/publicweb.go b/servers/publicweb/publicweb.go
new file mode 100644
index 0000000..008cc5d
--- /dev/null
+++ b/servers/publicweb/publicweb.go
@@ -0,0 +1,191 @@
+package publicweb
+
+import (
+ "encoding/json"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/viper"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "git.coopgo.io/coopgo-apps/parcoursmob/services"
+)
+
+// DataProvider returns data to hydrate a page
+type DataProvider func(r *http.Request) (any, error)
+
+// DynamicRoute defines a route with its HTML file and data provider
+type DynamicRoute struct {
+ HTMLFile string
+ DataProvider DataProvider
+}
+
+// Regex to find the placeholder script tag
+var dynamicDataRegex = regexp.MustCompile(``)
+
+type PublicWebServer struct {
+ cfg *viper.Viper
+ services *services.ServicesHandler
+ kv cache.KVHandler
+ filestorage cache.FileStorage
+ applicationHandler *application.ApplicationHandler
+ rootDir string
+ dynamicRoutes map[string]DynamicRoute
+}
+
+func Run(
+ cfg *viper.Viper,
+ svc *services.ServicesHandler,
+ applicationHandler *application.ApplicationHandler,
+ kv cache.KVHandler,
+ filestorage cache.FileStorage,
+) {
+ address := cfg.GetString("server.publicweb.listen")
+ rootDir := cfg.GetString("server.publicweb.root_dir")
+ serviceName := cfg.GetString("service_name")
+
+ server := &PublicWebServer{
+ cfg: cfg,
+ services: svc,
+ kv: kv,
+ filestorage: filestorage,
+ applicationHandler: applicationHandler,
+ rootDir: rootDir,
+ dynamicRoutes: make(map[string]DynamicRoute),
+ }
+
+ server.registerDynamicRoutes()
+
+ r := mux.NewRouter()
+
+ r.HandleFunc("/health", server.healthHandler).Methods("GET")
+
+ for pattern := range server.dynamicRoutes {
+ r.HandleFunc(pattern, server.dynamicHandler).Methods("GET", "POST")
+ }
+
+ r.PathPrefix("/").Handler(server.fileServerHandler())
+
+ srv := &http.Server{
+ Handler: r,
+ Addr: address,
+ WriteTimeout: 30 * time.Second,
+ ReadTimeout: 15 * time.Second,
+ }
+
+ log.Info().
+ Str("service_name", serviceName).
+ Str("address", address).
+ Str("root_dir", rootDir).
+ Msg("Running Public Web HTTP server")
+
+ err := srv.ListenAndServe()
+ log.Error().Err(err).Msg("Public Web server error")
+}
+
+func (s *PublicWebServer) registerDynamicRoutes() {
+ s.RegisterDynamicRoute("/recherche/", "recherche/index.html", s.journeySearchDataProvider)
+}
+
+func (s *PublicWebServer) RegisterDynamicRoute(pattern, htmlFile string, provider DataProvider) {
+ s.dynamicRoutes[pattern] = DynamicRoute{
+ HTMLFile: htmlFile,
+ DataProvider: provider,
+ }
+}
+
+func (s *PublicWebServer) dynamicHandler(w http.ResponseWriter, r *http.Request) {
+ route := mux.CurrentRoute(r)
+ pattern, _ := route.GetPathTemplate()
+
+ dynRoute, exists := s.dynamicRoutes[pattern]
+ if !exists {
+ http.NotFound(w, r)
+ return
+ }
+
+ data, err := dynRoute.DataProvider(r)
+ if err != nil {
+ log.Error().Err(err).Str("route", pattern).Msg("Error getting data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ if err := s.hydrate(w, dynRoute.HTMLFile, data); err != nil {
+ http.NotFound(w, r)
+ }
+}
+
+// hydrate reads an HTML file and injects JSON data into
+func (s *PublicWebServer) hydrate(w http.ResponseWriter, htmlFile string, data any) error {
+ htmlPath := filepath.Join(s.rootDir, htmlFile)
+ htmlContent, err := os.ReadFile(htmlPath)
+ if err != nil {
+ log.Error().Err(err).Str("file", htmlPath).Msg("Error reading HTML file")
+ return err
+ }
+
+ jsonData, err := json.Marshal(data)
+ if err != nil {
+ log.Error().Err(err).Msg("Error marshaling data to JSON")
+ return err
+ }
+
+ // Replace the placeholder with a script that assigns data to window.__PARCOURSMOB_DATA__
+ replacement := []byte(``)
+ modifiedHTML := dynamicDataRegex.ReplaceAll(htmlContent, replacement)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write(modifiedHTML)
+ return nil
+}
+
+func (s *PublicWebServer) fileServerHandler() http.Handler {
+ fs := http.FileServer(http.Dir(s.rootDir))
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ path := filepath.Join(s.rootDir, r.URL.Path)
+
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ if filepath.Ext(path) == "" {
+ if idx := filepath.Join(path, "index.html"); fileExists(idx) {
+ http.ServeFile(w, r, idx)
+ return
+ }
+ if idx := filepath.Join(s.rootDir, "index.html"); fileExists(idx) {
+ http.ServeFile(w, r, idx)
+ return
+ }
+ }
+ http.NotFound(w, r)
+ return
+ }
+
+ if info, _ := os.Stat(path); info != nil && info.IsDir() {
+ if idx := filepath.Join(path, "index.html"); fileExists(idx) && strings.HasSuffix(r.URL.Path, "/") {
+ http.ServeFile(w, r, idx)
+ return
+ }
+ }
+
+ fs.ServeHTTP(w, r)
+ })
+}
+
+func fileExists(path string) bool {
+ _, err := os.Stat(path)
+ return err == nil
+}
+
+func (s *PublicWebServer) healthHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`{"status":"healthy"}`))
+}
diff --git a/servers/web/api/auth.go b/servers/web/api/auth.go
new file mode 100644
index 0000000..edc1c3e
--- /dev/null
+++ b/servers/web/api/auth.go
@@ -0,0 +1,38 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
+ code := r.URL.Query().Get("code")
+ if code == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ session, _ := h.idp.SessionsStore.Get(r, "parcoursmob_session")
+ redirectSession := ""
+ if session.Values["redirect"] != nil && session.Values["redirect"] != "" {
+ redirectSession = session.Values["redirect"].(string)
+ delete(session.Values, "redirect")
+ }
+
+ result, err := h.applicationHandler.ProcessOAuth2Callback(code, redirectSession)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ session.Values["idtoken"] = result.IDToken
+
+ if err = session.Save(r, w); err != nil {
+ log.Error().Err(err).Msg("Cannot save session")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, result.RedirectURL, http.StatusFound)
+}
\ No newline at end of file
diff --git a/servers/web/api/cache.go b/servers/web/api/cache.go
new file mode 100644
index 0000000..93aecc3
--- /dev/null
+++ b/servers/web/api/cache.go
@@ -0,0 +1,48 @@
+package api
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/cache"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) GetCache(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ cacheID := vars["cacheid"]
+
+ // Parse query parameters
+ limitsMinStr := r.URL.Query().Get("limits.min")
+ limitsMaxStr := r.URL.Query().Get("limits.max")
+ limitsMin, limitsMax := cache.ParseLimits(limitsMinStr, limitsMaxStr)
+
+ // Use a channel to synchronize the goroutines
+ ch := make(chan []byte)
+
+ // Fetch data from cache asynchronously
+ go func() {
+ result, err := h.cacheService.GetCacheData(cacheID, limitsMin, limitsMax)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get cache data")
+ w.WriteHeader(http.StatusNotFound)
+ ch <- nil
+ return
+ }
+ ch <- result.Data // Signal that the data has been fetched successfully
+ close(ch)
+ }()
+
+ // Wait for the JSON marshaling goroutine to finish
+ data := <-ch
+ if data == nil {
+ return // Stop processing if an error occurred
+ }
+
+ // Send the JSON response to the client
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write(data)
+
+ <-ch
+}
\ No newline at end of file
diff --git a/servers/web/api/calendars.go b/servers/web/api/calendars.go
new file mode 100644
index 0000000..40ce8e8
--- /dev/null
+++ b/servers/web/api/calendars.go
@@ -0,0 +1,51 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) CalendarGlobal(w http.ResponseWriter, r *http.Request) {
+ enabled := h.config.GetBool("modules.agenda.enabled") && h.config.GetBool("modules.agenda.calendars.global.enabled")
+ if !enabled {
+ log.Error().Msg("global calendar not activated in configuration")
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ result, err := h.applicationHandler.GenerateGlobalCalendar(r.Context())
+ if err != nil {
+ log.Error().Err(err).Msg("error generating global calendar")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/calendar; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(result.CalendarData))
+}
+
+func (h *Handler) CalendarOrganizations(w http.ResponseWriter, r *http.Request) {
+ enabled := h.config.GetBool("modules.agenda.enabled") && h.config.GetBool("modules.agenda.calendars.organizations.enabled")
+ if !enabled {
+ log.Error().Msg("organizations calendar not activated in configuration")
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ vars := mux.Vars(r)
+ groupID := vars["groupid"]
+
+ result, err := h.applicationHandler.GenerateOrganizationCalendar(r.Context(), groupID)
+ if err != nil {
+ log.Error().Err(err).Msg("error generating organization calendar")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/calendar; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(result.CalendarData))
+}
\ No newline at end of file
diff --git a/servers/web/api/export.go b/servers/web/api/export.go
new file mode 100644
index 0000000..a296d97
--- /dev/null
+++ b/servers/web/api/export.go
@@ -0,0 +1,31 @@
+package api
+
+import (
+ "encoding/csv"
+ "fmt"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) CacheExport(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ cacheID := vars["cacheid"]
+
+ result, err := h.applicationHandler.ExportCacheAsCSV(cacheID)
+ if err != nil {
+ log.Error().Err(err).Msg("Error exporting cache")
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/csv")
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=export-%s.csv", cacheID))
+
+ csvWriter := csv.NewWriter(w)
+ defer csvWriter.Flush()
+
+ csvWriter.Write(result.Headers)
+ csvWriter.WriteAll(result.Values)
+}
\ No newline at end of file
diff --git a/servers/web/api/geo.go b/servers/web/api/geo.go
new file mode 100644
index 0000000..2140b20
--- /dev/null
+++ b/servers/web/api/geo.go
@@ -0,0 +1,31 @@
+package api
+
+import (
+ "net/http"
+)
+
+func (h *Handler) GeoAutocomplete(w http.ResponseWriter, r *http.Request) {
+ t, ok := r.URL.Query()["text"]
+ if !ok || len(t[0]) < 1 {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ text := t[0]
+
+ featureCollection, err := h.geoService.Autocomplete(text)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ j, err := featureCollection.MarshalJSON()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write(j)
+}
\ No newline at end of file
diff --git a/servers/web/api/handler.go b/servers/web/api/handler.go
new file mode 100644
index 0000000..7338689
--- /dev/null
+++ b/servers/web/api/handler.go
@@ -0,0 +1,42 @@
+package api
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/cache"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/geo"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ cacheStorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "github.com/spf13/viper"
+)
+
+type Handler struct {
+ config *viper.Viper
+ idp *identification.IdentificationProvider
+ applicationHandler *application.ApplicationHandler
+ cacheService *cache.CacheService
+ geoService *geo.GeoService
+}
+
+func NewHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, appHandler *application.ApplicationHandler, cacheHandler cacheStorage.CacheHandler) *Handler {
+ cacheService := cache.NewCacheService(cacheHandler)
+
+ // Get geocoding configuration
+ geoType := cfg.GetString("geo.type")
+ baseURL := cfg.GetString("geo." + geoType + ".url")
+ autocompleteEndpoint := cfg.GetString("geo." + geoType + ".autocomplete")
+ geoService := geo.NewGeoService(geoType, baseURL, autocompleteEndpoint)
+
+ return &Handler{
+ config: cfg,
+ idp: idp,
+ applicationHandler: appHandler,
+ cacheService: cacheService,
+ geoService: geoService,
+ }
+}
+
+func (h *Handler) NotFound(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+}
\ No newline at end of file
diff --git a/servers/web/api/protected/handler.go b/servers/web/api/protected/handler.go
new file mode 100644
index 0000000..e1cc3e1
--- /dev/null
+++ b/servers/web/api/protected/handler.go
@@ -0,0 +1,26 @@
+package protected
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "github.com/spf13/viper"
+)
+
+type Handler struct {
+ apiKey string
+ config *viper.Viper
+ applicationHandler *application.ApplicationHandler
+}
+
+func NewHandler(cfg *viper.Viper, appHandler *application.ApplicationHandler) *Handler {
+ return &Handler{
+ apiKey: cfg.GetString("services.api.api_key"),
+ config: cfg,
+ applicationHandler: appHandler,
+ }
+}
+
+func (h *Handler) NotFound(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+}
\ No newline at end of file
diff --git a/servers/web/api/protected/users.go b/servers/web/api/protected/users.go
new file mode 100644
index 0000000..2600525
--- /dev/null
+++ b/servers/web/api/protected/users.go
@@ -0,0 +1,29 @@
+package protected
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+
+ "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) RegisterUserHTTP(w http.ResponseWriter, r *http.Request) {
+ var user storage.Account
+ if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ result, err := h.applicationHandler.RegisterUser(context.Background(), user)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to register user")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(result)
+}
\ No newline at end of file
diff --git a/servers/web/api_routes.go b/servers/web/api_routes.go
new file mode 100644
index 0000000..eaeec83
--- /dev/null
+++ b/servers/web/api_routes.go
@@ -0,0 +1,14 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupAPIRoutes(r *mux.Router) {
+ api_router := r.PathPrefix("/api").Subrouter()
+ api_router.HandleFunc("/", ws.webAPIHandler.NotFound)
+ api_router.HandleFunc("/geo/autocomplete", ws.webAPIHandler.GeoAutocomplete)
+ api_router.HandleFunc("/cache/{cacheid}", ws.webAPIHandler.GetCache)
+ api_router.HandleFunc("/cache/{cacheid}/export", ws.webAPIHandler.CacheExport)
+ api_router.HandleFunc("/oauth2/callback", ws.webAPIHandler.OAuth2Callback)
+}
\ No newline at end of file
diff --git a/servers/web/app_administration_routes.go b/servers/web/app_administration_routes.go
new file mode 100644
index 0000000..cfd94eb
--- /dev/null
+++ b/servers/web/app_administration_routes.go
@@ -0,0 +1,22 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupAdministrationRoutes(appRouter *mux.Router) {
+ admin := appRouter.PathPrefix("/administration").Subrouter()
+ admin.HandleFunc("/", ws.appHandler.AdministrationHTTPHandler())
+
+ // Groups
+ admin.HandleFunc("/groups/", ws.appHandler.AdministrationCreateGroupHTTPHandler())
+ admin.HandleFunc("/groups/{groupid}", ws.appHandler.AdministrationGroupDisplayHTTPHandler())
+ admin.HandleFunc("/groups/{groupid}/invite-admin", ws.appHandler.AdministrationGroupInviteAdminHTTPHandler())
+ admin.HandleFunc("/groups/{groupid}/invite-member", ws.appHandler.AdministrationGroupInviteMemberHTTPHandler())
+
+ // Statistics
+ admin.HandleFunc("/stats/vehicles", ws.appHandler.AdminStatsVehiclesHTTPHandler())
+ admin.HandleFunc("/stats/bookings", ws.appHandler.AdminStatsBookingsHTTPHandler())
+ admin.HandleFunc("/stats/beneficaires", ws.appHandler.AdminStatsBeneficiariesHTTPHandler())
+ admin.HandleFunc("/stats/events", ws.appHandler.AdminStatsEventsHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_agenda_routes.go b/servers/web/app_agenda_routes.go
new file mode 100644
index 0000000..c8b4048
--- /dev/null
+++ b/servers/web/app_agenda_routes.go
@@ -0,0 +1,26 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+
+func (ws *WebServer) setupAgendaRoutes(appRouter *mux.Router) {
+ agenda := appRouter.PathPrefix("/agenda").Subrouter()
+ agenda.HandleFunc("/", ws.appHandler.AgendaHomeHTTPHandler())
+ agenda.HandleFunc("/history", ws.appHandler.AgendaHistoryHTTPHandler())
+ agenda.HandleFunc("/create-event", ws.appHandler.AgendaCreateEventHTTPHandler())
+
+ // Events
+ agenda.HandleFunc("/{eventid}", ws.appHandler.AgendaDisplayEventHTTPHandler())
+ agenda.HandleFunc("/{eventid}/update", ws.appHandler.AgendaUpdateEventHTTPHandler())
+ agenda.HandleFunc("/{eventid}/delete", ws.appHandler.AgendaDeleteEventHTTPHandler())
+ agenda.HandleFunc("/{eventid}/subscribe", ws.appHandler.AgendaSubscribeEventHTTPHandler())
+ agenda.HandleFunc("/{eventid}/{subscribeid}/delete", ws.appHandler.AgendaDeleteSubscribeEventHTTPHandler())
+ agenda.HandleFunc("/{eventid}/history", ws.appHandler.AgendaHistoryEventHTTPHandler())
+
+ // Documents
+ agenda.HandleFunc("/{eventid}/documents", ws.appHandler.EventDocumentsHTTPHandler())
+ agenda.HandleFunc("/{eventid}/documents/{document}", ws.appHandler.EventDocumentDownloadHTTPHandler())
+
+}
\ No newline at end of file
diff --git a/servers/web/app_beneficiaries_routes.go b/servers/web/app_beneficiaries_routes.go
new file mode 100644
index 0000000..d2c7746
--- /dev/null
+++ b/servers/web/app_beneficiaries_routes.go
@@ -0,0 +1,20 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupBeneficiariesRoutes(appRouter *mux.Router) {
+ beneficiaries := appRouter.PathPrefix("/beneficiaries").Subrouter()
+ beneficiaries.HandleFunc("/", ws.appHandler.BeneficiariesListHTTPHandler())
+ beneficiaries.HandleFunc("/create", ws.appHandler.BeneficiaryCreateHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}", ws.appHandler.BeneficiaryDisplayHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}/update", ws.appHandler.BeneficiaryUpdateHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}/archive", ws.appHandler.BeneficiaryArchiveHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}/unarchive", ws.appHandler.BeneficiaryUnarchiveHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}/documents", ws.appHandler.BeneficiaryDocumentsHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}/documents/{document}", ws.appHandler.BeneficiaryDocumentDownloadHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}/documents/{document}/delete", ws.appHandler.BeneficiaryDocumentDeleteHTTPHandler())
+ beneficiaries.HandleFunc("/{beneficiaryid}/picture", ws.appHandler.BeneficiaryPictureHTTPHandler())
+
+}
\ No newline at end of file
diff --git a/servers/web/app_dashboard_routes.go b/servers/web/app_dashboard_routes.go
new file mode 100644
index 0000000..7b9cf9d
--- /dev/null
+++ b/servers/web/app_dashboard_routes.go
@@ -0,0 +1,10 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupDashboardRoutes(appRouter *mux.Router) {
+ // Dashboard
+ appRouter.HandleFunc("/", ws.appHandler.DashboardHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_directory_routes.go b/servers/web/app_directory_routes.go
new file mode 100644
index 0000000..e178b75
--- /dev/null
+++ b/servers/web/app_directory_routes.go
@@ -0,0 +1,9 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupDirectoryRoutes(appRouter *mux.Router) {
+ appRouter.HandleFunc("/directory/", ws.appHandler.DirectoryHomeHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_group_module_routes.go b/servers/web/app_group_module_routes.go
new file mode 100644
index 0000000..efe10f7
--- /dev/null
+++ b/servers/web/app_group_module_routes.go
@@ -0,0 +1,12 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupGroupModuleRoutes(appRouter *mux.Router) {
+ groupModule := appRouter.PathPrefix("/group_module").Subrouter()
+ groupModule.HandleFunc("/", ws.appHandler.GroupsHTTPHandler())
+ groupModule.HandleFunc("/groups", ws.appHandler.CreateGroupModuleHTTPHandler())
+ groupModule.HandleFunc("/groups/{groupid}", ws.appHandler.DisplayGroupModuleHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_group_routes.go b/servers/web/app_group_routes.go
new file mode 100644
index 0000000..12bbf30
--- /dev/null
+++ b/servers/web/app_group_routes.go
@@ -0,0 +1,11 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupGroupRoutes(appRouter *mux.Router) {
+ // Group Settings
+ appRouter.HandleFunc("/group/settings", ws.appHandler.GroupSettingsDisplayHTTPHandler())
+ appRouter.HandleFunc("/group/settings/invite-member", ws.appHandler.GroupSettingsInviteMemberHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_journeys_routes.go b/servers/web/app_journeys_routes.go
new file mode 100644
index 0000000..006deb6
--- /dev/null
+++ b/servers/web/app_journeys_routes.go
@@ -0,0 +1,14 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupJourneysRoutes(appRouter *mux.Router) {
+ journeys := appRouter.PathPrefix("/journeys").Subrouter()
+ journeys.HandleFunc("/", ws.appHandler.JourneysSearchHTTPHandler())
+ journeys.HandleFunc("/search", ws.appHandler.JourneysSearchCompactHTTPHandler())
+ journeys.HandleFunc("/save", ws.appHandler.SaveSearchHTTPHandler()).Methods("GET")
+ journeys.HandleFunc("/saved-searches/{id}/delete", ws.appHandler.DeleteSavedSearchHTTPHandler()).Methods("GET")
+}
+
diff --git a/servers/web/app_members_routes.go b/servers/web/app_members_routes.go
new file mode 100644
index 0000000..ef35463
--- /dev/null
+++ b/servers/web/app_members_routes.go
@@ -0,0 +1,12 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupMembersRoutes(appRouter *mux.Router) {
+ members := appRouter.PathPrefix("/members").Subrouter()
+ members.HandleFunc("/", ws.appHandler.MembersListHTTPHandler())
+ members.HandleFunc("/{adminid}", ws.appHandler.MemberDisplayHTTPHandler())
+ members.HandleFunc("/{adminid}/update", ws.appHandler.MemberUpdateHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_misc_routes.go b/servers/web/app_misc_routes.go
new file mode 100644
index 0000000..991902d
--- /dev/null
+++ b/servers/web/app_misc_routes.go
@@ -0,0 +1,10 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+)
+
+func setupMiscRoutes(appRouter *mux.Router, applicationHandler *application.ApplicationHandler) {
+ // Future misc routes can be added here
+}
\ No newline at end of file
diff --git a/servers/web/app_organized_carpool_routes.go b/servers/web/app_organized_carpool_routes.go
new file mode 100644
index 0000000..02607ec
--- /dev/null
+++ b/servers/web/app_organized_carpool_routes.go
@@ -0,0 +1,29 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupOrganizedCarpoolRoutes(appRouter *mux.Router) {
+ organizedCarpool := appRouter.PathPrefix("/organized-carpool").Subrouter()
+ organizedCarpool.HandleFunc("/", ws.appHandler.OrganizedCarpoolOverviewHTTPHandler())
+
+ // Drivers
+ organizedCarpool.HandleFunc("/drivers/create", ws.appHandler.OrganizedCarpoolCreateDriverHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/update", ws.appHandler.OrganizedCarpoolUpdateDriverHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/trips", ws.appHandler.OrganizedCarpoolAddTripHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/archive", ws.appHandler.OrganizedCarpoolArchiveDriverHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/unarchive", ws.appHandler.OrganizedCarpoolUnarchiveDriverHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/documents", ws.appHandler.OrganizedCarpoolDriverDocumentsHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/documents/{document}", ws.appHandler.OrganizedCarpoolDocumentDownloadHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/documents/{document}/delete", ws.appHandler.OrganizedCarpoolDocumentDeleteHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/trips/{tripid}/delete", ws.appHandler.OrganizedCarpoolDeleteTripHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}", ws.appHandler.OrganizedCarpoolDriverDisplayHTTPHandler())
+ organizedCarpool.HandleFunc("/drivers/{driverid}/journeys/{journeyid}", ws.appHandler.OrganizedCarpoolJourneyHTTPHandler())
+
+ // Bookings
+ organizedCarpool.HandleFunc("/bookings/{bookingid}", ws.appHandler.OrganizedCarpoolBookingDisplayHTTPHandler())
+ organizedCarpool.HandleFunc("/bookings/{bookingid}/confirm", ws.appHandler.OrganizedCarpoolBookingStatusHTTPHandler("confirm"))
+ organizedCarpool.HandleFunc("/bookings/{bookingid}/cancel", ws.appHandler.OrganizedCarpoolBookingStatusHTTPHandler("cancel"))
+ organizedCarpool.HandleFunc("/bookings/{bookingid}/waitconfirmation", ws.appHandler.OrganizedCarpoolBookingStatusHTTPHandler("waitconfirmation"))
+}
\ No newline at end of file
diff --git a/servers/web/app_sms_routes.go b/servers/web/app_sms_routes.go
new file mode 100644
index 0000000..cde161d
--- /dev/null
+++ b/servers/web/app_sms_routes.go
@@ -0,0 +1,9 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupSMSRoutes(appRouter *mux.Router) {
+ appRouter.HandleFunc("/sms/send", ws.appHandler.SendSMSHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_solidarity_transport_routes.go b/servers/web/app_solidarity_transport_routes.go
new file mode 100644
index 0000000..c83dc0d
--- /dev/null
+++ b/servers/web/app_solidarity_transport_routes.go
@@ -0,0 +1,31 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupSolidarityTransportRoutes(appRouter *mux.Router) {
+ solidarityTransport := appRouter.PathPrefix("/solidarity-transport").Subrouter()
+ solidarityTransport.HandleFunc("/", ws.appHandler.SolidarityTransportOverviewHTTPHandler())
+
+ // Drivers
+ solidarityTransport.HandleFunc("/drivers/create", ws.appHandler.SolidarityTransportCreateDriverHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/update", ws.appHandler.SolidarityTransportUpdateDriverHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/availabilities", ws.appHandler.SolidarityTransportAddAvailabilityHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/archive", ws.appHandler.SolidarityTransportArchiveDriverHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/unarchive", ws.appHandler.SolidarityTransportUnarchiveDriverHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/documents", ws.appHandler.SolidarityTransportDriverDocumentsHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/documents/{document}", ws.appHandler.SolidarityTransportDocumentDownloadHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/documents/{document}/delete", ws.appHandler.SolidarityTransportDocumentDeleteHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/availabilities/{availabilityid}/delete", ws.appHandler.SolidarityTransportDeleteAvailabilityHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/journeys/{journeyid}", ws.appHandler.SolidarityTransportDriverJourneyHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}/journeys/{journeyid}/noreturn", ws.appHandler.SolidarityTransportDriverJourneyToggleNoreturnHTTPHandler())
+ solidarityTransport.HandleFunc("/drivers/{driverid}", ws.appHandler.SolidarityTransportDriverDisplayHTTPHandler())
+
+ // Bookings
+ solidarityTransport.HandleFunc("/bookings/{bookingid}", ws.appHandler.SolidarityTransportBookingDisplayHTTPHandler())
+ solidarityTransport.HandleFunc("/bookings/{bookingid}/confirm", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("confirm"))
+ solidarityTransport.HandleFunc("/bookings/{bookingid}/cancel", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("cancel"))
+ solidarityTransport.HandleFunc("/bookings/{bookingid}/waitconfirmation", ws.appHandler.SolidarityTransportBookingStatusHTTPHandler("waitconfirmation"))
+ solidarityTransport.HandleFunc("/bookings/{bookingid}/create-replacement", ws.appHandler.SolidarityTransportCreateReplacementBookingHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_support_routes.go b/servers/web/app_support_routes.go
new file mode 100644
index 0000000..8bfccb8
--- /dev/null
+++ b/servers/web/app_support_routes.go
@@ -0,0 +1,10 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupSupportRoutes(appRouter *mux.Router) {
+ // Support
+ appRouter.HandleFunc("/support/", ws.appHandler.SupportSendHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_vehicles_management_routes.go b/servers/web/app_vehicles_management_routes.go
new file mode 100644
index 0000000..29e6132
--- /dev/null
+++ b/servers/web/app_vehicles_management_routes.go
@@ -0,0 +1,24 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupVehiclesManagementRoutes(appRouter *mux.Router) {
+ vehiclesManagement := appRouter.PathPrefix("/vehicles-management").Subrouter()
+ vehiclesManagement.HandleFunc("/", ws.appHandler.VehiclesManagementOverviewHTTPHandler())
+
+ // Fleet
+ vehiclesManagement.HandleFunc("/fleet/add", ws.appHandler.VehiclesFleetAddHTTPHandler())
+ vehiclesManagement.HandleFunc("/fleet/{vehicleid}", ws.appHandler.VehiclesFleetDisplayHTTPHandler())
+ vehiclesManagement.HandleFunc("/fleet/{vehicleid}/unavailability", ws.appHandler.VehiclesFleetMakeUnavailableHTTPHandler())
+ vehiclesManagement.HandleFunc("/fleet/{vehicleid}/update", ws.appHandler.VehiclesFleetUpdateHTTPHandler())
+
+ // Bookings
+ vehiclesManagement.HandleFunc("/bookings/", ws.appHandler.VehiclesManagementBookingsListHTTPHandler())
+ vehiclesManagement.HandleFunc("/bookings/{bookingid}", ws.appHandler.VehicleManagementBookingDisplayHTTPHandler())
+ vehiclesManagement.HandleFunc("/bookings/{bookingid}/change-vehicle", ws.appHandler.VehicleManagementBookingChangeVehicleHTTPHandler())
+ vehiclesManagement.HandleFunc("/bookings/{bookingid}/delete", ws.appHandler.DeleteBookingHTTPHandler())
+ vehiclesManagement.HandleFunc("/bookings/{bookingid}/unbooking", ws.appHandler.UnbookingVehicleHTTPHandler())
+ vehiclesManagement.HandleFunc("/bookings/{bookingid}/documents/{document}", ws.appHandler.BookingDocumentDownloadHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_vehicles_routes.go b/servers/web/app_vehicles_routes.go
new file mode 100644
index 0000000..c1933c6
--- /dev/null
+++ b/servers/web/app_vehicles_routes.go
@@ -0,0 +1,18 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupVehiclesRoutes(appRouter *mux.Router) {
+ vehicles := appRouter.PathPrefix("/vehicles").Subrouter()
+ vehicles.HandleFunc("/", ws.appHandler.VehiclesSearchHTTPHandler())
+
+ // Bookings
+ vehicles.HandleFunc("/bookings/", ws.appHandler.VehiclesBookingsListHTTPHandler())
+ vehicles.HandleFunc("/bookings/{bookingid}", ws.appHandler.VehicleBookingDisplayHTTPHandler())
+ vehicles.HandleFunc("/bookings/{bookingid}/documents/{document}", ws.appHandler.BookingDocumentDownloadHTTPHandler())
+
+ // Vehicle booking
+ vehicles.HandleFunc("/v/{vehicleid}/b/{beneficiaryid}", ws.appHandler.BookVehicleHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/app_wallets_routes.go b/servers/web/app_wallets_routes.go
new file mode 100644
index 0000000..7b09eb6
--- /dev/null
+++ b/servers/web/app_wallets_routes.go
@@ -0,0 +1,10 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupWalletsRoutes(appRouter *mux.Router) {
+ wallets := appRouter.PathPrefix("/wallets").Subrouter()
+ wallets.HandleFunc("/{userid}/credit", ws.appHandler.CreditWalletHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/application/administration.go b/servers/web/application/administration.go
new file mode 100644
index 0000000..2304586
--- /dev/null
+++ b/servers/web/application/administration.go
@@ -0,0 +1,247 @@
+package application
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) AdministrationHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Call business logic handler
+ result, err := h.applicationHandler.GetAdministrationData(r.Context())
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving administration data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Render HTTP response
+ h.renderer.Administration(
+ w, r,
+ result.Accounts,
+ result.Beneficiaries,
+ result.Groups,
+ result.Bookings,
+ result.Events,
+ )
+ }
+}
+
+func (h *Handler) AdministrationCreateGroupHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract and validate form parameters
+ name := r.FormValue("name")
+ if name == "" {
+ log.Error().Str("name", name).Msg("Invalid name")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract modules configuration
+ modules := map[string]any{
+ "beneficiaries": r.FormValue("modules.beneficiaries") == "on",
+ "journeys": r.FormValue("modules.journeys") == "on",
+ "vehicles": r.FormValue("modules.vehicles") == "on",
+ "vehicles_management": r.FormValue("modules.vehicles_management") == "on",
+ "events": r.FormValue("modules.events") == "on",
+ "agenda": r.FormValue("modules.agenda") == "on",
+ "groups": r.FormValue("modules.groups") == "on",
+ "administration": r.FormValue("modules.administration") == "on",
+ "support": r.FormValue("modules.support") == "on",
+ "group_module": r.FormValue("modules.group_module") == "on",
+ "organized_carpool": r.FormValue("modules.organized_carpool") == "on",
+ "solidarity_transport": r.FormValue("modules.solidarity_transport") == "on",
+ }
+
+ // Call business logic handler
+ groupID, err := h.applicationHandler.CreateAdministrationGroup(r.Context(), name, modules)
+ if err != nil {
+ log.Error().Err(err).Msg("error creating administration group")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Redirect to group display
+ http.Redirect(w, r, "/app/administration/groups/"+groupID, http.StatusFound)
+ return
+ }
+
+ // For GET requests, render the create group form
+ h.renderer.AdministrationCreateGroup(w, r)
+ }
+}
+
+func (h *Handler) AdministrationGroupDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ groupID := vars["groupid"]
+
+ // Call business logic handler
+ result, err := h.applicationHandler.GetAdministrationGroupData(r.Context(), groupID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving administration group data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Render HTTP response
+ h.renderer.AdministrationGroupDisplay(w, r, result.Group, result.Members, result.Admins)
+ }
+}
+
+func (h *Handler) AdministrationGroupInviteAdminHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ groupID := vars["groupid"]
+
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract form parameters
+ username := r.FormValue("username")
+ if username == "" {
+ log.Error().Str("username", username).Msg("Invalid username")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Call business logic handler
+ err := h.applicationHandler.InviteAdministrationGroupAdmin(r.Context(), groupID, username)
+ if err != nil {
+ log.Error().Err(err).Msg("error inviting group admin")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Redirect back to group display
+ http.Redirect(w, r, "/app/administration/groups/"+groupID, http.StatusFound)
+ return
+ }
+
+ // For GET requests, redirect to group display (no separate form)
+ http.Redirect(w, r, "/app/administration/groups/"+groupID, http.StatusFound)
+ }
+}
+
+func (h *Handler) AdministrationGroupInviteMemberHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ groupID := vars["groupid"]
+
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract form parameters
+ username := r.FormValue("username")
+ if username == "" {
+ log.Error().Str("username", username).Msg("Invalid username")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Call business logic handler
+ err := h.applicationHandler.InviteAdministrationGroupMember(r.Context(), groupID, username)
+ if err != nil {
+ log.Error().Err(err).Msg("error inviting group member")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Redirect back to group display
+ http.Redirect(w, r, "/app/administration/groups/"+groupID, http.StatusFound)
+ return
+ }
+
+ // For GET requests, redirect to group display (no separate form)
+ http.Redirect(w, r, "/app/administration/groups/"+groupID, http.StatusFound)
+ }
+}
+func (h *Handler) AdminStatsVehiclesHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ result, err := h.applicationHandler.GetVehiclesStats()
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving vehicles stats")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AdminStatVehicles(w, r, result.Vehicles, result.Bookings, result.Groups)
+ }
+}
+
+func (h *Handler) AdminStatsBookingsHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Extract filter parameters from query
+ status := r.URL.Query().Get("status")
+ dateStart := r.URL.Query().Get("date_start")
+ dateEnd := r.URL.Query().Get("date_end")
+
+ // Default to last month if no dates specified
+ if dateStart == "" {
+ dateStart = time.Now().AddDate(0, -1, 0).Format("2006-01-02")
+ }
+ if dateEnd == "" {
+ dateEnd = time.Now().Format("2006-01-02")
+ }
+
+ result, err := h.applicationHandler.GetBookingsStats(status, dateStart, dateEnd)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving bookings stats")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Prepare filters map for template
+ filters := map[string]string{
+ "status": status,
+ "date_start": dateStart,
+ "date_end": dateEnd,
+ }
+
+ h.renderer.AdminStatBookings(w, r, result.Vehicles, result.Bookings, result.Groups, result.BeneficiariesMap, filters)
+ }
+}
+
+func (h *Handler) AdminStatsBeneficiariesHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ result, err := h.applicationHandler.GetBeneficiariesStats()
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving beneficiaries stats")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AdminStatBeneficaires(w, r, result.Beneficiaries, result.CacheID)
+ }
+}
+
+func (h *Handler) AdminStatsEventsHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ result, err := h.applicationHandler.GetEventsStats()
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving events stats")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AdminStatEvents(w, r, result.Events, result.Groups)
+ }
+}
diff --git a/servers/web/application/agenda.go b/servers/web/application/agenda.go
new file mode 100644
index 0000000..7ac52a2
--- /dev/null
+++ b/servers/web/application/agenda.go
@@ -0,0 +1,469 @@
+package application
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+type EventsForm struct {
+ Name string `json:"name" validate:"required"`
+ Type string `json:"type" validate:"required"`
+ Description string `json:"description"`
+ Address any `json:"address,omitempty"`
+ Allday bool `json:"allday"`
+ Startdate *time.Time `json:"startdate"`
+ Enddate *time.Time `json:"enddate"`
+ Starttime string `json:"starttime"`
+ Endtime string `json:"endtime"`
+ MaxSubscribers int `json:"max_subscribers"`
+}
+
+func parseEventsForm(r *http.Request) (*EventsForm, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+
+ var startdate *time.Time
+ var enddate *time.Time
+
+ if r.PostFormValue("startdate") != "" {
+ d, err := time.Parse("2006-01-02", r.PostFormValue("startdate"))
+ if err != nil {
+ return nil, err
+ }
+ startdate = &d
+ }
+
+ if r.PostFormValue("enddate") != "" {
+ d, err := time.Parse("2006-01-02", r.PostFormValue("enddate"))
+ if err != nil {
+ return nil, err
+ }
+ enddate = &d
+ }
+
+ max_subscribers, err := strconv.Atoi(r.PostFormValue("max_subscribers"))
+ if err != nil {
+ return nil, err
+ }
+
+ formData := &EventsForm{
+ Name: r.PostFormValue("name"),
+ Type: r.PostFormValue("type"),
+ Description: r.PostFormValue("description"),
+ Startdate: startdate,
+ Enddate: enddate,
+ Starttime: r.PostFormValue("starttime"),
+ Endtime: r.PostFormValue("endtime"),
+ MaxSubscribers: max_subscribers,
+ }
+
+ if r.PostFormValue("allday") == "true" {
+ formData.Allday = true
+ }
+
+ if r.PostFormValue("address") != "" {
+ var a any
+ json.Unmarshal([]byte(r.PostFormValue("address")), &a)
+
+ formData.Address = a
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return nil, err
+ }
+
+ return formData, nil
+}
+
+func (h *Handler) AgendaHomeHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ today := time.Now().Truncate(24 * time.Hour)
+ result, err := h.applicationHandler.GetAgendaEvents(r.Context(), &today, nil)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda events")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AgendaHome(w, r, result.Events, result.Groups)
+ }
+}
+
+func (h *Handler) AgendaHistoryHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ result, err := h.applicationHandler.GetAgendaEvents(r.Context(), nil, nil)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda events")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AgendaHistory(w, r, result.Events, result.Groups)
+ }
+}
+
+func (h *Handler) AgendaCreateEventHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ // Parse form data
+ eventForm, err := parseEventsForm(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing event form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Handle file upload if present
+ var file io.Reader
+ var filename string
+ var fileSize int64
+ var documentType, documentName string
+
+ contentType := r.Header.Get("Content-Type")
+ if strings.HasPrefix(contentType, "multipart/form-data") {
+ err = r.ParseMultipartForm(100 * 1024 * 1024) // 100 MB limit
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing multipart form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ uploadedFile, header, err := r.FormFile("file-upload")
+ if err == nil {
+ defer uploadedFile.Close()
+ file = uploadedFile
+ filename = header.Filename
+ fileSize = header.Size
+ documentType = r.FormValue("file_type")
+ documentName = r.FormValue("file_name")
+ } else if err != http.ErrMissingFile {
+ log.Error().Err(err).Msg("error retrieving file")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ eventID, err := h.applicationHandler.CreateAgendaEvent(
+ r.Context(),
+ eventForm.Name,
+ eventForm.Type,
+ eventForm.Description,
+ eventForm.Address,
+ eventForm.Allday,
+ eventForm.Startdate,
+ eventForm.Enddate,
+ eventForm.Starttime,
+ eventForm.Endtime,
+ eventForm.MaxSubscribers,
+ file,
+ filename,
+ fileSize,
+ documentType,
+ documentName,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error creating agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventID), http.StatusFound)
+ return
+ }
+
+ // For GET requests, render the create event form with config data
+ documentTypes := h.cfg.GetStringSlice("modules.agenda.documents_types")
+ fileTypes := h.cfg.GetStringMapString("storage.files.file_types")
+ h.renderer.AgendaCreateEvent(w, r, documentTypes, fileTypes, nil)
+ }
+}
+
+func (h *Handler) AgendaDisplayEventHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+
+ result, err := h.applicationHandler.GetAgendaEvent(r.Context(), eventID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ documentTypes := h.cfg.GetStringSlice("modules.agenda.documents_types")
+ fileTypes := h.cfg.GetStringMapString("storage.files.file_types")
+ h.renderer.AgendaDisplayEvent(w, r, result.Event, result.Group, documentTypes, fileTypes, result.Documents, result.Subscribers, result.Accounts)
+ }
+}
+
+func (h *Handler) AgendaUpdateEventHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+
+ if r.Method == "POST" {
+ // Parse form data
+ eventForm, err := parseEventsForm(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing event form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ updatedEventID, err := h.applicationHandler.UpdateAgendaEvent(
+ r.Context(),
+ eventID,
+ eventForm.Name,
+ eventForm.Type,
+ eventForm.Description,
+ eventForm.Address,
+ eventForm.Allday,
+ eventForm.Startdate,
+ eventForm.Enddate,
+ eventForm.Starttime,
+ eventForm.Endtime,
+ eventForm.MaxSubscribers,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error updating agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", updatedEventID), http.StatusFound)
+ return
+ }
+
+ // For GET requests, render the update event form
+ result, err := h.applicationHandler.GetAgendaEvent(r.Context(), eventID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AgendaUpdateEvent(w, r, result.Event)
+ }
+}
+
+func (h *Handler) AgendaDeleteEventHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+
+ if r.Method == "POST" {
+ if err := h.applicationHandler.DeleteAgendaEvent(r.Context(), eventID); err != nil {
+ log.Error().Err(err).Msg("error deleting agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/agenda/", http.StatusFound)
+ return
+ }
+
+ // For GET requests, render the delete confirmation form
+ result, err := h.applicationHandler.GetAgendaEvent(r.Context(), eventID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AgendaDeleteEvent(w, r, result.Event)
+ }
+}
+
+func (h *Handler) AgendaSubscribeEventHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ subscriber := r.FormValue("subscriber")
+
+ // Get current user and group information
+ current_group, err := h.currentGroup(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting current group")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ current_user_token, current_user_claims, err := h.currentUser(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting current user")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ subscriptionData := map[string]any{
+ "subscribed_by": map[string]any{
+ "user": map[string]any{
+ "id": current_user_token.Subject,
+ "display_name": current_user_claims["first_name"].(string) + " " + current_user_claims["last_name"].(string),
+ "email": current_user_claims["email"].(string),
+ },
+ "group": map[string]any{
+ "id": current_group.ID,
+ "name": current_group.Data["name"],
+ },
+ },
+ }
+
+ if err := h.applicationHandler.SubscribeToAgendaEvent(r.Context(), eventID, subscriber, subscriptionData); err != nil {
+ log.Error().Err(err).Msg("error subscribing to agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventID), http.StatusFound)
+ return
+ }
+
+ // For GET requests, redirect to event display
+ http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventID), http.StatusFound)
+ }
+}
+
+func (h *Handler) AgendaDeleteSubscribeEventHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+ subscribeID := vars["subscribeid"]
+
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ motif := r.FormValue("motif")
+
+ // Get current user and group information
+ current_group, err := h.currentGroup(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting current group")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ current_user_token, current_user_claims, err := h.currentUser(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting current user")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ if err := h.applicationHandler.UnsubscribeFromAgendaEvent(
+ r.Context(),
+ eventID,
+ subscribeID,
+ motif,
+ current_user_token.Subject,
+ current_user_claims["first_name"].(string)+" "+current_user_claims["last_name"].(string),
+ current_user_claims["email"].(string),
+ current_group.ID,
+ current_group.Data["name"].(string),
+ ); err != nil {
+ log.Error().Err(err).Msg("error unsubscribing from agenda event")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventID), http.StatusFound)
+ return
+ }
+
+ // For GET requests, render the delete subscription form
+ h.renderer.AgendaDeleteSubscribeEvent(w, r, eventID)
+ }
+}
+
+func (h *Handler) AgendaHistoryEventHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+
+ result, err := h.applicationHandler.GetAgendaEventHistory(r.Context(), eventID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving agenda event history")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.AgendaHistoryEvent(w, r, result.Event, result.Group, result.Subscribers, result.Accounts)
+ }
+}
+
+func (h *Handler) EventDocumentsHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+
+ if err := r.ParseMultipartForm(100 * 1024 * 1024); err != nil { // 100 MB limit
+ log.Error().Err(err).Msg("error parsing multipart form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ documentType := r.FormValue("type")
+ documentName := r.FormValue("name")
+
+ file, header, err := r.FormFile("file-upload")
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving file")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ if err := h.applicationHandler.AddEventDocument(r.Context(), eventID, file, header.Filename, header.Size, documentType, documentName); err != nil {
+ log.Error().Err(err).Msg("error adding event document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventID), http.StatusFound)
+ }
+}
+
+func (h *Handler) EventDocumentDownloadHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+ document := vars["document"]
+
+ file, info, err := h.applicationHandler.GetEventDocument(r.Context(), eventID, document)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving event document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", info.ContentType)
+ if _, err = io.Copy(w, file); err != nil {
+ log.Error().Err(err).Msg("error copying file content")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventID), http.StatusFound)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/beneficiaries.go b/servers/web/application/beneficiaries.go
new file mode 100644
index 0000000..07e8d6d
--- /dev/null
+++ b/servers/web/application/beneficiaries.go
@@ -0,0 +1,324 @@
+package application
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "time"
+
+ formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+type BeneficiariesForm struct {
+ FirstName string `json:"first_name" validate:"required"`
+ LastName string `json:"last_name" validate:"required"`
+ Email string `json:"email" validate:"required,email"`
+ Birthdate *time.Time `json:"birthdate" validate:"required"`
+ PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
+ FileNumber string `json:"file_number"`
+ Address any `json:"address,omitempty"`
+ Gender string `json:"gender"`
+ OtherProperties any `json:"other_properties,omitempty"`
+}
+
+func parseBeneficiariesForm(r *http.Request) (*BeneficiariesForm, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+
+ var date *time.Time
+
+ if r.PostFormValue("birthdate") != "" {
+ d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate"))
+ if err != nil {
+ return nil, err
+ }
+ date = &d
+ }
+
+ formData := &BeneficiariesForm{
+ FirstName: r.PostFormValue("first_name"),
+ LastName: r.PostFormValue("last_name"),
+ Email: r.PostFormValue("email"),
+ Birthdate: date,
+ PhoneNumber: r.PostFormValue("phone_number"),
+ FileNumber: r.PostFormValue("file_number"),
+ Gender: r.PostFormValue("gender"),
+ }
+
+ if r.PostFormValue("address") != "" {
+ var a any
+ json.Unmarshal([]byte(r.PostFormValue("address")), &a)
+ formData.Address = a
+ }
+
+ if r.PostFormValue("other_properties") != "" {
+ var a any
+ json.Unmarshal([]byte(r.PostFormValue("other_properties")), &a)
+ formData.OtherProperties = a
+ }
+
+ validate := formvalidators.New()
+ if err := validate.Struct(formData); err != nil {
+ return nil, err
+ }
+
+ return formData, nil
+}
+
+func (h *Handler) BeneficiariesListHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Extract search and archived filters from query parameters
+ searchFilter := ""
+ if search := r.URL.Query().Get("search"); search != "" {
+ searchFilter = search
+ }
+
+ archivedFilter := false
+ if archived := r.URL.Query().Get("archived"); archived == "true" {
+ archivedFilter = true
+ }
+
+ result, err := h.applicationHandler.GetBeneficiaries(r.Context(), searchFilter, archivedFilter)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving beneficiaries")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.BeneficiariesList(w, r, result.Accounts, result.CacheID, archivedFilter)
+ }
+}
+
+func (h *Handler) BeneficiaryCreateHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ beneficiaryForm, err := parseBeneficiariesForm(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing beneficiary form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ beneficiaryID, err := h.applicationHandler.CreateBeneficiary(
+ r.Context(),
+ beneficiaryForm.FirstName,
+ beneficiaryForm.LastName,
+ beneficiaryForm.Email,
+ beneficiaryForm.Birthdate,
+ beneficiaryForm.PhoneNumber,
+ beneficiaryForm.FileNumber,
+ beneficiaryForm.Address,
+ beneficiaryForm.Gender,
+ beneficiaryForm.OtherProperties,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error creating beneficiary")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
+ return
+ }
+
+ h.renderer.BeneficiaryCreate(w, r)
+ }
+}
+
+func (h *Handler) BeneficiaryDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+
+ // Extract tab parameter
+ tab := r.URL.Query().Get("tab")
+ if tab == "" {
+ tab = "documents" // Default tab
+ }
+
+ result, err := h.applicationHandler.GetBeneficiaryData(r.Context(), beneficiaryID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving beneficiary data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ beneficiariesFileTypes := h.cfg.GetStringSlice("modules.beneficiaries.documents_types")
+ fileTypesMap := h.cfg.GetStringMapString("storage.files.file_types")
+
+ h.renderer.BeneficiaryDisplay(w, r, result.Account, result.Bookings, result.Organizations, beneficiariesFileTypes, fileTypesMap, result.Documents, result.EventsList, result.SolidarityTransportStats, result.SolidarityTransportBookings, result.SolidarityDriversMap, result.OrganizedCarpoolStats, result.OrganizedCarpoolBookings, result.OrganizedCarpoolDriversMap, result.WalletBalance, tab)
+ }
+}
+
+func (h *Handler) BeneficiaryUpdateHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+
+ if r.Method == "POST" {
+ beneficiaryForm, err := parseBeneficiariesForm(r)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing beneficiary form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ updatedBeneficiaryID, err := h.applicationHandler.UpdateBeneficiary(
+ r.Context(),
+ beneficiaryID,
+ beneficiaryForm.FirstName,
+ beneficiaryForm.LastName,
+ beneficiaryForm.Email,
+ beneficiaryForm.Birthdate,
+ beneficiaryForm.PhoneNumber,
+ beneficiaryForm.FileNumber,
+ beneficiaryForm.Address,
+ beneficiaryForm.Gender,
+ beneficiaryForm.OtherProperties,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error updating beneficiary")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", updatedBeneficiaryID), http.StatusFound)
+ return
+ }
+
+ // For GET requests, just get the basic beneficiary account data
+ result, err := h.applicationHandler.GetBeneficiary(r.Context(), beneficiaryID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving beneficiary for update")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.BeneficiaryUpdate(w, r, result.Account)
+ }
+}
+
+func (h *Handler) BeneficiaryArchiveHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+
+ if err := h.applicationHandler.ArchiveBeneficiary(r.Context(), beneficiaryID); err != nil {
+ log.Error().Err(err).Msg("error archiving beneficiary")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
+ }
+}
+
+func (h *Handler) BeneficiaryUnarchiveHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+
+ if err := h.applicationHandler.UnarchiveBeneficiary(r.Context(), beneficiaryID); err != nil {
+ log.Error().Err(err).Msg("error unarchiving beneficiary")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
+ }
+}
+
+func (h *Handler) BeneficiaryPictureHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+
+ imageData, contentType, err := h.applicationHandler.GetBeneficiaryPicture(r.Context(), beneficiaryID)
+ if err != nil {
+ log.Error().Err(err).Msg("error generating beneficiary picture")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", contentType)
+ w.Header().Set("Content-Length", strconv.Itoa(len(imageData)))
+ if _, err := w.Write(imageData); err != nil {
+ log.Error().Err(err).Msg("unable to write image")
+ }
+ }
+}
+
+func (h *Handler) BeneficiaryDocumentsHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+
+ if err := r.ParseMultipartForm(100 * 1024 * 1024); err != nil { // 100 MB limit
+ log.Error().Err(err).Msg("error parsing multipart form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ documentType := r.FormValue("type")
+ documentName := r.FormValue("name")
+
+ file, header, err := r.FormFile("file-upload")
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving file")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ if err := h.applicationHandler.AddBeneficiaryDocument(r.Context(), beneficiaryID, file, header.Filename, header.Size, documentType, documentName); err != nil {
+ log.Error().Err(err).Msg("error adding beneficiary document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
+ }
+}
+
+func (h *Handler) BeneficiaryDocumentDownloadHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+ document := vars["document"]
+
+ file, info, err := h.applicationHandler.GetBeneficiaryDocument(r.Context(), beneficiaryID, document)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving beneficiary document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", info.ContentType)
+ if _, err = io.Copy(w, file); err != nil {
+ log.Error().Err(err).Msg("error copying file content")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+}
+
+func (h *Handler) BeneficiaryDocumentDeleteHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ beneficiaryID := vars["beneficiaryid"]
+ document := vars["document"]
+
+ if err := h.applicationHandler.DeleteBeneficiaryDocument(r.Context(), beneficiaryID, document); err != nil {
+ log.Error().Err(err).Msg("error deleting beneficiary document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/dashboard.go b/servers/web/application/dashboard.go
new file mode 100644
index 0000000..8a464d2
--- /dev/null
+++ b/servers/web/application/dashboard.go
@@ -0,0 +1,74 @@
+package application
+
+import (
+ "net/http"
+ "sort"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) DashboardHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse driver address geography filter
+ driverAddressGeo := r.URL.Query().Get("driver_address_geo")
+ driverAddressGeoLayer, driverAddressGeoCode := "", ""
+ if driverAddressGeo != "" {
+ parts := strings.SplitN(driverAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ driverAddressGeoLayer, driverAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ result, err := h.applicationHandler.GetDashboardData(r.Context(), driverAddressGeoLayer, driverAddressGeoCode)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving dashboard data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Enrich geography filters with names from geography service
+ var enrichedGeoFilters []map[string]string
+ if h.cfg.GetBool("geography.filters.enabled") {
+ geoFilters := h.cfg.Get("geography.filters.geographies")
+ if geoList, ok := geoFilters.([]any); ok {
+ for _, geoItem := range geoList {
+ if geoMap, ok := geoItem.(map[string]any); ok {
+ layer := ""
+ code := ""
+ if l, ok := geoMap["layer"].(string); ok {
+ layer = l
+ }
+ if c, ok := geoMap["code"].(string); ok {
+ code = c
+ }
+
+ enrichedGeo := map[string]string{
+ "layer": layer,
+ "code": code,
+ "name": code, // Default to code if name fetch fails
+ }
+
+ // Fetch name from geography service
+ if layer != "" && code != "" {
+ if geoFeature, err := h.services.Geography.Find(layer, code); err == nil {
+ if name := geoFeature.Properties.MustString("nom"); name != "" {
+ enrichedGeo["name"] = name
+ }
+ }
+ }
+
+ enrichedGeoFilters = append(enrichedGeoFilters, enrichedGeo)
+ }
+ }
+ }
+
+ // Sort by name
+ sort.Slice(enrichedGeoFilters, func(i, j int) bool {
+ return enrichedGeoFilters[i]["name"] < enrichedGeoFilters[j]["name"]
+ })
+ }
+
+ h.renderer.Dashboard(w, r, result.Accounts, len(result.Accounts), len(result.Members), result.Events, result.Bookings, result.SolidarityDrivers, result.OrganizedCarpoolDrivers, driverAddressGeo, enrichedGeoFilters)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/directory.go b/servers/web/application/directory.go
new file mode 100644
index 0000000..2af66bd
--- /dev/null
+++ b/servers/web/application/directory.go
@@ -0,0 +1,11 @@
+package application
+
+import (
+ "net/http"
+)
+
+func (h *Handler) DirectoryHomeHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.renderer.DirectoryHome(w, r)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/group.go b/servers/web/application/group.go
new file mode 100644
index 0000000..2489efc
--- /dev/null
+++ b/servers/web/application/group.go
@@ -0,0 +1,54 @@
+package application
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) GroupSettingsDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ group := g.(storage.Group)
+
+ result, err := h.applicationHandler.GetGroupSettings(r.Context(), group.ID)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting group settings")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.GroupSettingsDisplay(w, r, result.Group, result.GroupMembers, result.Admins)
+ }
+}
+
+func (h *Handler) GroupSettingsInviteMemberHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ group := g.(storage.Group)
+
+ r.ParseForm()
+ username := r.FormValue("username")
+
+ err := h.applicationHandler.InviteMemberToGroup(r.Context(), group.ID, username)
+ if err != nil {
+ log.Error().Err(err).Msg("error inviting member to group")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/group/settings", http.StatusFound)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/group_module.go b/servers/web/application/group_module.go
new file mode 100644
index 0000000..8a96b45
--- /dev/null
+++ b/servers/web/application/group_module.go
@@ -0,0 +1,101 @@
+package application
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) GroupsHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ result, err := h.applicationHandler.GetGroups(r.Context())
+ if err != nil {
+ log.Error().Err(err).Msg("error getting groups")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.Groups(w, r, result.Groups)
+ }
+}
+
+func (h *Handler) CreateGroupModuleHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ r.ParseForm()
+
+ name := r.FormValue("name")
+ groupType := r.FormValue("type")
+ description := r.FormValue("description")
+ address := r.PostFormValue("address")
+
+ result, err := h.applicationHandler.CreateGroupModule(r.Context(), name, groupType, description, address)
+ if err != nil {
+ log.Error().Err(err).Msg("error creating group module")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/group_module/groups/"+result.GroupID, http.StatusFound)
+ return
+ }
+
+ result, err := h.applicationHandler.GetGroupModuleCreateData(r.Context())
+ if err != nil {
+ log.Error().Err(err).Msg("error getting group module create data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.CreateGroupModule(w, r, result.GroupTypes)
+ }
+}
+
+func (h *Handler) DisplayGroupModuleHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ groupID := vars["groupid"]
+
+ if r.Method == "POST" || r.FormValue("beneficiaryid") != "" {
+ r.ParseForm()
+ beneficiaryID := r.FormValue("beneficiaryid")
+
+ err := h.applicationHandler.SubscribeBeneficiaryToGroup(r.Context(), groupID, beneficiaryID)
+ if err != nil {
+ log.Error().Err(err).Msg("error subscribing beneficiary to group")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/group_module/groups/"+groupID, http.StatusFound)
+ return
+ }
+
+ // Get current user's group from context
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ currentUserGroup := g.(storage.Group)
+
+ // Parse search filter
+ searchFilter := ""
+ if searchFilters, ok := r.URL.Query()["search"]; ok && len(searchFilters) > 0 {
+ searchFilter = searchFilters[0]
+ }
+
+ result, err := h.applicationHandler.DisplayGroupModule(r.Context(), groupID, searchFilter, currentUserGroup)
+ if err != nil {
+ log.Error().Err(err).Msg("error displaying group module")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.DisplayGroupModule(w, r, result.GroupID, result.Accounts, result.CacheID,
+ result.Searched, result.Beneficiary, result.Group, result.AccountsBeneficiaire)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/handler.go b/servers/web/application/handler.go
new file mode 100644
index 0000000..fd5c074
--- /dev/null
+++ b/servers/web/application/handler.go
@@ -0,0 +1,60 @@
+package application
+
+import (
+ "errors"
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/renderer"
+ "git.coopgo.io/coopgo-apps/parcoursmob/services"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/spf13/viper"
+)
+
+type Handler struct {
+ cfg *viper.Viper
+ renderer *renderer.Renderer
+ applicationHandler *application.ApplicationHandler
+ idp *identification.IdentificationProvider
+ services *services.ServicesHandler
+}
+
+func NewHandler(cfg *viper.Viper, renderer *renderer.Renderer, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, services *services.ServicesHandler) *Handler {
+ return &Handler{
+ cfg: cfg,
+ renderer: renderer,
+ applicationHandler: applicationHandler,
+ idp: idp,
+ services: services,
+ }
+}
+
+func (h *Handler) currentGroup(r *http.Request) (current_group storage.Group, err error) {
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ return storage.Group{}, errors.New("current group not found")
+ }
+ current_group = g.(storage.Group)
+
+ return current_group, nil
+}
+
+func (h *Handler) currentUser(r *http.Request) (current_user_token *oidc.IDToken, current_user_claims map[string]any, err error) {
+ // Get current user ID
+ u := r.Context().Value(identification.IdtokenKey)
+ if u == nil {
+ return nil, nil, errors.New("current user not found")
+ }
+ current_user_token = u.(*oidc.IDToken)
+
+ // Get current user claims
+ c := r.Context().Value(identification.ClaimsKey)
+ if c == nil {
+ return nil, nil, errors.New("current user claims not found")
+ }
+ current_user_claims = c.(map[string]any)
+
+ return current_user_token, current_user_claims, nil
+}
\ No newline at end of file
diff --git a/servers/web/application/journeys.go b/servers/web/application/journeys.go
new file mode 100644
index 0000000..01e82e5
--- /dev/null
+++ b/servers/web/application/journeys.go
@@ -0,0 +1,422 @@
+package application
+
+import (
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ savedsearchtypes "git.coopgo.io/coopgo-platform/saved-search/data/types"
+ "github.com/gorilla/mux"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract and convert HTTP parameters
+ departureDate := r.FormValue("departuredate")
+ departureTime := r.FormValue("departuretime")
+ departure := r.FormValue("departure")
+ destination := r.FormValue("destination")
+ passengerID := r.FormValue("passengerid")
+ solidarityTransportExcludeDriver := r.FormValue("solidarity_transport_exclude_driver")
+
+ // Parse timezone and datetime
+ locTime, err := time.LoadLocation("Europe/Paris")
+ if err != nil {
+ log.Error().Err(err).Msg("timezone error")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ var departureDateTime time.Time
+ if departureDate != "" && departureTime != "" {
+ departureDateTime, err = time.ParseInLocation("2006-01-02 15:04", departureDate+" "+departureTime, locTime)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing datetime")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ log.Info().
+ Str("departureDate", departureDate).
+ Str("departureTime", departureTime).
+ Time("departureDateTime", departureDateTime).
+ Str("timezone", departureDateTime.Location().String()).
+ Str("RFC3339", departureDateTime.Format(time.RFC3339)).
+ Msg("Journey search - parsed departure datetime")
+ }
+
+ // Parse departure location
+ var departureGeo *geojson.Feature
+ if departure == "" && passengerID != "" {
+ // Get passenger address
+ p, err := h.services.GetAccount(passengerID)
+ if err != nil {
+ log.Error().Err(err).Msg("could not retrieve passenger")
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ departureBytes, err := json.Marshal(p.Data["address"])
+ if err != nil {
+ log.Error().Err(err).Any("address", p.Data["address"]).Msg("could not marshal address")
+ } else {
+ departureGeo, err = geojson.UnmarshalFeature(departureBytes)
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling passenger departure")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+ } else if departure != "" {
+ departureGeo, err = geojson.UnmarshalFeature([]byte(departure))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling departure")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse destination location
+ var destinationGeo *geojson.Feature
+ if destination != "" {
+ destinationGeo, err = geojson.UnmarshalFeature([]byte(destination))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling destination")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Call business logic handler
+ result, err := h.applicationHandler.SearchJourneys(
+ r.Context(),
+ departureDateTime,
+ departureGeo,
+ destinationGeo,
+ passengerID,
+ solidarityTransportExcludeDriver,
+ "", // solidarityExcludeGroupId - for modal search replacement only
+ nil, // options - use defaults
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error in journey search")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Get beneficiaries for rendering
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ log.Error().Msg("group not found in request context")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ group := g.(groupstorage.Group)
+
+ beneficiaries, err := h.services.GetBeneficiariesInGroup(group)
+ if err != nil {
+ log.Error().Err(err).Msg("issue retrieving beneficiaries")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Get saved searches for this group when no search has been performed
+ var savedSearches []*savedsearchtypes.SavedSearch
+ var beneficiariesMap map[string]mobilityaccountsstorage.Account
+ if !result.Searched {
+ savedSearches, err = h.applicationHandler.GetSavedSearchesByOwner(r.Context(), group.ID)
+ if err != nil {
+ log.Error().Err(err).Msg("issue retrieving saved searches")
+ // Don't fail the request, just log the error
+ savedSearches = []*savedsearchtypes.SavedSearch{}
+ }
+
+ // Create beneficiaries map for template lookup
+ beneficiariesMap = make(map[string]mobilityaccountsstorage.Account)
+ for _, b := range beneficiaries {
+ beneficiariesMap[b.ID] = b
+ }
+ }
+
+ // Render HTTP response
+ h.renderer.JourneysSearch(
+ w, r,
+ result.CarpoolResults,
+ result.TransitResults,
+ result.VehicleResults,
+ result.Searched,
+ departureGeo,
+ destinationGeo,
+ departureDate,
+ departureTime,
+ result.DriverJourneys,
+ result.Drivers,
+ result.OrganizedCarpools,
+ beneficiaries,
+ result.KnowledgeBaseResults,
+ passengerID,
+ savedSearches,
+ beneficiariesMap,
+ )
+ }
+}
+
+func (h *Handler) SaveSearchHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract search parameters from URL query
+ query := r.URL.Query()
+ departureDate := query.Get("departuredate")
+ departureTime := query.Get("departuretime")
+ departure := query.Get("departure")
+ destination := query.Get("destination")
+ passengerID := query.Get("passengerid")
+
+ // Debug logging
+ log.Debug().
+ Str("departuredate", departureDate).
+ Str("departuretime", departureTime).
+ Str("departure", departure).
+ Str("destination", destination).
+ Str("passengerid", passengerID).
+ Str("query", r.URL.RawQuery).
+ Msg("SaveSearch request parameters")
+
+ // Parse timezone and datetime
+ locTime, err := time.LoadLocation("Europe/Paris")
+ if err != nil {
+ log.Error().Err(err).Msg("timezone error")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ var departureDateTime time.Time
+ if departureDate != "" && departureTime != "" {
+ departureDateTime, err = time.ParseInLocation("2006-01-02 15:04", departureDate+" "+departureTime, locTime)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing datetime")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse departure location
+ var departureGeo *geojson.Feature
+ if departure != "" {
+ departureGeo, err = geojson.UnmarshalFeature([]byte(departure))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling departure")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse destination location
+ var destinationGeo *geojson.Feature
+ if destination != "" {
+ destinationGeo, err = geojson.UnmarshalFeature([]byte(destination))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling destination")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Get group ID from session/context
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ log.Error().Msg("group not found in request context")
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ group := g.(groupstorage.Group)
+
+ // Prepare additional data
+ additionalData := map[string]interface{}{}
+ if passengerID != "" {
+ additionalData["passenger_id"] = passengerID
+ }
+
+ // Save the search using the business logic
+ err = h.applicationHandler.SaveSearch(
+ r.Context(),
+ group.ID,
+ departureDateTime,
+ departureGeo,
+ destinationGeo,
+ additionalData,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error saving search")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Redirect back to search with success message
+ http.Redirect(w, r, "/app/journeys/", http.StatusSeeOther)
+ }
+}
+
+func (h *Handler) DeleteSavedSearchHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get search ID from URL parameters
+ vars := mux.Vars(r)
+ searchID := vars["id"]
+ if searchID == "" {
+ log.Error().Msg("search ID not provided")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Get group ID from session/context for authorization
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ log.Error().Msg("group not found in request context")
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ group := g.(groupstorage.Group)
+
+ // Delete the saved search using the business logic
+ err := h.applicationHandler.DeleteSavedSearch(r.Context(), searchID, group.ID)
+ if err != nil {
+ log.Error().Err(err).Str("search_id", searchID).Msg("error deleting saved search")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ log.Info().Str("search_id", searchID).Str("group_id", group.ID).Msg("saved search deleted successfully")
+
+ // Redirect back to journeys page
+ http.Redirect(w, r, "/app/journeys/?deleted=1", http.StatusSeeOther)
+ }
+}
+
+func (h *Handler) JourneysSearchCompactHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract and convert HTTP parameters (same as JourneysSearchHTTPHandler)
+ departureDate := r.FormValue("departuredate")
+ departureTime := r.FormValue("departuretime")
+ departure := r.FormValue("departure")
+ destination := r.FormValue("destination")
+ passengerID := r.FormValue("passengerid")
+ solidarityTransportExcludeDriver := r.FormValue("solidarity_transport_exclude_driver")
+
+ // Parse timezone and datetime
+ locTime, err := time.LoadLocation("Europe/Paris")
+ if err != nil {
+ log.Error().Err(err).Msg("timezone error")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ var departureDateTime time.Time
+ if departureDate != "" && departureTime != "" {
+ departureDateTime, err = time.ParseInLocation("2006-01-02 15:04", departureDate+" "+departureTime, locTime)
+ if err != nil {
+ log.Error().Err(err).Msg("error parsing datetime")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse departure location
+ var departureGeo *geojson.Feature
+ if departure == "" && passengerID != "" {
+ // Get passenger address
+ p, err := h.services.GetAccount(passengerID)
+ if err != nil {
+ log.Error().Err(err).Msg("could not retrieve passenger")
+ http.Error(w, "Not Found", http.StatusNotFound)
+ return
+ }
+
+ departureBytes, err := json.Marshal(p.Data["address"])
+ if err != nil {
+ log.Error().Err(err).Any("address", p.Data["address"]).Msg("could not marshal address")
+ } else {
+ departureGeo, err = geojson.UnmarshalFeature(departureBytes)
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling passenger departure")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+ } else if departure != "" {
+ departureGeo, err = geojson.UnmarshalFeature([]byte(departure))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling departure")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse destination location
+ var destinationGeo *geojson.Feature
+ if destination != "" {
+ destinationGeo, err = geojson.UnmarshalFeature([]byte(destination))
+ if err != nil {
+ log.Error().Err(err).Msg("error unmarshalling destination")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Call business logic handler
+ result, err := h.applicationHandler.SearchJourneys(
+ r.Context(),
+ departureDateTime,
+ departureGeo,
+ destinationGeo,
+ passengerID,
+ solidarityTransportExcludeDriver,
+ "", // solidarityExcludeGroupId - for modal search replacement only
+ nil, // options - use defaults
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error in journey search")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Render compact HTTP response
+ h.renderer.JourneysSearchCompact(
+ w, r,
+ result.CarpoolResults,
+ result.TransitResults,
+ result.VehicleResults,
+ result.Searched,
+ departureGeo,
+ destinationGeo,
+ departureDate,
+ departureTime,
+ result.DriverJourneys,
+ result.Drivers,
+ result.OrganizedCarpools,
+ result.KnowledgeBaseResults,
+ passengerID,
+ )
+ }
+}
+
diff --git a/servers/web/application/members.go b/servers/web/application/members.go
new file mode 100644
index 0000000..44ac557
--- /dev/null
+++ b/servers/web/application/members.go
@@ -0,0 +1,72 @@
+package application
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) MembersListHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ result, err := h.applicationHandler.GetMembers(r.Context())
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving members")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.MembersList(w, r, result.Accounts, result.CacheID, result.GroupsNames)
+ }
+}
+
+func (h *Handler) MemberDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ memberID := vars["adminid"]
+
+ result, err := h.applicationHandler.GetMemberData(r.Context(), memberID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving member data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.MemberDisplay(w, r, result.Account, result.GroupsNames)
+ }
+}
+
+func (h *Handler) MemberUpdateHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ memberID := vars["adminid"]
+
+ if r.Method == "POST" {
+ firstName := r.PostFormValue("first_name")
+ lastName := r.PostFormValue("last_name")
+ email := r.PostFormValue("email")
+ phoneNumber := r.PostFormValue("phone_number")
+ gender := r.PostFormValue("gender")
+
+ updatedMemberID, err := h.applicationHandler.UpdateMember(r.Context(), memberID, firstName, lastName, email, phoneNumber, gender)
+ if err != nil {
+ log.Error().Err(err).Msg("error updating member")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/members/%s", updatedMemberID), http.StatusFound)
+ return
+ }
+
+ result, err := h.applicationHandler.GetMember(r.Context(), memberID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving member for update")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.MemberUpdate(w, r, result.Account)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/organized_carpool.go b/servers/web/application/organized_carpool.go
new file mode 100644
index 0000000..adecf15
--- /dev/null
+++ b/servers/web/application/organized_carpool.go
@@ -0,0 +1,596 @@
+package application
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "sort"
+ "strings"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/gorilla/mux"
+ "github.com/paulmach/orb/geojson"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) OrganizedCarpoolOverviewHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse form to get both query params and form data
+ r.ParseForm()
+
+ // Extract filter parameters
+ tab := r.FormValue("tab")
+ if tab == "" {
+ tab = "carpoolService" // Default to showing current bookings
+ }
+
+ // Extract archived filter
+ archivedFilter := false
+ if archived := r.URL.Query().Get("archived"); archived == "true" {
+ archivedFilter = true
+ }
+
+ // Apply filters conditionally based on tab
+ var status, driverID, startDate, endDate, departureGeo, destinationGeo, passengerAddressGeo, driverAddressGeo string
+ var histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeo, histDestinationGeo, histPassengerAddressGeo string
+
+ // Driver address geography filter (applies when on drivers tab)
+ if tab == "drivers" {
+ driverAddressGeo = r.FormValue("driver_address_geo")
+ }
+
+ if tab == "carpoolService" {
+ status = r.FormValue("status")
+ driverID = r.FormValue("driver_id")
+ startDate = r.FormValue("date_start")
+ endDate = r.FormValue("date_end")
+ departureGeo = r.FormValue("departure_geo")
+ destinationGeo = r.FormValue("destination_geo")
+ passengerAddressGeo = r.FormValue("passenger_address_geo")
+ }
+
+ // History filters (apply when on carpoolHistory tab)
+ if tab == "carpoolHistory" {
+ histStatus = r.FormValue("status")
+ histDriverID = r.FormValue("driver_id")
+ histStartDate = r.FormValue("date_start")
+ histEndDate = r.FormValue("date_end")
+ histDepartureGeo = r.FormValue("departure_geo")
+ histDestinationGeo = r.FormValue("destination_geo")
+ histPassengerAddressGeo = r.FormValue("passenger_address_geo")
+ }
+
+ // Set default history dates if not provided
+ if histStartDate == "" {
+ histStartDate = time.Now().Add(-30 * 24 * time.Hour).Format("2006-01-02")
+ }
+ if histEndDate == "" {
+ histEndDate = time.Now().Add(-24 * time.Hour).Format("2006-01-02")
+ }
+
+ // Parse geography parameters (format: "layer:code")
+ departureGeoLayer, departureGeoCode := "", ""
+ if departureGeo != "" {
+ parts := strings.SplitN(departureGeo, ":", 2)
+ if len(parts) == 2 {
+ departureGeoLayer, departureGeoCode = parts[0], parts[1]
+ }
+ }
+ destinationGeoLayer, destinationGeoCode := "", ""
+ if destinationGeo != "" {
+ parts := strings.SplitN(destinationGeo, ":", 2)
+ if len(parts) == 2 {
+ destinationGeoLayer, destinationGeoCode = parts[0], parts[1]
+ }
+ }
+ passengerAddressGeoLayer, passengerAddressGeoCode := "", ""
+ if passengerAddressGeo != "" {
+ parts := strings.SplitN(passengerAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ passengerAddressGeoLayer, passengerAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ histDepartureGeoLayer, histDepartureGeoCode := "", ""
+ if histDepartureGeo != "" {
+ parts := strings.SplitN(histDepartureGeo, ":", 2)
+ if len(parts) == 2 {
+ histDepartureGeoLayer, histDepartureGeoCode = parts[0], parts[1]
+ }
+ }
+ histDestinationGeoLayer, histDestinationGeoCode := "", ""
+ if histDestinationGeo != "" {
+ parts := strings.SplitN(histDestinationGeo, ":", 2)
+ if len(parts) == 2 {
+ histDestinationGeoLayer, histDestinationGeoCode = parts[0], parts[1]
+ }
+ }
+ histPassengerAddressGeoLayer, histPassengerAddressGeoCode := "", ""
+ if histPassengerAddressGeo != "" {
+ parts := strings.SplitN(histPassengerAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ histPassengerAddressGeoLayer, histPassengerAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ // Parse driver address geography parameter
+ driverAddressGeoLayer, driverAddressGeoCode := "", ""
+ if driverAddressGeo != "" {
+ parts := strings.SplitN(driverAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ driverAddressGeoLayer, driverAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ result, err := h.applicationHandler.GetOrganizedCarpoolOverview(r.Context(), status, driverID, startDate, endDate, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode, histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeoLayer, histDepartureGeoCode, histDestinationGeoLayer, histDestinationGeoCode, histPassengerAddressGeoLayer, histPassengerAddressGeoCode, archivedFilter, driverAddressGeoLayer, driverAddressGeoCode)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving organized carpool overview")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Build filters map for template
+ filters := map[string]any{
+ "tab": tab,
+ "date_start": startDate,
+ "date_end": endDate,
+ "status": status,
+ "driver_id": driverID,
+ "departure_geo": departureGeo,
+ "destination_geo": destinationGeo,
+ "passenger_address_geo": passengerAddressGeo,
+ "driver_address_geo": driverAddressGeo,
+ }
+
+ histFilters := map[string]any{
+ "tab": tab,
+ "date_start": histStartDate,
+ "date_end": histEndDate,
+ "status": histStatus,
+ "driver_id": histDriverID,
+ "departure_geo": histDepartureGeo,
+ "destination_geo": histDestinationGeo,
+ "passenger_address_geo": histPassengerAddressGeo,
+ }
+
+ // Enrich geography filters with names from geography service
+ var enrichedGeoFilters []map[string]string
+ if h.cfg.GetBool("geography.filters.enabled") {
+ geoFilters := h.cfg.Get("geography.filters.geographies")
+ if geoList, ok := geoFilters.([]any); ok {
+ for _, geoItem := range geoList {
+ if geoMap, ok := geoItem.(map[string]any); ok {
+ layer := ""
+ code := ""
+ if l, ok := geoMap["layer"].(string); ok {
+ layer = l
+ }
+ if c, ok := geoMap["code"].(string); ok {
+ code = c
+ }
+
+ enrichedGeo := map[string]string{
+ "layer": layer,
+ "code": code,
+ "name": code, // Default to code if name fetch fails
+ }
+
+ // Fetch name from geography service
+ if layer != "" && code != "" {
+ if geoFeature, err := h.services.Geography.Find(layer, code); err == nil {
+ if name := geoFeature.Properties.MustString("nom"); name != "" {
+ enrichedGeo["name"] = name
+ }
+ }
+ }
+
+ enrichedGeoFilters = append(enrichedGeoFilters, enrichedGeo)
+ }
+ }
+ }
+
+ // Sort by name
+ sort.Slice(enrichedGeoFilters, func(i, j int) bool {
+ return enrichedGeoFilters[i]["name"] < enrichedGeoFilters[j]["name"]
+ })
+ }
+
+ h.renderer.OrganizedCarpoolOverview(w, r, result.Accounts, result.AccountsMap, result.BeneficiariesMap, result.Bookings, result.BookingsHistory, filters, histFilters, tab, enrichedGeoFilters, archivedFilter)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolBookingDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ result, err := h.applicationHandler.GetOrganizedCarpoolBookingData(r.Context(), bookingID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving organized carpool booking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.OrganizedCarpoolBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.DriverDepartureAddress, result.DriverArrivalAddress)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolBookingStatusHTTPHandler(action string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ err := h.applicationHandler.UpdateOrganizedCarpoolBookingStatus(r.Context(), bookingID, action)
+ if err != nil {
+ log.Error().Err(err).Msg("error updating organized carpool booking status")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/bookings/%s", bookingID), http.StatusSeeOther)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolCreateDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ // Parse form data
+ firstName := r.PostFormValue("first_name")
+ lastName := r.PostFormValue("last_name")
+ email := r.PostFormValue("email")
+ phoneNumber := r.PostFormValue("phone_number")
+ fileNumber := r.PostFormValue("file_number")
+ gender := r.PostFormValue("gender")
+
+ var birthdate *time.Time
+ if r.PostFormValue("birthdate") != "" {
+ if d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate")); err == nil {
+ birthdate = &d
+ }
+ }
+
+ // Parse JSON address fields
+ var address, addressDestination any
+ if r.PostFormValue("address") != "" {
+ json.Unmarshal([]byte(r.PostFormValue("address")), &address)
+ }
+ if r.PostFormValue("address_destination") != "" {
+ json.Unmarshal([]byte(r.PostFormValue("address_destination")), &addressDestination)
+ }
+
+ driverID, err := h.applicationHandler.CreateOrganizedCarpoolDriver(
+ r.Context(),
+ firstName,
+ lastName,
+ email,
+ birthdate,
+ phoneNumber,
+ fileNumber,
+ address,
+ addressDestination,
+ gender,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error creating organized carpool driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", driverID), http.StatusFound)
+ return
+ }
+
+ h.renderer.OrganizedCarpoolCreateDriver(w, r)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolDriverDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ // Extract tab parameter
+ tab := r.URL.Query().Get("tab")
+ if tab == "" {
+ tab = "documents" // Default tab
+ }
+
+ result, err := h.applicationHandler.GetOrganizedCarpoolDriverData(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving organized carpool driver data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.OrganizedCarpoolDriverDisplay(w, r, result.Driver, result.Trips, result.Documents, result.Bookings, result.BeneficiariesMap, result.Stats, result.WalletBalance, tab)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolUpdateDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ if r.Method == "POST" {
+ // Parse form data
+ firstName := r.PostFormValue("first_name")
+ lastName := r.PostFormValue("last_name")
+ email := r.PostFormValue("email")
+ phoneNumber := r.PostFormValue("phone_number")
+ fileNumber := r.PostFormValue("file_number")
+ gender := r.PostFormValue("gender")
+
+ var birthdate *time.Time
+ if r.PostFormValue("birthdate") != "" {
+ if d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate")); err == nil {
+ birthdate = &d
+ }
+ }
+
+ // Parse JSON address fields
+ var address, addressDestination any
+ if r.PostFormValue("address") != "" {
+ json.Unmarshal([]byte(r.PostFormValue("address")), &address)
+ }
+ if r.PostFormValue("address_destination") != "" {
+ json.Unmarshal([]byte(r.PostFormValue("address_destination")), &addressDestination)
+ }
+
+ updatedDriverID, err := h.applicationHandler.UpdateOrganizedCarpoolDriver(
+ r.Context(),
+ driverID,
+ firstName,
+ lastName,
+ email,
+ birthdate,
+ phoneNumber,
+ fileNumber,
+ address,
+ addressDestination,
+ gender,
+ r.PostFormValue("other_properties"),
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error updating organized carpool driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", updatedDriverID), http.StatusFound)
+ return
+ }
+
+ result, err := h.applicationHandler.GetOrganizedCarpoolDriver(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving organized carpool driver for update")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.OrganizedCarpoolUpdateDriver(w, r, result.Driver)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolArchiveDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ err := h.applicationHandler.ArchiveOrganizedCarpoolDriver(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error archiving organized carpool driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolUnarchiveDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ err := h.applicationHandler.UnarchiveOrganizedCarpoolDriver(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error unarchiving organized carpool driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolDriverDocumentsHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ if err := r.ParseMultipartForm(100 * 1024 * 1024); err != nil { // 100 MB limit
+ log.Error().Err(err).Msg("error parsing multipart form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ documentType := r.FormValue("type")
+ documentName := r.FormValue("name")
+
+ file, header, err := r.FormFile("file-upload")
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving file")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ if err := h.applicationHandler.AddOrganizedCarpoolDriverDocument(r.Context(), driverID, file, header.Filename, header.Size, documentType, documentName); err != nil {
+ log.Error().Err(err).Msg("error adding organized carpool driver document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolDocumentDownloadHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ document := vars["document"]
+
+ file, info, err := h.applicationHandler.GetOrganizedCarpoolDriverDocument(r.Context(), driverID, document)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving organized carpool driver document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", info.ContentType)
+ if _, err = io.Copy(w, file); err != nil {
+ log.Error().Err(err).Msg("error copying file content")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+}
+
+func (h *Handler) OrganizedCarpoolDocumentDeleteHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ document := vars["document"]
+
+ if err := h.applicationHandler.DeleteOrganizedCarpoolDriverDocument(r.Context(), driverID, document); err != nil {
+ log.Error().Err(err).Msg("error deleting organized carpool driver document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolAddTripHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ log.Error().Msg("Wrong method")
+ http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing availabilities form")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ // Parse form data
+ outwardtime := r.PostFormValue("outwardtime")
+ returntime := r.PostFormValue("returntime")
+
+ // Parse GeoJSON features
+ departure, err := geojson.UnmarshalFeature([]byte(r.PostFormValue("address_departure")))
+ if err != nil {
+ log.Error().Err(err).Msg("failed parsing departure geojson")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ destination, err := geojson.UnmarshalFeature([]byte(r.PostFormValue("address_destination")))
+ if err != nil {
+ log.Error().Err(err).Msg("failed parsing destination geojson")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Parse days
+ days := map[string]bool{
+ "monday": r.PostFormValue("days.monday") == "on",
+ "tuesday": r.PostFormValue("days.tuesday") == "on",
+ "wednesday": r.PostFormValue("days.wednesday") == "on",
+ "thursday": r.PostFormValue("days.thursday") == "on",
+ "friday": r.PostFormValue("days.friday") == "on",
+ "saturday": r.PostFormValue("days.saturday") == "on",
+ "sunday": r.PostFormValue("days.sunday") == "on",
+ }
+
+ err = h.applicationHandler.AddOrganizedCarpoolTrip(r.Context(), driverID, outwardtime, returntime, departure, destination, days)
+ if err != nil {
+ log.Error().Err(err).Msg("error adding organized carpool trip")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolDeleteTripHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ tripID := vars["tripid"]
+
+ err := h.applicationHandler.DeleteOrganizedCarpoolTrip(r.Context(), tripID)
+ if err != nil {
+ log.Error().Err(err).Msg("error deleting organized carpool trip")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) OrganizedCarpoolJourneyHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ journeyID := vars["journeyid"]
+ passengerID := r.URL.Query().Get("passengerid")
+
+ if r.Method == "POST" {
+ // Parse form data
+ motivation := r.PostFormValue("motivation")
+ message := r.PostFormValue("message")
+ doNotSend := r.PostFormValue("do_not_send") == "on"
+
+ bookingID, err := h.applicationHandler.CreateOrganizedCarpoolJourneyBooking(r.Context(), driverID, journeyID, passengerID, motivation, message, doNotSend)
+ if err != nil {
+ log.Error().Err(err).Msg("error creating organized carpool journey booking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ log.Info().Str("booking_id", bookingID).Msg("Carpool booking created successfully")
+ http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/"), http.StatusFound)
+ return
+ }
+
+ // Get current user's group
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ log.Error().Msg("group not found in request context")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ group := g.(groupstorage.Group)
+
+ result, err := h.applicationHandler.GetOrganizedCarpoolJourneyData(r.Context(), driverID, journeyID, passengerID, group)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving organized carpool journey data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.OrganizedCarpoolJourney(w, r, result.Journey, result.Driver, result.Passenger, result.Beneficiaries, result.PassengerWalletBalance, result.PricingResult)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/sms.go b/servers/web/application/sms.go
new file mode 100644
index 0000000..a653683
--- /dev/null
+++ b/servers/web/application/sms.go
@@ -0,0 +1,36 @@
+package application
+
+import (
+ "net/http"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) SendSMSHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ referer := r.Referer()
+
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("Bad request")
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ message := r.PostFormValue("message")
+ beneficiaryID := r.PostFormValue("beneficiaryid")
+
+ err := h.applicationHandler.SendSMS(r.Context(), beneficiaryID, message)
+ if err != nil {
+ log.Error().Err(err).Msg("error sending SMS")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, referer, http.StatusFound)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/solidarity_transport.go b/servers/web/application/solidarity_transport.go
new file mode 100644
index 0000000..0be6974
--- /dev/null
+++ b/servers/web/application/solidarity_transport.go
@@ -0,0 +1,788 @@
+package application
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "sort"
+ "strings"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ gen "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) SolidarityTransportOverviewHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Parse form to get both query params and form data
+ r.ParseForm()
+
+ // Extract filter parameters using the same field names as the old handler
+ tab := r.FormValue("tab")
+ if tab == "" {
+ tab = "solidarityService" // Default to showing current bookings
+ }
+
+ // Extract archived filter
+ archivedFilter := false
+ if archived := r.URL.Query().Get("archived"); archived == "true" {
+ archivedFilter = true
+ }
+
+ // Apply filters conditionally based on tab (matching old handler logic)
+ var status, driverID, startDate, endDate, departureGeo, destinationGeo, passengerAddressGeo, driverAddressGeo string
+ var histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeo, histDestinationGeo, histPassengerAddressGeo string
+
+ // Driver address geography filter (applies when on drivers tab)
+ if tab == "drivers" {
+ driverAddressGeo = r.FormValue("driver_address_geo")
+ }
+
+ if tab == "solidarityService" {
+ status = r.FormValue("status")
+ driverID = r.FormValue("driver_id")
+ startDate = r.FormValue("date_start")
+ endDate = r.FormValue("date_end")
+ departureGeo = r.FormValue("departure_geo")
+ destinationGeo = r.FormValue("destination_geo")
+ passengerAddressGeo = r.FormValue("passenger_address_geo")
+ }
+
+ // History filters (apply when on solidarityHistory tab)
+ if tab == "solidarityHistory" {
+ histStatus = r.FormValue("status") // Note: history uses same field names as current
+ histDriverID = r.FormValue("driver_id")
+ histStartDate = r.FormValue("date_start")
+ histEndDate = r.FormValue("date_end")
+ histDepartureGeo = r.FormValue("departure_geo")
+ histDestinationGeo = r.FormValue("destination_geo")
+ histPassengerAddressGeo = r.FormValue("passenger_address_geo")
+ }
+
+ // Set default history dates if not provided
+ if histStartDate == "" {
+ histStartDate = time.Now().Add(-30 * 24 * time.Hour).Format("2006-01-02")
+ }
+ if histEndDate == "" {
+ histEndDate = time.Now().Add(-24 * time.Hour).Format("2006-01-02")
+ }
+
+ // Parse geography parameters (format: "layer:code")
+ departureGeoLayer, departureGeoCode := "", ""
+ if departureGeo != "" {
+ parts := strings.SplitN(departureGeo, ":", 2)
+ if len(parts) == 2 {
+ departureGeoLayer, departureGeoCode = parts[0], parts[1]
+ }
+ }
+ destinationGeoLayer, destinationGeoCode := "", ""
+ if destinationGeo != "" {
+ parts := strings.SplitN(destinationGeo, ":", 2)
+ if len(parts) == 2 {
+ destinationGeoLayer, destinationGeoCode = parts[0], parts[1]
+ }
+ }
+ passengerAddressGeoLayer, passengerAddressGeoCode := "", ""
+ if passengerAddressGeo != "" {
+ parts := strings.SplitN(passengerAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ passengerAddressGeoLayer, passengerAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ histDepartureGeoLayer, histDepartureGeoCode := "", ""
+ if histDepartureGeo != "" {
+ parts := strings.SplitN(histDepartureGeo, ":", 2)
+ if len(parts) == 2 {
+ histDepartureGeoLayer, histDepartureGeoCode = parts[0], parts[1]
+ }
+ }
+ histDestinationGeoLayer, histDestinationGeoCode := "", ""
+ if histDestinationGeo != "" {
+ parts := strings.SplitN(histDestinationGeo, ":", 2)
+ if len(parts) == 2 {
+ histDestinationGeoLayer, histDestinationGeoCode = parts[0], parts[1]
+ }
+ }
+ histPassengerAddressGeoLayer, histPassengerAddressGeoCode := "", ""
+ if histPassengerAddressGeo != "" {
+ parts := strings.SplitN(histPassengerAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ histPassengerAddressGeoLayer, histPassengerAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ // Parse driver address geography parameter
+ driverAddressGeoLayer, driverAddressGeoCode := "", ""
+ if driverAddressGeo != "" {
+ parts := strings.SplitN(driverAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ driverAddressGeoLayer, driverAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ result, err := h.applicationHandler.GetSolidarityTransportOverview(r.Context(), status, driverID, startDate, endDate, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode, histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeoLayer, histDepartureGeoCode, histDestinationGeoLayer, histDestinationGeoCode, histPassengerAddressGeoLayer, histPassengerAddressGeoCode, archivedFilter, driverAddressGeoLayer, driverAddressGeoCode)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving solidarity transport overview")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Build filters map for template (matching old handler format)
+ filters := map[string]any{
+ "tab": tab,
+ "date_start": startDate,
+ "date_end": endDate,
+ "status": status,
+ "driver_id": driverID,
+ "departure_geo": departureGeo,
+ "destination_geo": destinationGeo,
+ "passenger_address_geo": passengerAddressGeo,
+ "driver_address_geo": driverAddressGeo,
+ }
+
+ histFilters := map[string]any{
+ "tab": tab,
+ "date_start": histStartDate,
+ "date_end": histEndDate,
+ "status": histStatus,
+ "driver_id": histDriverID,
+ "departure_geo": histDepartureGeo,
+ "destination_geo": histDestinationGeo,
+ "passenger_address_geo": histPassengerAddressGeo,
+ }
+
+ // Enrich geography filters with names from geography service
+ var enrichedGeoFilters []map[string]string
+ if h.cfg.GetBool("geography.filters.enabled") {
+ geoFilters := h.cfg.Get("geography.filters.geographies")
+ if geoList, ok := geoFilters.([]any); ok {
+ for _, geoItem := range geoList {
+ if geoMap, ok := geoItem.(map[string]any); ok {
+ layer := ""
+ code := ""
+ if l, ok := geoMap["layer"].(string); ok {
+ layer = l
+ }
+ if c, ok := geoMap["code"].(string); ok {
+ code = c
+ }
+
+ enrichedGeo := map[string]string{
+ "layer": layer,
+ "code": code,
+ "name": code, // Default to code if name fetch fails
+ }
+
+ // Fetch name from geography service
+ if layer != "" && code != "" {
+ if geoFeature, err := h.services.Geography.Find(layer, code); err == nil {
+ if name := geoFeature.Properties.MustString("nom"); name != "" {
+ enrichedGeo["name"] = name
+ }
+ }
+ }
+
+ enrichedGeoFilters = append(enrichedGeoFilters, enrichedGeo)
+ }
+ }
+ }
+
+ // Sort by name
+ sort.Slice(enrichedGeoFilters, func(i, j int) bool {
+ return enrichedGeoFilters[i]["name"] < enrichedGeoFilters[j]["name"]
+ })
+ }
+
+ h.renderer.SolidarityTransportOverview(w, r, result.Accounts, result.AccountsMap, result.BeneficiariesMap, result.Bookings, result.BookingsHistory, filters, histFilters, tab, enrichedGeoFilters, archivedFilter)
+ }
+}
+
+func (h *Handler) SolidarityTransportCreateDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ // Parse form data
+ firstName := r.PostFormValue("first_name")
+ lastName := r.PostFormValue("last_name")
+ email := r.PostFormValue("email")
+ phoneNumber := r.PostFormValue("phone_number")
+ gender := r.PostFormValue("gender")
+
+ var birthdate *time.Time
+ if r.PostFormValue("birthdate") != "" {
+ if d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate")); err == nil {
+ birthdate = &d
+ }
+ }
+
+ // Parse JSON address field
+ var address any
+ if r.PostFormValue("address") != "" {
+ if err := json.Unmarshal([]byte(r.PostFormValue("address")), &address); err != nil {
+ log.Error().Err(err).Msg("failed parsing address JSON")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse JSON other_properties field
+ var otherProperties any
+ if r.PostFormValue("other_properties") != "" {
+ if err := json.Unmarshal([]byte(r.PostFormValue("other_properties")), &otherProperties); err != nil {
+ log.Error().Err(err).Msg("failed parsing other_properties JSON")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ driverID, err := h.applicationHandler.CreateSolidarityTransportDriver(
+ r.Context(),
+ firstName,
+ lastName,
+ email,
+ birthdate,
+ phoneNumber,
+ address,
+ gender,
+ otherProperties,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error creating solidarity transport driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", driverID), http.StatusFound)
+ return
+ }
+
+ h.renderer.SolidarityTransportCreateDriver(w, r)
+ }
+}
+
+func (h *Handler) SolidarityTransportUpdateDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ if r.Method == "POST" {
+ // Parse form data
+ firstName := r.PostFormValue("first_name")
+ lastName := r.PostFormValue("last_name")
+ email := r.PostFormValue("email")
+ phoneNumber := r.PostFormValue("phone_number")
+ gender := r.PostFormValue("gender")
+
+ var birthdate *time.Time
+ if r.PostFormValue("birthdate") != "" {
+ if d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate")); err == nil {
+ birthdate = &d
+ }
+ }
+
+ // Parse JSON address field
+ var address any
+ if r.PostFormValue("address") != "" {
+ if err := json.Unmarshal([]byte(r.PostFormValue("address")), &address); err != nil {
+ log.Error().Err(err).Msg("failed parsing address JSON")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse JSON other_properties field
+ var otherProperties any
+ if r.PostFormValue("other_properties") != "" {
+ if err := json.Unmarshal([]byte(r.PostFormValue("other_properties")), &otherProperties); err != nil {
+ log.Error().Err(err).Msg("failed parsing other_properties JSON")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ updatedDriverID, err := h.applicationHandler.UpdateSolidarityTransportDriver(
+ r.Context(),
+ driverID,
+ firstName,
+ lastName,
+ email,
+ birthdate,
+ phoneNumber,
+ address,
+ gender,
+ otherProperties,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error updating solidarity transport driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", updatedDriverID), http.StatusFound)
+ return
+ }
+
+ result, err := h.applicationHandler.GetSolidarityTransportDriver(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving solidarity transport driver for update")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.SolidarityTransportUpdateDriver(w, r, result.Driver)
+ }
+}
+
+func (h *Handler) SolidarityTransportDriverDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ // Extract tab parameter
+ tab := r.URL.Query().Get("tab")
+ if tab == "" {
+ tab = "documents" // Default tab
+ }
+
+ result, err := h.applicationHandler.GetSolidarityTransportDriverData(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving solidarity transport driver data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.SolidarityTransportDriverDisplay(w, r, result.Driver, result.Availabilities, result.Documents, result.Bookings, result.BeneficiariesMap, result.Stats, result.WalletBalance, tab)
+ }
+}
+
+func (h *Handler) SolidarityTransportAddAvailabilityHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ log.Error().Msg("Wrong method")
+ http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing availabilities form")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ // Parse form data
+ starttime := r.PostFormValue("starttime")
+ endtime := r.PostFormValue("endtime")
+
+ // Parse JSON address field
+ var address any
+ if r.PostFormValue("address") != "" {
+ if err := json.Unmarshal([]byte(r.PostFormValue("address")), &address); err != nil {
+ log.Error().Err(err).Msg("failed parsing address JSON")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Parse days
+ days := map[string]bool{
+ "monday": r.PostFormValue("days.monday") == "on",
+ "tuesday": r.PostFormValue("days.tuesday") == "on",
+ "wednesday": r.PostFormValue("days.wednesday") == "on",
+ "thursday": r.PostFormValue("days.thursday") == "on",
+ "friday": r.PostFormValue("days.friday") == "on",
+ "saturday": r.PostFormValue("days.saturday") == "on",
+ "sunday": r.PostFormValue("days.sunday") == "on",
+ }
+
+ err := h.applicationHandler.AddSolidarityTransportAvailability(r.Context(), driverID, starttime, endtime, address, days)
+ if err != nil {
+ log.Error().Err(err).Msg("error adding solidarity transport availability")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportArchiveDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ err := h.applicationHandler.ArchiveSolidarityTransportDriver(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error archiving solidarity transport driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportUnarchiveDriverHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ err := h.applicationHandler.UnarchiveSolidarityTransportDriver(r.Context(), driverID)
+ if err != nil {
+ log.Error().Err(err).Msg("error unarchiving solidarity transport driver")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportDriverDocumentsHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+
+ if err := r.ParseMultipartForm(100 * 1024 * 1024); err != nil { // 100 MB limit
+ log.Error().Err(err).Msg("error parsing multipart form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ documentType := r.FormValue("type")
+ documentName := r.FormValue("name")
+
+ file, header, err := r.FormFile("file-upload")
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving file")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ if err := h.applicationHandler.AddSolidarityTransportDriverDocument(r.Context(), driverID, file, header.Filename, header.Size, documentType, documentName); err != nil {
+ log.Error().Err(err).Msg("error adding solidarity transport driver document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportDocumentDownloadHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ document := vars["document"]
+
+ file, info, err := h.applicationHandler.GetSolidarityTransportDriverDocument(r.Context(), driverID, document)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving solidarity transport driver document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", info.ContentType)
+ if _, err = io.Copy(w, file); err != nil {
+ log.Error().Err(err).Msg("error copying file content")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+}
+
+func (h *Handler) SolidarityTransportDeleteAvailabilityHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ availabilityID := vars["availabilityid"]
+
+ err := h.applicationHandler.DeleteSolidarityTransportAvailability(r.Context(), driverID, availabilityID)
+ if err != nil {
+ log.Error().Err(err).Msg("error deleting solidarity transport availability")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ journeyID := vars["journeyid"]
+ // Preserve passengerid from either query parameter or form data
+ passengerID := r.URL.Query().Get("passengerid")
+ if passengerID == "" {
+ passengerID = r.FormValue("passengerid")
+ }
+ replacesBookingID := r.URL.Query().Get("replaces_booking_id")
+
+ if r.Method == "POST" {
+ // Parse form data
+ motivation := r.PostFormValue("motivation")
+ message := r.PostFormValue("message")
+ doNotSend := r.PostFormValue("do_not_send") == "on"
+ returnWaitingTimeMinutes := 0
+ if r.PostFormValue("return_waiting_time") != "" {
+ fmt.Sscanf(r.PostFormValue("return_waiting_time"), "%d", &returnWaitingTimeMinutes)
+ }
+
+ replacesBookingID := r.PostFormValue("replaces_booking_id")
+ bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(r.Context(), driverID, journeyID, passengerID, motivation, message, doNotSend, returnWaitingTimeMinutes, replacesBookingID)
+ if err != nil {
+ log.Error().Err(err).Msg("error creating solidarity transport journey booking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ log.Info().Str("booking_id", bookingID).Msg("Solidarity transport booking created successfully")
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/"), http.StatusFound)
+ return
+ }
+
+ // Get current user's group
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ log.Error().Msg("group not found in request context")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ group := g.(groupstorage.Group)
+
+ result, err := h.applicationHandler.GetSolidarityTransportJourneyData(r.Context(), driverID, journeyID, passengerID, group)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving solidarity transport journey data")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.SolidarityTransportDriverJourney(w, r, result.Journey, result.Driver, result.Passenger, result.Beneficiaries, result.PassengerWalletBalance, result.PricingResult, replacesBookingID)
+ }
+}
+
+func (h *Handler) SolidarityTransportDriverJourneyToggleNoreturnHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ journeyID := vars["journeyid"]
+
+ err := h.applicationHandler.ToggleSolidarityTransportJourneyNoreturn(r.Context(), driverID, journeyID)
+ if err != nil {
+ log.Error().Err(err).Msg("error toggling solidarity transport journey noreturn")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Preserve passengerid query parameter if present
+ passengerID := r.URL.Query().Get("passengerid")
+ if passengerID == "" {
+ passengerID = r.FormValue("passengerid")
+ }
+ redirectURL := fmt.Sprintf("/app/solidarity-transport/drivers/%s/journeys/%s", driverID, journeyID)
+ if passengerID != "" {
+ redirectURL += "?passengerid=" + passengerID
+ }
+ http.Redirect(w, r, redirectURL, http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ result, err := h.applicationHandler.GetSolidarityTransportBookingData(r.Context(), bookingID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving solidarity transport booking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // If booking is cancelled, search for replacement drivers
+ var replacementDrivers any
+ var replacementDriversMap map[string]mobilityaccountsstorage.Account
+ var replacementPricing map[string]map[string]interface{}
+ var replacementLocations map[string]string
+ if result.Booking.Status == "CANCELLED" {
+ // Initialize maps to avoid nil pointer in template
+ replacementDriversMap = make(map[string]mobilityaccountsstorage.Account)
+ replacementPricing = make(map[string]map[string]interface{})
+ replacementLocations = make(map[string]string)
+
+ searchResult, err := h.applicationHandler.SearchJourneys(
+ r.Context(),
+ result.Booking.Journey.PassengerPickupDate,
+ result.Booking.Journey.PassengerPickup,
+ result.Booking.Journey.PassengerDrop,
+ result.Booking.PassengerId,
+ result.Booking.DriverId, // Exclude the original driver
+ result.Booking.GroupId, // Exclude drivers with bookings in this group
+ nil, // options - use defaults
+ )
+ if err == nil {
+ replacementDrivers = searchResult.DriverJourneys
+ replacementDriversMap = searchResult.Drivers
+
+ // Calculate pricing for each replacement driver journey
+ for _, dj := range searchResult.DriverJourneys {
+ // Extract driver departure location
+ if dj.DriverDeparture != nil && dj.DriverDeparture.Serialized != "" {
+ var feature map[string]interface{}
+ if err := json.Unmarshal([]byte(dj.DriverDeparture.Serialized), &feature); err == nil {
+ if props, ok := feature["properties"].(map[string]interface{}); ok {
+ if name, ok := props["name"].(string); ok {
+ replacementLocations[dj.Id] = name
+ } else if label, ok := props["label"].(string); ok {
+ replacementLocations[dj.Id] = label
+ }
+ }
+ }
+ }
+
+ pricingResult, err := h.applicationHandler.CalculateSolidarityTransportPricing(r.Context(), dj, result.Booking.PassengerId)
+ if err == nil {
+ pricing := map[string]interface{}{
+ "passenger": map[string]interface{}{
+ "amount": pricingResult["passenger"].Amount,
+ "currency": pricingResult["passenger"].Currency,
+ },
+ "driver": map[string]interface{}{
+ "amount": pricingResult["driver"].Amount,
+ "currency": pricingResult["driver"].Currency,
+ },
+ }
+ replacementPricing[dj.Id] = pricing
+ }
+ }
+ }
+ }
+
+ h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance, replacementDrivers, replacementDriversMap, replacementPricing, replacementLocations)
+ }
+}
+
+func (h *Handler) SolidarityTransportCreateReplacementBookingHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ vars := mux.Vars(r)
+ oldBookingID := vars["bookingid"]
+
+ // Get the old booking to retrieve its data
+ oldBookingResult, err := h.applicationHandler.GetSolidarityTransportBookingData(r.Context(), oldBookingID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving old booking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Parse form data for new driver/journey
+ driverID := r.PostFormValue("driver_id")
+ journeyID := r.PostFormValue("journey_id")
+ message := r.PostFormValue("message")
+ doNotSend := r.PostFormValue("do_not_send") == "on"
+
+ // Use old booking's data
+ passengerID := oldBookingResult.Booking.PassengerId
+ motivation := ""
+ if oldBookingResult.Booking.Data != nil {
+ if m, ok := oldBookingResult.Booking.Data["motivation"].(string); ok {
+ motivation = m
+ }
+ }
+
+ // Get the new driver journey to retrieve journey information
+ driverJourneyResp, err := h.services.GRPC.SolidarityTransport.GetDriverJourney(r.Context(), &gen.GetDriverJourneyRequest{
+ DriverId: driverID,
+ JourneyId: journeyID,
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving new driver journey")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Calculate return waiting time based on journey type
+ returnWaitingTimeMinutes := 30 // Default for round trips
+ if driverJourneyResp.DriverJourney.Noreturn {
+ returnWaitingTimeMinutes = 0
+ }
+
+ // Create the replacement booking with pricing calculated in CreateSolidarityTransportJourneyBooking
+ bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(
+ r.Context(),
+ driverID,
+ journeyID,
+ passengerID,
+ motivation,
+ message, // message from form
+ doNotSend, // doNotSend from form checkbox
+ returnWaitingTimeMinutes,
+ oldBookingID,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error creating replacement booking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ log.Info().Str("booking_id", bookingID).Str("replaces", oldBookingID).Msg("Replacement booking created successfully")
+
+ // Redirect to the new booking
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/bookings/%s", bookingID), http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportDocumentDeleteHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ driverID := vars["driverid"]
+ document := vars["document"]
+
+ if err := h.applicationHandler.DeleteSolidarityTransportDriverDocument(r.Context(), driverID, document); err != nil {
+ log.Error().Err(err).Msg("error deleting solidarity transport driver document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s", driverID), http.StatusFound)
+ }
+}
+
+func (h *Handler) SolidarityTransportBookingStatusHTTPHandler(action string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ // Extract reason from form data for cancellations
+ reason := ""
+ if action == "cancel" && r.Method == "POST" {
+ r.ParseForm()
+ reason = r.PostFormValue("reason")
+ }
+
+ err := h.applicationHandler.UpdateSolidarityTransportBookingStatus(r.Context(), bookingID, action, reason, "", false)
+ if err != nil {
+ log.Error().Err(err).Msg("error updating solidarity transport booking status")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/bookings/%s", bookingID), http.StatusSeeOther)
+ }
+}
+
diff --git a/servers/web/application/support.go b/servers/web/application/support.go
new file mode 100644
index 0000000..c278abe
--- /dev/null
+++ b/servers/web/application/support.go
@@ -0,0 +1,40 @@
+package application
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) SupportSendHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ c := r.Context().Value(identification.ClaimsKey)
+ if c == nil {
+ log.Error().Msg("no current user claims")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ currentUserClaims := c.(map[string]any)
+
+ if r.Method == "POST" {
+ // Parse form data
+ comment := r.PostFormValue("comment")
+ userEmail := currentUserClaims["email"].(string)
+
+ err := h.applicationHandler.SendSupportMessage(r.Context(), comment, userEmail)
+ if err != nil {
+ log.Error().Err(err).Msg("error sending support message")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ return
+ }
+
+ // GET request - show form
+ comment := r.FormValue("comment")
+ h.renderer.SupportSend(w, r, comment, currentUserClaims)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/vehicles.go b/servers/web/application/vehicles.go
new file mode 100644
index 0000000..6d60c12
--- /dev/null
+++ b/servers/web/application/vehicles.go
@@ -0,0 +1,197 @@
+package application
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) VehiclesSearchHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+
+ // Extract parameters
+ beneficiaryID := r.FormValue("beneficiaryid")
+ startDateStr := r.FormValue("startdate")
+ endDateStr := r.FormValue("enddate")
+ startTimeStr := r.FormValue("starttime")
+ endTimeStr := r.FormValue("endtime")
+ vehicleType := r.FormValue("type")
+ automatic := r.FormValue("automatic") == "on"
+
+ // Default times if not provided
+ if startTimeStr == "" {
+ startTimeStr = "00:00"
+ }
+ if endTimeStr == "" {
+ endTimeStr = "23:59"
+ }
+
+ // Parse dates and times in Europe/Paris timezone
+ paris, _ := time.LoadLocation("Europe/Paris")
+ var startDate, endDate time.Time
+ if startDateStr != "" {
+ startDate, _ = time.ParseInLocation("2006-01-02 15:04", startDateStr+" "+startTimeStr, paris)
+ }
+ if endDateStr != "" {
+ endDate, _ = time.ParseInLocation("2006-01-02 15:04", endDateStr+" "+endTimeStr, paris)
+ }
+
+ // Call business logic
+ result, err := h.applicationHandler.SearchVehicles(r.Context(), beneficiaryID, startDate, endDate, vehicleType, automatic)
+ if err != nil {
+ log.Error().Err(err).Msg("error searching vehicles")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Render response
+ h.renderer.VehiclesSearch(w, r, result.Beneficiaries, result.Searched, result.Vehicles, result.Beneficiary,
+ result.StartDate, result.EndDate, result.MandatoryDocuments, result.FileTypesMap,
+ result.BeneficiaryDocuments, result.VehicleType, result.Automatic, result.VehicleTypes, result.Groups)
+ }
+}
+
+func (h *Handler) BookVehicleHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Extract URL parameters
+ vars := mux.Vars(r)
+ vehicleID := vars["vehicleid"]
+ beneficiaryID := vars["beneficiaryid"]
+
+ // Extract user context
+ currentUserToken := r.Context().Value(identification.IdtokenKey).(*oidc.IDToken)
+ currentUserClaims := r.Context().Value(identification.ClaimsKey).(map[string]any)
+ currentGroup := r.Context().Value(identification.GroupKey)
+
+ // Parse multipart form
+ if err := r.ParseMultipartForm(100 * 1024 * 1024); err != nil {
+ log.Error().Err(err).Msg("error parsing multipart form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Extract form data
+ startDateStr := r.FormValue("startdate")
+ endDateStr := r.FormValue("enddate")
+ startTimeStr := r.FormValue("starttime")
+ endTimeStr := r.FormValue("endtime")
+
+ // Default times if not provided
+ if startTimeStr == "" {
+ startTimeStr = "00:00"
+ }
+ if endTimeStr == "" {
+ endTimeStr = "23:59"
+ }
+
+ // Parse dates and times in Europe/Paris timezone
+ paris, _ := time.LoadLocation("Europe/Paris")
+ startDate, _ := time.ParseInLocation("2006-01-02 15:04", startDateStr+" "+startTimeStr, paris)
+ endDate, _ := time.ParseInLocation("2006-01-02 15:04", endDateStr+" "+endTimeStr, paris)
+
+ // Extract documents
+ documents := make(map[string]io.Reader)
+ documentHeaders := make(map[string]string)
+ existingDocs := make(map[string]string)
+
+ // Get mandatory document types from config
+ mandatoryDocTypes := h.cfg.GetStringSlice("modules.fleets.booking_documents.mandatory")
+
+ for _, docType := range mandatoryDocTypes {
+ existingFile := r.FormValue("type-" + docType)
+ if existingFile != "" {
+ existingDocs[docType] = existingFile
+ } else {
+ file, header, err := r.FormFile("doc-" + docType)
+ if err != nil {
+ log.Error().Err(err).Msg("missing required document: " + docType)
+ http.Error(w, "Document manquant : "+docType, http.StatusBadRequest)
+ return
+ }
+ documents[docType] = file
+ documentHeaders[docType] = header.Filename
+ }
+ }
+
+ // Call business logic
+ result, err := h.applicationHandler.BookVehicle(r.Context(), vehicleID, beneficiaryID, startDate, endDate,
+ documents, documentHeaders, existingDocs, currentUserToken.Subject, currentUserClaims, currentGroup)
+ if err != nil {
+ log.Error().Err(err).Msg("error booking vehicle")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Redirect to booking details
+ http.Redirect(w, r, fmt.Sprintf("/app/vehicles/bookings/%s", result.BookingID), http.StatusFound)
+ }
+}
+
+func (h *Handler) VehicleBookingDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ result, err := h.applicationHandler.GetVehicleBookingDetails(r.Context(), bookingID)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting booking details")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.VehicleBookingDisplay(w, r, result.Booking, result.Vehicle, result.Beneficiary,
+ result.Group, result.Documents, result.FileTypesMap)
+ }
+}
+
+func (h *Handler) VehiclesBookingsListHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get group from context
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ group := g.(storage.Group)
+
+ result, err := h.applicationHandler.GetVehicleBookingsList(r.Context(), group.ID)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting bookings list")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.VehicleBookingsList(w, r, result.Bookings, result.VehiclesMap, result.GroupsMap)
+ }
+}
+
+func (h *Handler) BookingDocumentDownloadHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+ document := vars["document"]
+
+ fileReader, contentType, err := h.applicationHandler.GetBookingDocument(r.Context(), bookingID, document)
+ if err != nil {
+ log.Error().Err(err).Msg("error getting booking document")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", contentType)
+ if _, err = io.Copy(w, fileReader); err != nil {
+ log.Error().Err(err).Msg("error writing document to response")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application/vehicles_management.go b/servers/web/application/vehicles_management.go
new file mode 100644
index 0000000..f57aef0
--- /dev/null
+++ b/servers/web/application/vehicles_management.go
@@ -0,0 +1,433 @@
+package application
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) VehiclesManagementOverviewHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Extract filter parameters from request
+ groupID := ""
+ if g := r.Context().Value(identification.GroupKey); g != nil {
+ group := g.(storage.Group)
+ groupID = group.ID
+ }
+
+ result, err := h.applicationHandler.GetVehiclesManagementOverview(r.Context(), groupID)
+ 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)
+ }
+}
+
+func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Extract filter parameters from request
+ groupID := ""
+ if g := r.Context().Value(identification.GroupKey); g != nil {
+ group := g.(storage.Group)
+ groupID = group.ID
+ }
+
+ // Extract filter parameters from query
+ status := r.URL.Query().Get("status")
+ dateStart := r.URL.Query().Get("date_start")
+ dateEnd := r.URL.Query().Get("date_end")
+
+ // Default to last month if no dates specified
+ if dateStart == "" {
+ dateStart = time.Now().AddDate(0, -1, 0).Format("2006-01-02")
+ }
+ if dateEnd == "" {
+ dateEnd = time.Now().Format("2006-01-02")
+ }
+
+ result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), groupID, status, dateStart, dateEnd)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving vehicles management bookings list")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Prepare filters map for template
+ filters := map[string]string{
+ "status": status,
+ "date_start": dateStart,
+ "date_end": dateEnd,
+ }
+
+ h.renderer.VehiclesManagementBookingsList(w, r, result.VehiclesMap, result.DriversMap, result.Bookings, result.CacheID, filters)
+ }
+}
+
+func (h *Handler) VehiclesFleetAddHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ // Get group from context
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Parse form data
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Parse address JSON
+ var address map[string]any
+ if addressStr := r.FormValue("address"); addressStr != "" {
+ if err := json.Unmarshal([]byte(addressStr), &address); err != nil {
+ log.Error().Err(err).Msg("error parsing address JSON")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ // Parse other_properties from form fields
+ otherProperties := make(map[string]any)
+ vehicleOptionalFields := h.cfg.Get("modules.fleets.vehicle_optional_fields")
+ log.Debug().Msgf("CREATE: vehicleOptionalFields type: %T, value: %+v", vehicleOptionalFields, vehicleOptionalFields)
+ if fields, ok := vehicleOptionalFields.([]map[string]any); ok {
+ log.Debug().Msgf("CREATE: Successfully cast to []map[string]any, length: %d", len(fields))
+ for _, field := range fields {
+ log.Debug().Any("field", field).Msg("vehicle other properties field handling")
+ if fieldName, ok := field["name"].(string); ok {
+ value := r.FormValue(fieldName)
+ log.Debug().Msgf("CREATE: Field %s: value='%s'", fieldName, value)
+ if value != "" {
+ otherProperties[fieldName] = value
+ }
+ }
+ }
+ } else {
+ log.Debug().Msg("CREATE: Failed to cast vehicleOptionalFields to []map[string]any")
+ }
+ log.Debug().Msgf("CREATE: Final otherProperties: %+v", otherProperties)
+
+ vehicleID, err := h.applicationHandler.CreateVehicle(r.Context(),
+ r.FormValue("name"),
+ r.FormValue("type"),
+ r.FormValue("informations"),
+ r.FormValue("licence_plate"),
+ r.FormValue("kilometers"),
+ r.FormValue("automatic") == "on",
+ address,
+ otherProperties,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error creating vehicle")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/fleet/%s", vehicleID), http.StatusFound)
+ return
+ }
+
+ // GET request - show form
+ vehicleTypes, err := h.applicationHandler.GetVehicleTypes(r.Context())
+ if err != nil {
+ log.Error().Err(err).Msg("error getting vehicle types")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.VehiclesFleetAdd(w, r, vehicleTypes)
+ }
+}
+
+func (h *Handler) VehiclesFleetDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ vehicleID := vars["vehicleid"]
+
+ result, err := h.applicationHandler.GetVehicleDisplay(r.Context(), vehicleID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving vehicle display")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.VehiclesFleetDisplay(w, r, result.Vehicle, result.Beneficiaries)
+ }
+}
+
+func (h *Handler) VehicleManagementBookingDisplayHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ if r.Method == "POST" {
+ // Parse form data
+ r.ParseForm()
+
+ // Extract date and time values
+ startDateStr := r.FormValue("startdate")
+ startTimeStr := r.FormValue("starttime")
+ endDateStr := r.FormValue("enddate")
+ endTimeStr := r.FormValue("endtime")
+
+ // Parse dates with times in Europe/Paris timezone
+ paris, _ := time.LoadLocation("Europe/Paris")
+
+ var startDate, endDate *time.Time
+ if startDateStr != "" {
+ if startTimeStr == "" {
+ startTimeStr = "00:00"
+ }
+ t, _ := time.ParseInLocation("2006-01-02 15:04", startDateStr+" "+startTimeStr, paris)
+ startDate = &t
+ }
+ if endDateStr != "" {
+ if endTimeStr == "" {
+ endTimeStr = "23:59"
+ }
+ t, _ := time.ParseInLocation("2006-01-02 15:04", endDateStr+" "+endTimeStr, paris)
+ endDate = &t
+ }
+
+ err := h.applicationHandler.UpdateBooking(r.Context(), bookingID,
+ startDate,
+ endDate,
+ r.FormValue("unavailablefrom"),
+ r.FormValue("unavailableto"),
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error updating booking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ result, err := h.applicationHandler.GetBookingDisplay(r.Context(), bookingID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving booking display")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.VehicleManagementBookingDisplay(w, r, result.Booking, result.Vehicle, result.Beneficiary, result.Group, result.Documents, result.FileTypesMap, result.Alternatives)
+ }
+}
+
+func (h *Handler) VehicleManagementBookingChangeVehicleHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ r.ParseForm()
+ newVehicle := r.FormValue("vehicle")
+
+ err := h.applicationHandler.ChangeBookingVehicle(r.Context(), bookingID, newVehicle)
+ if err != nil {
+ log.Error().Err(err).Msg("error changing booking vehicle")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/bookings/%s", bookingID), http.StatusFound)
+ }
+}
+
+func (h *Handler) VehiclesFleetMakeUnavailableHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Get context values
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ log.Error().Msg("no current group")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ u := r.Context().Value(identification.IdtokenKey)
+ if u == nil {
+ log.Error().Msg("no current user")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ currentUserToken := u.(*oidc.IDToken)
+
+ c := r.Context().Value(identification.ClaimsKey)
+ if c == nil {
+ log.Error().Msg("no current user claims")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ currentUserClaims := c.(map[string]any)
+
+ vars := mux.Vars(r)
+ vehicleID := vars["vehicleid"]
+
+ r.ParseForm()
+
+ err := h.applicationHandler.MakeVehicleUnavailable(r.Context(), vehicleID,
+ r.FormValue("unavailablefrom"),
+ r.FormValue("unavailableto"),
+ r.FormValue("comment"),
+ currentUserToken.Subject,
+ currentUserClaims,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error making vehicle unavailable")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/fleet/%s", vehicleID), http.StatusFound)
+ }
+}
+
+func (h *Handler) DeleteBookingHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ // Get form data
+ motif := r.FormValue("motif")
+
+ // Get current user information
+ 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.UnbookVehicle(r.Context(), bookingID, motif, currentUserToken.Subject, currentUserClaims, currentGroup)
+ if err != nil {
+ log.Error().Err(err).Msg("error unbooking vehicle")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/vehicles-management/bookings/", http.StatusSeeOther)
+ }
+}
+
+func (h *Handler) UnbookingVehicleHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingID := vars["bookingid"]
+
+ if r.Method == "POST" {
+ // Extract user and group info from context
+ currentUserToken := r.Context().Value(identification.IdtokenKey).(*oidc.IDToken)
+ currentUserClaims := r.Context().Value(identification.ClaimsKey).(map[string]any)
+ currentGroup := r.Context().Value(identification.GroupKey)
+
+ motif := r.FormValue("motif")
+
+ err := h.applicationHandler.UnbookVehicle(r.Context(), bookingID, motif,
+ currentUserToken.Subject, currentUserClaims, currentGroup)
+ if err != nil {
+ log.Error().Err(err).Msg("error unbooking vehicle")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/vehicles-management/", http.StatusFound)
+ return
+ }
+
+ // GET request - show form
+ booking, err := h.applicationHandler.GetBookingForUnbooking(r.Context(), bookingID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving booking for unbooking")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.UnbookingVehicle(w, r, booking)
+ }
+}
+
+func (h *Handler) VehiclesFleetUpdateHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ vehicleID := vars["vehicleid"]
+
+ if r.Method == "POST" {
+ if err := r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ // Parse address JSON
+ var address map[string]any
+ if addressStr := r.FormValue("address"); addressStr != "" {
+ if err := json.Unmarshal([]byte(addressStr), &address); err != nil {
+ log.Error().Err(err).Msg("error parsing address JSON")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ // Parse other_properties from form fields
+ otherProperties := make(map[string]any)
+ vehicleOptionalFields := h.cfg.Get("modules.fleets.vehicle_optional_fields")
+ log.Debug().Msgf("UPDATE: vehicleOptionalFields type: %T, value: %+v", vehicleOptionalFields, vehicleOptionalFields)
+ if fields, ok := vehicleOptionalFields.([]any); ok {
+ log.Debug().Msgf("UPDATE: Successfully cast to []map[string]any, length: %d", len(fields))
+ for _, f := range fields {
+ if field, ok := f.(map[string]any); ok {
+ log.Debug().Any("field", field).Msg("vehicle other properties field handling (update)")
+ if fieldName, ok := field["name"].(string); ok {
+ value := r.FormValue(fieldName)
+ log.Debug().Msgf("UPDATE: Field %s: value='%s'", fieldName, value)
+ if value != "" {
+ otherProperties[fieldName] = value
+ }
+ }
+ }
+ }
+ } else {
+ log.Debug().Msg("UPDATE: Failed to cast vehicleOptionalFields to []map[string]any")
+ }
+ log.Debug().Msgf("UPDATE: Final otherProperties: %+v", otherProperties)
+
+ updatedVehicleID, err := h.applicationHandler.UpdateVehicle(r.Context(), vehicleID,
+ r.FormValue("name"),
+ r.FormValue("type"),
+ r.FormValue("informations"),
+ r.FormValue("licence_plate"),
+ r.FormValue("kilometers"),
+ r.FormValue("automatic") == "on",
+ address,
+ otherProperties,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("error updating vehicle")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, fmt.Sprintf("/app/vehicles-management/fleet/%s", updatedVehicleID), http.StatusFound)
+ return
+ }
+
+ // GET request - show form
+ result, err := h.applicationHandler.GetVehicleForUpdate(r.Context(), vehicleID)
+ if err != nil {
+ log.Error().Err(err).Msg("error retrieving vehicle for update")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ h.renderer.VehiclesFleetUpdate(w, r, result.Vehicle, result.VehicleTypes)
+ }
+}
diff --git a/servers/web/application/wallets.go b/servers/web/application/wallets.go
new file mode 100644
index 0000000..55ae5ed
--- /dev/null
+++ b/servers/web/application/wallets.go
@@ -0,0 +1,58 @@
+package application
+
+import (
+ "net/http"
+ "net/url"
+ "strconv"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) CreditWalletHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ userid := vars["userid"]
+
+ if r.Method != "POST" {
+ http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ r.ParseForm()
+
+ amountStr := r.FormValue("amount")
+ paymentMethod := r.FormValue("payment_method")
+ description := r.FormValue("description")
+
+ amount, err := strconv.ParseFloat(amountStr, 64)
+ if err != nil {
+ log.Error().Err(err).Msg("could not read amount")
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ if paymentMethod == "" {
+ paymentMethod = "Paiement en espèce (MMS)"
+ }
+
+ err = h.applicationHandler.CreditWallet(r.Context(), userid, amount, paymentMethod, description)
+ if err != nil {
+ log.Error().Err(err).Msg("could not credit wallet")
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ refererURL, err := url.Parse(r.Referer())
+ if err != nil {
+ http.Redirect(w, r, r.Referer(), http.StatusFound)
+ return
+ }
+
+ query := refererURL.Query()
+ query.Set("tab", "wallet")
+ refererURL.RawQuery = query.Encode()
+
+ http.Redirect(w, r, refererURL.String(), http.StatusFound)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/application_routes.go b/servers/web/application_routes.go
new file mode 100644
index 0000000..5053814
--- /dev/null
+++ b/servers/web/application_routes.go
@@ -0,0 +1,32 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupApplicationRoutes(r *mux.Router) {
+ application := r.PathPrefix("/app").Subrouter()
+
+ // Setup all application route groups
+ ws.setupDashboardRoutes(application)
+ setupMiscRoutes(application, ws.applicationHandler)
+ ws.setupDirectoryRoutes(application)
+ ws.setupGroupRoutes(application)
+ ws.setupBeneficiariesRoutes(application)
+ ws.setupMembersRoutes(application)
+ ws.setupWalletsRoutes(application)
+ ws.setupJourneysRoutes(application)
+ ws.setupSolidarityTransportRoutes(application)
+ ws.setupOrganizedCarpoolRoutes(application)
+ ws.setupVehiclesRoutes(application)
+ ws.setupVehiclesManagementRoutes(application)
+ ws.setupSMSRoutes(application)
+ ws.setupSupportRoutes(application)
+ ws.setupAgendaRoutes(application)
+ ws.setupGroupModuleRoutes(application)
+ ws.setupAdministrationRoutes(application)
+
+ // Apply middleware
+ application.Use(ws.idp.Middleware)
+ application.Use(ws.idp.GroupsMiddleware)
+}
\ No newline at end of file
diff --git a/handlers/auth/disconnect.go b/servers/web/auth/disconnect.go
similarity index 73%
rename from handlers/auth/disconnect.go
rename to servers/web/auth/disconnect.go
index be3cc25..a224d4a 100644
--- a/handlers/auth/disconnect.go
+++ b/servers/web/auth/disconnect.go
@@ -2,7 +2,7 @@ package auth
import "net/http"
-func (h *AuthHandler) Disconnect(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) Disconnect(w http.ResponseWriter, r *http.Request) {
session, err := h.idp.SessionsStore.Get(r, "parcoursmob_session")
if err == nil {
session.Options.MaxAge = -1
@@ -10,4 +10,4 @@ func (h *AuthHandler) Disconnect(w http.ResponseWriter, r *http.Request) {
}
http.Redirect(w, r, "/", http.StatusOK)
-}
+}
\ No newline at end of file
diff --git a/servers/web/auth/groups.go b/servers/web/auth/groups.go
new file mode 100644
index 0000000..a240a10
--- /dev/null
+++ b/servers/web/auth/groups.go
@@ -0,0 +1,58 @@
+package auth
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) Groups(w http.ResponseWriter, r *http.Request) {
+ session, _ := h.idp.SessionsStore.Get(r, "parcoursmob_session")
+
+ if r.Method == "POST" {
+ r.ParseForm()
+
+ groupid := r.FormValue("group")
+
+ session.Values["organization"] = groupid
+ session.Save(r, w)
+
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ return
+ }
+
+ tokenstring, ok := session.Values["idtoken"]
+ if !ok {
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ return
+ }
+
+ idtoken, err := h.idp.TokenVerifier.Verify(context.Background(), tokenstring.(string))
+ if err != nil {
+ delete(session.Values, "idtoken")
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ return
+ }
+
+ result, err := h.applicationHandler.GetUserGroups(idtoken)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get user groups")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ var groupsresponse = []any{}
+ for _, group := range result.Groups {
+ groupsresponse = append(groupsresponse, group)
+ }
+
+ h.renderer.AuthGroups(w, r, groupsresponse)
+}
+
+func (h *Handler) GroupSwitch(w http.ResponseWriter, r *http.Request) {
+ session, _ := h.idp.SessionsStore.Get(r, "parcoursmob_session")
+ delete(session.Values, "organization")
+ session.Save(r, w)
+ http.Redirect(w, r, "/app/", http.StatusFound)
+}
\ No newline at end of file
diff --git a/servers/web/auth/handler.go b/servers/web/auth/handler.go
new file mode 100644
index 0000000..1c61914
--- /dev/null
+++ b/servers/web/auth/handler.go
@@ -0,0 +1,30 @@
+package auth
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-apps/parcoursmob/renderer"
+ "github.com/spf13/viper"
+)
+
+type Handler struct {
+ config *viper.Viper
+ applicationHandler *application.ApplicationHandler
+ idp *identification.IdentificationProvider
+ renderer *renderer.Renderer
+}
+
+func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer) *Handler {
+ return &Handler{
+ config: cfg,
+ applicationHandler: applicationHandler,
+ idp: idp,
+ renderer: renderer,
+ }
+}
+
+func (h *Handler) NotFound(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+}
\ No newline at end of file
diff --git a/servers/web/auth/lost_password.go b/servers/web/auth/lost_password.go
new file mode 100644
index 0000000..de5d9a7
--- /dev/null
+++ b/servers/web/auth/lost_password.go
@@ -0,0 +1,52 @@
+package auth
+
+import (
+ "net/http"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) LostPasswordInit(w http.ResponseWriter, r *http.Request) {
+ if r.Method == "POST" {
+ r.ParseForm()
+ email := r.FormValue("email")
+ if email != "" {
+ _, err := h.applicationHandler.InitiateLostPassword(email)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to initiate password reset")
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ return
+ }
+
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ }
+ }
+ h.renderer.LostPasswordInit(w, r)
+}
+
+func (h *Handler) LostPasswordRecover(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+
+ key := r.FormValue("key")
+ recover, err := h.applicationHandler.GetPasswordRecoveryData(key)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get password recovery data")
+ h.renderer.LostPasswordRecoverKO(w, r, key)
+ return
+ }
+
+ if r.Method == "POST" {
+ newpassword := r.FormValue("password")
+ _, err := h.applicationHandler.RecoverLostPassword(key, newpassword)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to recover password")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ return
+ }
+
+ h.renderer.LostPasswordRecover(w, r, recover)
+}
\ No newline at end of file
diff --git a/servers/web/auth/onboarding.go b/servers/web/auth/onboarding.go
new file mode 100644
index 0000000..47f0b7a
--- /dev/null
+++ b/servers/web/auth/onboarding.go
@@ -0,0 +1,37 @@
+package auth
+
+import (
+ "net/http"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) Onboarding(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+
+ key := r.FormValue("key")
+ onboarding, err := h.applicationHandler.GetOnboardingData(key)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get onboarding data")
+ h.renderer.AuthOnboardingKO(w, r, key)
+ return
+ }
+
+ if r.Method == "POST" {
+ firstName := r.FormValue("first_name")
+ lastName := r.FormValue("last_name")
+ password := r.FormValue("password")
+
+ _, err := h.applicationHandler.CompleteOnboarding(key, password, firstName, lastName)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to complete onboarding")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ http.Redirect(w, r, "/app/", http.StatusFound)
+ return
+ }
+
+ h.renderer.AuthOnboarding(w, r, key, onboarding)
+}
\ No newline at end of file
diff --git a/servers/web/auth_routes.go b/servers/web/auth_routes.go
new file mode 100644
index 0000000..ad8d308
--- /dev/null
+++ b/servers/web/auth_routes.go
@@ -0,0 +1,14 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupAuthRoutes(r *mux.Router) {
+ r.HandleFunc("/auth/onboarding", ws.authHandler.Onboarding)
+ r.HandleFunc("/auth/disconnect", ws.authHandler.Disconnect)
+ r.HandleFunc("/auth/lost-password", ws.authHandler.LostPasswordInit)
+ r.HandleFunc("/auth/lost-password/recover", ws.authHandler.LostPasswordRecover)
+ r.HandleFunc("/auth/groups/", ws.authHandler.Groups)
+ r.HandleFunc("/auth/groups/switch", ws.authHandler.GroupSwitch)
+}
\ No newline at end of file
diff --git a/servers/web/calendars_routes.go b/servers/web/calendars_routes.go
new file mode 100644
index 0000000..781ca6f
--- /dev/null
+++ b/servers/web/calendars_routes.go
@@ -0,0 +1,11 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupCalendarsRoutes(r *mux.Router) {
+ calendars_router := r.PathPrefix("/api/calendars").Subrouter()
+ calendars_router.HandleFunc("/global.ics", ws.webAPIHandler.CalendarGlobal)
+ calendars_router.HandleFunc("/organizations/{groupid}.ics", ws.webAPIHandler.CalendarOrganizations)
+}
\ No newline at end of file
diff --git a/servers/web/exports/agenda.go b/servers/web/exports/agenda.go
new file mode 100644
index 0000000..ac83bf0
--- /dev/null
+++ b/servers/web/exports/agenda.go
@@ -0,0 +1,47 @@
+package exports
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) AllAgendaEvents(w http.ResponseWriter, r *http.Request) {
+ result, err := h.applicationHandler.ExportAllAgendaEvents()
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to export all agenda events")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ defer func() {
+ if err := result.ExcelFile.Close(); err != nil {
+ log.Error().Err(err).Msg("Failed to close Excel file")
+ }
+ }()
+
+ h.writeFileResponse(w, "Agenda_Events.xlsx")
+ result.ExcelFile.Write(w)
+}
+
+func (h *Handler) SingleAgendaEvent(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ eventID := vars["eventid"]
+
+ result, err := h.applicationHandler.ExportSingleAgendaEvent(eventID)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to export single agenda event")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ defer func() {
+ if err := result.ExcelFile.Close(); err != nil {
+ log.Error().Err(err).Msg("Failed to close Excel file")
+ }
+ }()
+
+ h.writeFileResponse(w, "Event_"+eventID+".xlsx")
+ result.ExcelFile.Write(w)
+}
\ No newline at end of file
diff --git a/servers/web/exports/fleets.go b/servers/web/exports/fleets.go
new file mode 100644
index 0000000..9355740
--- /dev/null
+++ b/servers/web/exports/fleets.go
@@ -0,0 +1,66 @@
+package exports
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) AllFleetBookings(w http.ResponseWriter, r *http.Request) {
+ // Extract filter parameters from query
+ status := r.URL.Query().Get("status")
+ dateStart := r.URL.Query().Get("date_start")
+ dateEnd := r.URL.Query().Get("date_end")
+
+ // Get bookings using the admin stats function (all bookings across all groups)
+ result, err := h.applicationHandler.GetBookingsStats(status, dateStart, dateEnd)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get all vehicle bookings for export")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Convert vehicles map to the format expected by VehicleBookings renderer
+ vehiclesMap := make(map[string]interface{})
+ for k, v := range result.Vehicles {
+ vehiclesMap[k] = v
+ }
+
+ // Convert beneficiaries map to the format expected by VehicleBookings renderer
+ driversMap := make(map[string]interface{})
+ for k, v := range result.BeneficiariesMap {
+ driversMap[k] = v
+ }
+
+ // Render to Excel
+ h.renderer.XLSX.VehicleBookingsAdmin(w, result.Bookings, vehiclesMap, driversMap, result.Groups)
+}
+
+func (h *Handler) FleetBookingsInGroup(w http.ResponseWriter, r *http.Request) {
+ // Get current group from context (set by middleware)
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ log.Error().Msg("No group found in context")
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ group := g.(groupsstorage.Group)
+
+ // Extract filter parameters from query
+ status := r.URL.Query().Get("status")
+ 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)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get vehicle bookings for export")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Render to Excel
+ h.renderer.XLSX.VehicleBookings(w, result.Bookings, result.VehiclesMap, result.DriversMap)
+}
+
diff --git a/servers/web/exports/handler.go b/servers/web/exports/handler.go
new file mode 100644
index 0000000..0febe56
--- /dev/null
+++ b/servers/web/exports/handler.go
@@ -0,0 +1,33 @@
+package exports
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ "git.coopgo.io/coopgo-apps/parcoursmob/renderer"
+ "github.com/spf13/viper"
+)
+
+type Handler struct {
+ config *viper.Viper
+ applicationHandler *application.ApplicationHandler
+ idp *identification.IdentificationProvider
+ renderer *renderer.Renderer
+}
+
+func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer) *Handler {
+ return &Handler{
+ config: cfg,
+ applicationHandler: applicationHandler,
+ idp: idp,
+ renderer: renderer,
+ }
+}
+
+func (h *Handler) writeFileResponse(w http.ResponseWriter, filename string) {
+ w.Header().Set("Content-Type", "application/octet-stream")
+ w.Header().Set("Content-Disposition", "attachment; filename="+filename)
+ w.Header().Set("Content-Transfer-Encoding", "binary")
+ w.Header().Set("Expires", "0")
+}
\ No newline at end of file
diff --git a/servers/web/exports/organized-carpool.go b/servers/web/exports/organized-carpool.go
new file mode 100644
index 0000000..5060c70
--- /dev/null
+++ b/servers/web/exports/organized-carpool.go
@@ -0,0 +1,99 @@
+package exports
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) OrganizedCarpoolBookings(w http.ResponseWriter, r *http.Request) {
+ // Parse query parameters
+ var startDate, endDate *time.Time
+
+ if startDateStr := r.URL.Query().Get("start_date"); startDateStr != "" {
+ if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil {
+ startDate = &parsed
+ }
+ }
+
+ if endDateStr := r.URL.Query().Get("end_date"); endDateStr != "" {
+ if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil {
+ endDate = &parsed
+ }
+ }
+
+ status := r.URL.Query().Get("status")
+ driverID := r.URL.Query().Get("driver_id")
+
+ // Parse geography parameters
+ departureGeo := r.URL.Query().Get("departure_geo")
+ destinationGeo := r.URL.Query().Get("destination_geo")
+ passengerAddressGeo := r.URL.Query().Get("passenger_address_geo")
+
+ departureGeoLayer, departureGeoCode := "", ""
+ if departureGeo != "" {
+ parts := strings.SplitN(departureGeo, ":", 2)
+ if len(parts) == 2 {
+ departureGeoLayer, departureGeoCode = parts[0], parts[1]
+ }
+ }
+ destinationGeoLayer, destinationGeoCode := "", ""
+ if destinationGeo != "" {
+ parts := strings.SplitN(destinationGeo, ":", 2)
+ if len(parts) == 2 {
+ destinationGeoLayer, destinationGeoCode = parts[0], parts[1]
+ }
+ }
+ passengerAddressGeoLayer, passengerAddressGeoCode := "", ""
+ if passengerAddressGeo != "" {
+ parts := strings.SplitN(passengerAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ passengerAddressGeoLayer, passengerAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ // Get bookings data
+ result, err := h.applicationHandler.GetOrganizedCarpoolBookings(r.Context(), startDate, endDate, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get organized carpool bookings")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Render to Excel
+ h.renderer.XLSX.OrganizedCarpoolBookings(w, result)
+}
+
+func (h *Handler) OrganizedCarpoolDrivers(w http.ResponseWriter, r *http.Request) {
+ // Parse query parameters - same as web page
+ archivedFilter := r.URL.Query().Get("archived") == "true"
+ driverAddressGeo := r.URL.Query().Get("driver_address_geo")
+
+ // Parse driver address geography parameter
+ driverAddressGeoLayer, driverAddressGeoCode := "", ""
+ if driverAddressGeo != "" {
+ parts := strings.SplitN(driverAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ driverAddressGeoLayer, driverAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ // Get drivers data through overview (same as web page)
+ result, err := h.applicationHandler.GetOrganizedCarpoolOverview(
+ r.Context(),
+ "", "", "", "", "", "", "", "", "", "", // booking filters (empty)
+ "", "", "", "", "", "", "", "", "", "", // history booking filters (empty)
+ archivedFilter,
+ driverAddressGeoLayer, driverAddressGeoCode,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get organized carpool drivers")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Render to Excel
+ h.renderer.XLSX.OrganizedCarpoolDrivers(w, result)
+}
diff --git a/servers/web/exports/solidarity-transport.go b/servers/web/exports/solidarity-transport.go
new file mode 100644
index 0000000..e6fe672
--- /dev/null
+++ b/servers/web/exports/solidarity-transport.go
@@ -0,0 +1,99 @@
+package exports
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) SolidarityTransportBookings(w http.ResponseWriter, r *http.Request) {
+ // Parse query parameters
+ var startDate, endDate *time.Time
+
+ if startDateStr := r.URL.Query().Get("start_date"); startDateStr != "" {
+ if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil {
+ startDate = &parsed
+ }
+ }
+
+ if endDateStr := r.URL.Query().Get("end_date"); endDateStr != "" {
+ if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil {
+ endDate = &parsed
+ }
+ }
+
+ status := r.URL.Query().Get("status")
+ driverID := r.URL.Query().Get("driver_id")
+
+ // Parse geography parameters
+ departureGeo := r.URL.Query().Get("departure_geo")
+ destinationGeo := r.URL.Query().Get("destination_geo")
+ passengerAddressGeo := r.URL.Query().Get("passenger_address_geo")
+
+ departureGeoLayer, departureGeoCode := "", ""
+ if departureGeo != "" {
+ parts := strings.SplitN(departureGeo, ":", 2)
+ if len(parts) == 2 {
+ departureGeoLayer, departureGeoCode = parts[0], parts[1]
+ }
+ }
+ destinationGeoLayer, destinationGeoCode := "", ""
+ if destinationGeo != "" {
+ parts := strings.SplitN(destinationGeo, ":", 2)
+ if len(parts) == 2 {
+ destinationGeoLayer, destinationGeoCode = parts[0], parts[1]
+ }
+ }
+ passengerAddressGeoLayer, passengerAddressGeoCode := "", ""
+ if passengerAddressGeo != "" {
+ parts := strings.SplitN(passengerAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ passengerAddressGeoLayer, passengerAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ // Get bookings data
+ result, err := h.applicationHandler.GetSolidarityTransportBookings(r.Context(), startDate, endDate, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get solidarity transport bookings")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Render to Excel
+ h.renderer.XLSX.SolidarityTransportBookings(w, result)
+}
+
+func (h *Handler) SolidarityTransportDrivers(w http.ResponseWriter, r *http.Request) {
+ // Parse query parameters - same as web page
+ archivedFilter := r.URL.Query().Get("archived") == "true"
+ driverAddressGeo := r.URL.Query().Get("driver_address_geo")
+
+ // Parse driver address geography parameter
+ driverAddressGeoLayer, driverAddressGeoCode := "", ""
+ if driverAddressGeo != "" {
+ parts := strings.SplitN(driverAddressGeo, ":", 2)
+ if len(parts) == 2 {
+ driverAddressGeoLayer, driverAddressGeoCode = parts[0], parts[1]
+ }
+ }
+
+ // Get drivers data through overview (same as web page)
+ result, err := h.applicationHandler.GetSolidarityTransportOverview(
+ r.Context(),
+ "", "", "", "", "", "", "", "", "", "", // booking filters (empty)
+ "", "", "", "", "", "", "", "", "", "", // history booking filters (empty)
+ archivedFilter,
+ driverAddressGeoLayer, driverAddressGeoCode,
+ )
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to get solidarity transport drivers")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Render to Excel
+ h.renderer.XLSX.SolidarityTransportDrivers(w, result)
+}
diff --git a/servers/web/exports_routes.go b/servers/web/exports_routes.go
new file mode 100644
index 0000000..50c6005
--- /dev/null
+++ b/servers/web/exports_routes.go
@@ -0,0 +1,20 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupExportsRoutes(r *mux.Router) {
+ export := r.PathPrefix("/exports").Subrouter()
+ export.HandleFunc("/fleets/bookings/all_bookings.xlsx", ws.exportsHandler.AllFleetBookings)
+ export.HandleFunc("/fleets/bookings/group_bookings.xlsx", ws.exportsHandler.FleetBookingsInGroup)
+ export.HandleFunc("/agenda/subscriptions", ws.exportsHandler.AllAgendaEvents)
+ export.HandleFunc("/agenda/{eventid}", ws.exportsHandler.SingleAgendaEvent)
+ export.HandleFunc("/solidarity-transport/bookings.xlsx", ws.exportsHandler.SolidarityTransportBookings)
+ export.HandleFunc("/solidarity-transport/drivers.xlsx", ws.exportsHandler.SolidarityTransportDrivers)
+ export.HandleFunc("/organized-carpool/bookings.xlsx", ws.exportsHandler.OrganizedCarpoolBookings)
+ export.HandleFunc("/organized-carpool/drivers.xlsx", ws.exportsHandler.OrganizedCarpoolDrivers)
+ export.Use(ws.idp.Middleware)
+ export.Use(ws.idp.GroupsMiddleware)
+}
+
diff --git a/servers/web/external/handler.go b/servers/web/external/handler.go
new file mode 100644
index 0000000..c88e4c3
--- /dev/null
+++ b/servers/web/external/handler.go
@@ -0,0 +1,21 @@
+package external
+
+import (
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "github.com/spf13/viper"
+)
+
+type Handler struct {
+ cfg *viper.Viper
+ applicationHandler *application.ApplicationHandler
+ filestorage cache.FileStorage
+}
+
+func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, filestorage cache.FileStorage) *Handler {
+ return &Handler{
+ cfg: cfg,
+ applicationHandler: applicationHandler,
+ filestorage: filestorage,
+ }
+}
\ No newline at end of file
diff --git a/servers/web/external/solidarity_transport.go b/servers/web/external/solidarity_transport.go
new file mode 100644
index 0000000..696b3aa
--- /dev/null
+++ b/servers/web/external/solidarity_transport.go
@@ -0,0 +1,55 @@
+package external
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+ "git.coopgo.io/coopgo-apps/parcoursmob/renderer"
+)
+
+func (h *Handler) SolidarityTransportExternalBookingProposalHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ bookingId := vars["bookingid"]
+
+ // Get booking data
+ result, err := h.applicationHandler.GetSolidarityTransportBookingData(r.Context(), bookingId)
+ if err != nil {
+ log.Error().Err(err).Msg("could not get booking data")
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ // Handle POST form submission
+ if r.Method == "POST" {
+ if err = r.ParseForm(); err != nil {
+ log.Error().Err(err).Msg("error parsing form data")
+ }
+ message := r.FormValue("message")
+ action := r.FormValue("action")
+
+ if action != "" {
+ err := h.applicationHandler.UpdateSolidarityTransportBookingStatus(r.Context(), bookingId, action, "Refusé par le bénévole", message, true)
+ if err != nil {
+ log.Error().Err(err).Msg("update booking status issue")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Get updated booking data
+ result, err = h.applicationHandler.GetSolidarityTransportBookingData(r.Context(), bookingId)
+ if err != nil {
+ log.Error().Err(err).Msg("could not get updated booking data")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ }
+ }
+
+ // Create renderer temporarily to maintain functionality
+ templates_root := h.cfg.GetString("templates.root")
+ renderer := renderer.NewRenderer(h.cfg, templates_root, h.filestorage)
+ renderer.SolidarityTransportExternalBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger)
+ }
+}
\ No newline at end of file
diff --git a/servers/web/external_routes.go b/servers/web/external_routes.go
new file mode 100644
index 0000000..1e76def
--- /dev/null
+++ b/servers/web/external_routes.go
@@ -0,0 +1,10 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (webServer *WebServer) setupExternalRoutes(r *mux.Router) {
+ ext_router := r.PathPrefix("/ext").Subrouter()
+ ext_router.HandleFunc("/st/bp/{bookingid}", webServer.extHandler.SolidarityTransportExternalBookingProposalHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/protected_api/handler.go b/servers/web/protected_api/handler.go
new file mode 100644
index 0000000..33f56b2
--- /dev/null
+++ b/servers/web/protected_api/handler.go
@@ -0,0 +1,31 @@
+package protected_api
+
+import (
+ "net/http"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "github.com/spf13/viper"
+)
+
+type Handler struct {
+ cfg *viper.Viper
+ applicationHandler *application.ApplicationHandler
+}
+
+func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler) *Handler {
+ return &Handler{
+ cfg: cfg,
+ applicationHandler: applicationHandler,
+ }
+}
+
+func (h *Handler) ApiKeyMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ apiKey := r.Header.Get("X-API-Key")
+ if apiKey != h.cfg.GetString("services.api.api_key") {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
+}
\ No newline at end of file
diff --git a/servers/web/protected_api/users.go b/servers/web/protected_api/users.go
new file mode 100644
index 0000000..d7c02c0
--- /dev/null
+++ b/servers/web/protected_api/users.go
@@ -0,0 +1,39 @@
+package protected_api
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
+ "github.com/rs/zerolog/log"
+)
+
+func (h *Handler) UsersHTTPHandler() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ var user storage.Account
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&user)
+ if err != nil {
+ log.Error().Err(err).Msg("could not read account input")
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ result, err := h.applicationHandler.RegisterUser(r.Context(), user)
+ if err != nil {
+ log.Error().Err(err).Msg("error registering user")
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{
+ "user_id": result.UserID,
+ })
+ }
+}
\ No newline at end of file
diff --git a/servers/web/protected_api_routes.go b/servers/web/protected_api_routes.go
new file mode 100644
index 0000000..16c9f63
--- /dev/null
+++ b/servers/web/protected_api_routes.go
@@ -0,0 +1,12 @@
+package web
+
+import (
+ "github.com/gorilla/mux"
+)
+
+func (ws *WebServer) setupProtectedAPIRoutes(r *mux.Router) {
+ api_router := r.PathPrefix("/api").Subrouter()
+ protected_api_router := api_router.PathPrefix("/protected").Subrouter()
+ protected_api_router.Use(ws.protectedAPIHandler.ApiKeyMiddleware)
+ protected_api_router.HandleFunc("/users", ws.protectedAPIHandler.UsersHTTPHandler())
+}
\ No newline at end of file
diff --git a/servers/web/web.go b/servers/web/web.go
new file mode 100644
index 0000000..a08bb95
--- /dev/null
+++ b/servers/web/web.go
@@ -0,0 +1,141 @@
+package web
+
+import (
+ "errors"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/viper"
+
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
+ "git.coopgo.io/coopgo-apps/parcoursmob/renderer"
+ webapi "git.coopgo.io/coopgo-apps/parcoursmob/servers/web/api"
+ webapplication "git.coopgo.io/coopgo-apps/parcoursmob/servers/web/application"
+ webauth "git.coopgo.io/coopgo-apps/parcoursmob/servers/web/auth"
+ webexports "git.coopgo.io/coopgo-apps/parcoursmob/servers/web/exports"
+ webexternal "git.coopgo.io/coopgo-apps/parcoursmob/servers/web/external"
+ webprotectedapi "git.coopgo.io/coopgo-apps/parcoursmob/servers/web/protected_api"
+ "git.coopgo.io/coopgo-apps/parcoursmob/services"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
+ cacheStorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
+ "git.coopgo.io/coopgo-platform/groups-management/storage"
+ "github.com/coreos/go-oidc/v3/oidc"
+)
+
+type WebServer struct {
+ cfg *viper.Viper
+ services *services.ServicesHandler
+ renderer *renderer.Renderer
+ applicationHandler *application.ApplicationHandler
+ exportsHandler *webexports.Handler
+ authHandler *webauth.Handler
+ idp *identification.IdentificationProvider
+
+ // Web handler subpackages
+ appHandler *webapplication.Handler
+ extHandler *webexternal.Handler
+ protectedAPIHandler *webprotectedapi.Handler
+ webAPIHandler *webapi.Handler
+}
+
+func Run(cfg *viper.Viper, services *services.ServicesHandler, renderer *renderer.Renderer, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, cacheHandler cacheStorage.CacheHandler, filestorage cacheStorage.FileStorage) {
+ var (
+ address = cfg.GetString("server.web.listen")
+ service_name = cfg.GetString("service_name")
+ templates_public_dir = cfg.GetString("templates.public_dir")
+ dev_env = cfg.GetBool("dev_env")
+ )
+
+ webServer := &WebServer{
+ cfg: cfg,
+ services: services,
+ renderer: renderer,
+ applicationHandler: applicationHandler,
+ idp: idp,
+
+ // Initialize web handler subpackages
+ appHandler: webapplication.NewHandler(cfg, renderer, applicationHandler, idp, services),
+ authHandler: webauth.NewHandler(cfg, applicationHandler, idp, renderer),
+ exportsHandler: webexports.NewHandler(cfg, applicationHandler, idp, renderer),
+ extHandler: webexternal.NewHandler(cfg, applicationHandler, filestorage),
+ protectedAPIHandler: webprotectedapi.NewHandler(cfg, applicationHandler),
+ webAPIHandler: webapi.NewHandler(cfg, idp, applicationHandler, cacheHandler),
+ }
+
+ r := mux.NewRouter()
+
+ // Static files
+ r.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir(templates_public_dir))))
+
+ // Root redirect
+ r.HandleFunc("/", redirectApp)
+
+ // Development middleware
+ if dev_env {
+ r.Use(trackPage)
+ }
+
+ // Setup all route groups
+ webServer.setupAuthRoutes(r)
+ webServer.setupCalendarsRoutes(r)
+ webServer.setupExternalRoutes(r)
+ webServer.setupAPIRoutes(r)
+ webServer.setupProtectedAPIRoutes(r)
+ webServer.setupApplicationRoutes(r)
+ webServer.setupExportsRoutes(r)
+
+ srv := &http.Server{
+ Handler: r,
+ Addr: address,
+ WriteTimeout: 30 * time.Second,
+ ReadTimeout: 15 * time.Second,
+ }
+
+ log.Info().Str("service_name", service_name).Str("address", address).Msg("Running HTTP server")
+
+ err := srv.ListenAndServe()
+ log.Error().Err(err).Msg("Web server error")
+}
+
+func redirectApp(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "/app/", http.StatusFound)
+}
+
+func trackPage(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ log.Trace().Str("requested_uri", r.RequestURI).Msg("New request")
+ next.ServeHTTP(w, r.WithContext(r.Context()))
+ })
+}
+
+// Helper functions for extracting user and group information from request context
+func currentGroup(r *http.Request) (current_group storage.Group, err error) {
+ g := r.Context().Value(identification.GroupKey)
+ if g == nil {
+ return storage.Group{}, errors.New("current group not found")
+ }
+ current_group = g.(storage.Group)
+
+ return current_group, nil
+}
+
+func currentUser(r *http.Request) (current_user_token *oidc.IDToken, current_user_claims map[string]any, err error) {
+ // Get current user ID
+ u := r.Context().Value(identification.IdtokenKey)
+ if u == nil {
+ return nil, nil, errors.New("current user not found")
+ }
+ current_user_token = u.(*oidc.IDToken)
+
+ // Get current user claims
+ c := r.Context().Value(identification.ClaimsKey)
+ if c == nil {
+ return current_user_token, nil, errors.New("current user claims not found")
+ }
+ current_user_claims = c.(map[string]any)
+
+ return current_user_token, current_user_claims, nil
+}
+
diff --git a/services/agenda.go b/services/agenda.go
old mode 100644
new mode 100755
index b27e30f..bf9ca40
--- a/services/agenda.go
+++ b/services/agenda.go
@@ -1,8 +1,14 @@
package services
import (
+ "context"
+ "fmt"
+ "time"
+
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
+ "git.coopgo.io/coopgo-platform/agenda/storage"
"google.golang.org/grpc"
+ "google.golang.org/protobuf/types/known/timestamppb"
)
type AgendaService struct {
@@ -21,3 +27,39 @@ func NewAgendaService(dial string) (*AgendaService, error) {
AgendaClient: client,
}, nil
}
+
+func (s *ServicesHandler) GetAgendaEvents() ([]AgendaEvent, error) {
+ resp, err := s.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
+ Namespaces: []string{"parcoursmob_dispositifs"},
+ Mindate: timestamppb.New(time.Now().Add(-24 * time.Hour)),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ groups, err := s.GetGroupsMap()
+ if err != nil {
+ return nil, fmt.Errorf("error in groups request : %w", err)
+ }
+
+ events := []AgendaEvent{}
+
+ for _, e := range resp.Events {
+ newEvent := AgendaEvent{
+ Event: e.ToStorageType(),
+ }
+ for _, o := range e.Owners {
+ newEvent.OwnersGroups = append(newEvent.OwnersGroups, GroupsManagementGroup{Group: groups[o]})
+ }
+ events = append(events, newEvent)
+ }
+
+ return events, nil
+}
+
+// Enriched types
+
+type AgendaEvent struct {
+ OwnersGroups []GroupsManagementGroup
+ storage.Event
+}
diff --git a/services/carpool.go b/services/carpool.go
new file mode 100644
index 0000000..61af535
--- /dev/null
+++ b/services/carpool.go
@@ -0,0 +1,23 @@
+package services
+
+import (
+ "git.coopgo.io/coopgo-platform/carpool-service/servers/grpc/proto"
+ "google.golang.org/grpc"
+)
+
+type CarpoolService struct {
+ proto.CarpoolServiceClient
+}
+
+func NewCarpoolService(dial string) (*CarpoolService, error) {
+ conn, err := grpc.Dial(dial, grpc.WithInsecure())
+
+ client := proto.NewCarpoolServiceClient(conn)
+ if err != nil {
+ return nil, err
+ }
+
+ return &CarpoolService{
+ CarpoolServiceClient: client,
+ }, nil
+}
diff --git a/services/fleets.go b/services/fleets.go
old mode 100644
new mode 100755
index f0b9d89..724a62f
--- a/services/fleets.go
+++ b/services/fleets.go
@@ -4,9 +4,10 @@ import (
"context"
"sort"
- "git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
+ "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
"git.coopgo.io/coopgo-platform/fleets/storage"
+ "github.com/rs/zerolog/log"
"google.golang.org/grpc"
)
@@ -56,13 +57,17 @@ func (s *ServicesHandler) GetBookings() (bookings []storage.Booking, err error)
func (s *ServicesHandler) GetVehiclesMap() (vehicles map[string]storage.Vehicle, err error) {
vehicles = map[string]storage.Vehicle{}
- request := &fleets.GetVehiclesRequest{}
+ request := &fleets.GetVehiclesRequest{
+ Namespaces: []string{"parcoursmob"},
+ }
resp, err := s.GRPC.Fleets.GetVehicles(context.TODO(), request)
- if err == nil {
+ if err != nil {
+ log.Error().Err(err).Msg("")
+ } else {
for _, vehicle := range resp.Vehicles {
vehicles[vehicle.Id] = vehicle.ToStorageType()
}
}
- return
+ return vehicles, err
}
diff --git a/services/groupsmanagement.go b/services/groupsmanagement.go
old mode 100644
new mode 100755
index 08647b7..7fbdba7
--- a/services/groupsmanagement.go
+++ b/services/groupsmanagement.go
@@ -41,7 +41,7 @@ func (s *ServicesHandler) GetGroupsMap() (groups map[string]storage.Group, err e
return
}
-////////////////////////////////optimize the code//////////////////////////////////////
+// //////////////////////////////optimize the code//////////////////////////////////////
func (s *ServicesHandler) GetGroupsMemberMap(id string) (groups map[string]any, err error) {
groups = map[string]any{}
@@ -56,3 +56,23 @@ func (s *ServicesHandler) GetGroupsMemberMap(id string) (groups map[string]any,
}
return
}
+
+func (s *ServicesHandler) GetGroup(groupid string) (*storage.Group, error) {
+ groupresp, err := s.GRPC.GroupsManagement.GetGroup(context.TODO(), &groupsmanagement.GetGroupRequest{
+ Id: groupid,
+ Namespace: "parcoursmob_organizations",
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ group := groupresp.Group.ToStorageType()
+
+ return &group, nil
+}
+
+// Enriched types
+
+type GroupsManagementGroup struct {
+ storage.Group
+}
diff --git a/services/mobilityaccounts.go b/services/mobilityaccounts.go
old mode 100644
new mode 100755
index b31ad04..7c99263
--- a/services/mobilityaccounts.go
+++ b/services/mobilityaccounts.go
@@ -3,6 +3,7 @@ package services
import (
"context"
+ groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
"git.coopgo.io/coopgo-platform/mobility-accounts/storage"
"google.golang.org/grpc"
@@ -42,6 +43,22 @@ func (s *ServicesHandler) GetBeneficiaries() (accounts []storage.Account, err er
return
}
+func (s *ServicesHandler) GetBeneficiariesMap() (accounts map[string]storage.Account, err error) {
+ accounts = map[string]storage.Account{}
+ request := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: []string{"parcoursmob_beneficiaries"},
+ }
+ resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
+
+ if err == nil {
+ for _, v := range resp.Accounts {
+ accounts[v.Id] = v.ToStorageType()
+ }
+ }
+
+ return
+}
+
func (s *ServicesHandler) GetAccounts() (accounts []storage.Account, err error) {
accounts = []storage.Account{}
request := &mobilityaccounts.GetAccountsRequest{
@@ -58,3 +75,87 @@ func (s *ServicesHandler) GetAccounts() (accounts []storage.Account, err error)
return
}
+
+func (s *ServicesHandler) GetAccountsInNamespace(namespace string) (accounts []storage.Account, err error) {
+ accounts = []storage.Account{}
+ request := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: []string{namespace},
+ }
+ resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
+
+ if err == nil {
+ for _, v := range resp.Accounts {
+ a := v.ToStorageType()
+ accounts = append(accounts, a)
+ }
+ }
+
+ return
+}
+func (s *ServicesHandler) GetAccountsInNamespaceMap(namespace string) (accounts map[string]storage.Account, err error) {
+ accounts = map[string]storage.Account{}
+ request := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: []string{namespace},
+ }
+ resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
+
+ if err == nil {
+ for _, v := range resp.Accounts {
+ accounts[v.Id] = v.ToStorageType()
+ }
+ }
+
+ return
+}
+
+func (s *ServicesHandler) GetAccountsInNamespacesMap(namespaces []string) (accounts map[string]storage.Account, err error) {
+ accounts = map[string]storage.Account{}
+ request := &mobilityaccounts.GetAccountsRequest{
+ Namespaces: namespaces,
+ }
+ resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
+
+ if err == nil {
+ for _, v := range resp.Accounts {
+ accounts[v.Id] = v.ToStorageType()
+ }
+ }
+
+ return
+}
+
+func (s *ServicesHandler) GetAccount(id string) (account storage.Account, err error) {
+ request := &mobilityaccounts.GetAccountRequest{
+ Id: id,
+ }
+ resp, err := s.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
+ if err != nil {
+ return storage.Account{}, err
+ }
+
+ return resp.Account.ToStorageType(), nil
+}
+
+func (s *ServicesHandler) GetBeneficiariesInGroup(group groupstorage.Group) (accounts []storage.Account, err error) {
+ accounts = []storage.Account{}
+
+ if len(group.Members) == 0 {
+ return accounts, nil
+ }
+
+ request := &mobilityaccounts.GetAccountsBatchRequest{
+ Accountids: group.Members,
+ }
+
+ resp, err := s.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), request)
+ if err != nil {
+ return accounts, err
+ }
+
+ for _, account := range resp.Accounts {
+ a := account.ToStorageType()
+ accounts = append(accounts, a)
+ }
+
+ return accounts, nil
+}
diff --git a/services/savedsearch.go b/services/savedsearch.go
new file mode 100644
index 0000000..c0556c2
--- /dev/null
+++ b/services/savedsearch.go
@@ -0,0 +1,23 @@
+package services
+
+import (
+ "git.coopgo.io/coopgo-platform/saved-search/servers/grpc/proto/gen"
+ "google.golang.org/grpc"
+)
+
+type SavedSearchService struct {
+ gen.SavedSearchServiceClient
+}
+
+func NewSavedSearchService(dial string) (*SavedSearchService, error) {
+ conn, err := grpc.Dial(dial, grpc.WithInsecure())
+
+ client := gen.NewSavedSearchServiceClient(conn)
+ if err != nil {
+ return nil, err
+ }
+
+ return &SavedSearchService{
+ SavedSearchServiceClient: client,
+ }, nil
+}
\ No newline at end of file
diff --git a/services/services.go b/services/services.go
old mode 100644
new mode 100755
index 14b4890..9818b24
--- a/services/services.go
+++ b/services/services.go
@@ -2,56 +2,147 @@ package services
import (
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
+ carpoolservice "git.coopgo.io/coopgo-platform/carpool-service/servers/grpc/proto"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
+ "git.coopgo.io/coopgo-platform/geography/handlers/admin"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
+ multimodal "git.coopgo.io/coopgo-platform/multimodal-routing/handlers"
+ "git.coopgo.io/coopgo-platform/multimodal-routing/libs/transit/transitous"
+ "git.coopgo.io/coopgo-platform/payments/pricing"
+ "git.coopgo.io/coopgo-platform/routing-service"
+ savedsearch "git.coopgo.io/coopgo-platform/saved-search/servers/grpc/proto/gen"
+ "git.coopgo.io/coopgo-platform/sms"
+ solidaritytransport "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
+ "github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
type ServicesHandler struct {
- GRPC GRPCServices
+ GRPC GRPCServices
+ InteropCarpool *multimodal.CarpoolRoutingHandler
+ Routing routing.RoutingService
+ TransitRouting *transitous.Client
+ SMS *sms.SMSHandler
+ Pricing pricing.PricingService
+ Geography admin.AdminIndex
}
type GRPCServices struct {
- MobilityAccounts mobilityaccounts.MobilityAccountsClient
- GroupsManagement groupsmanagement.GroupsManagementClient
- Fleets fleets.FleetsClient
- Agenda agenda.AgendaClient
+ MobilityAccounts mobilityaccounts.MobilityAccountsClient
+ GroupsManagement groupsmanagement.GroupsManagementClient
+ Fleets fleets.FleetsClient
+ Agenda agenda.AgendaClient
+ SolidarityTransport solidaritytransport.SolidarityTransportClient
+ CarpoolService carpoolservice.CarpoolServiceClient
+ SavedSearch savedsearch.SavedSearchServiceClient
}
func NewServicesHandler(cfg *viper.Viper) (*ServicesHandler, error) {
var (
- mobilityAccountsDial = cfg.GetString("services.grpc.mobilityaccounts.dial")
- groupsManagementDial = cfg.GetString("services.grpc.groupsmanagement.dial")
- fleetsDial = cfg.GetString("services.grpc.fleets.dial")
- agendaDial = cfg.GetString("services.grpc.agenda.dial")
+ mobilityAccountsDial = cfg.GetString("services.grpc.mobilityaccounts.dial")
+ groupsManagementDial = cfg.GetString("services.grpc.groupsmanagement.dial")
+ fleetsDial = cfg.GetString("services.grpc.fleets.dial")
+ agendaDial = cfg.GetString("services.grpc.agenda.dial")
+ solidarityTransportDial = cfg.GetString("services.grpc.solidaritytransport.dial")
+ carpoolServiceDial = cfg.GetString("services.grpc.carpoolservice.dial")
+ savedSearchDial = cfg.GetString("services.grpc.savedsearch.dial")
+ routing_service_type = cfg.GetString("routing.type")
+ valhalla_base_url = cfg.GetString("routing.valhalla.base_url")
+
)
mobilityAccounts, err := NewMobilityAccountService(mobilityAccountsDial)
if err != nil {
+ log.Error().Err(err).Msg("Mobility Accounts service issue")
return nil, err
}
groupsManagement, err := NewGroupsManagementService(groupsManagementDial)
if err != nil {
+ log.Error().Err(err).Msg("Groups mgmt service issue")
return nil, err
}
fleetsSvc, err := NewFleetsService(fleetsDial)
if err != nil {
+ log.Error().Err(err).Msg("Fleets service issue")
return nil, err
}
agendaSvc, err := NewAgendaService(agendaDial)
if err != nil {
+ log.Error().Err(err).Msg("Agenda service issue")
+ return nil, err
+ }
+
+ solidarityTransportSvc, err := NewSolidarityTransportService(solidarityTransportDial)
+ if err != nil {
+ log.Error().Err(err).Msg("Solidarity Transport service issue")
+ return nil, err
+ }
+ carpoolSvc, err := NewCarpoolService(carpoolServiceDial)
+ if err != nil {
+ log.Error().Err(err).Msg("Carpool service service issue")
+ return nil, err
+ }
+
+ savedSearchSvc, err := NewSavedSearchService(savedSearchDial)
+ if err != nil {
+ log.Error().Err(err).Msg("Saved Search service issue")
+ return nil, err
+ }
+
+ routing, err := routing.NewRoutingService(routing_service_type, valhalla_base_url)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Could not initiate the routing service")
+ return nil, err
+ }
+
+ carpoolRouting, err := multimodal.NewCarpoolRoutingHandler(cfg.Sub("multimodal.modes.carpool"))
+ if err != nil {
+ log.Fatal().Err(err).Msg("Could not initiate the carpool routing service")
+ return nil, err
+ }
+
+ transitousRouting := transitous.NewClient(cfg.GetString("multimodal.modes.transit.transitous.server"))
+
+ pricing_type := cfg.GetString("payments.pricing.type")
+ if pricing_type == "" {
+ pricing_type = "mms43"
+ }
+ pricingService, err := pricing.NewPricingService(pricing_type)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Could not initiate the pricing service")
+ return nil, err
+ }
+
+ smsHandler, err := sms.NewSMSHandler(cfg.Sub("sms"))
+ if err != nil {
+ log.Fatal().Err(err).Msg("Could not initiate the SMS handler")
+ return nil, err
+ }
+
+ geography, err := admin.NewAdminIndex(cfg.Sub("geography"))
+ if err != nil {
+ log.Fatal().Err(err).Msg("could not initiate admin index")
return nil, err
}
return &ServicesHandler{
GRPC: GRPCServices{
- MobilityAccounts: mobilityAccounts,
- GroupsManagement: groupsManagement,
- Fleets: fleetsSvc,
- Agenda: agendaSvc,
+ MobilityAccounts: mobilityAccounts,
+ GroupsManagement: groupsManagement,
+ Fleets: fleetsSvc,
+ Agenda: agendaSvc,
+ SolidarityTransport: solidarityTransportSvc,
+ CarpoolService: carpoolSvc,
+ SavedSearch: savedSearchSvc,
},
+ Routing: routing,
+ InteropCarpool: carpoolRouting,
+ TransitRouting: transitousRouting,
+ SMS: smsHandler,
+ Pricing: pricingService,
+ Geography: geography,
}, nil
}
diff --git a/services/solidaritytransport.go b/services/solidaritytransport.go
new file mode 100644
index 0000000..59d9b00
--- /dev/null
+++ b/services/solidaritytransport.go
@@ -0,0 +1,23 @@
+package services
+
+import (
+ "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
+ "google.golang.org/grpc"
+)
+
+type SolidarityTransportService struct {
+ gen.SolidarityTransportClient
+}
+
+func NewSolidarityTransportService(dial string) (*SolidarityTransportService, error) {
+ conn, err := grpc.Dial(dial, grpc.WithInsecure())
+
+ client := gen.NewSolidarityTransportClient(conn)
+ if err != nil {
+ return nil, err
+ }
+
+ return &SolidarityTransportService{
+ SolidarityTransportClient: client,
+ }, nil
+}
diff --git a/themes b/themes
new file mode 160000
index 0000000..db4c226
--- /dev/null
+++ b/themes
@@ -0,0 +1 @@
+Subproject commit db4c22699eff6756851b0d3eff430b7bbe4c1c1b
diff --git a/utils/sorting/fleets.go b/utils/sorting/fleets.go
deleted file mode 100644
index c973585..0000000
--- a/utils/sorting/fleets.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package sorting
-
-import (
- "strings"
-
- fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
-)
-
-type VehiclesByLicencePlate []fleetsstorage.Vehicle
-
-func (a VehiclesByLicencePlate) Len() int { return len(a) }
-func (a VehiclesByLicencePlate) Less(i, j int) bool {
- return strings.Compare(a[i].Data["licence_plate"].(string), a[j].Data["licence_plate"].(string)) < 0
-}
-func (a VehiclesByLicencePlate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-
-type BookingsByStartdate []fleetsstorage.Booking
-
-func (a BookingsByStartdate) Len() int { return len(a) }
-func (a BookingsByStartdate) Less(i, j int) bool {
- return a[i].Startdate.Before(a[j].Startdate)
-}
-func (a BookingsByStartdate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }