From a60466d891d4bc38fe2e332eeaea34ec92ce1c46 Mon Sep 17 00:00:00 2001 From: Arnaud Delcasse Date: Wed, 25 Feb 2026 15:38:05 +0100 Subject: [PATCH] Beneficiaries export as XLSX --- renderer/xlsx/beneficiaries.go | 99 ++++++++++++++++++++++++++++ servers/web/exports/beneficiaries.go | 63 ++++++++++++++++++ servers/web/exports/handler.go | 5 +- servers/web/exports_routes.go | 1 + servers/web/web.go | 2 +- 5 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 renderer/xlsx/beneficiaries.go create mode 100644 servers/web/exports/beneficiaries.go diff --git a/renderer/xlsx/beneficiaries.go b/renderer/xlsx/beneficiaries.go new file mode 100644 index 0000000..aee6a51 --- /dev/null +++ b/renderer/xlsx/beneficiaries.go @@ -0,0 +1,99 @@ +package xlsx + +import ( + "fmt" + "net/http" + + "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/gender" + mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage" + "github.com/rs/zerolog/log" +) + +type BeneficiaryGeoInfo struct { + Commune string + EPCI string + Departement string + Region string +} + +func (r *XLSXRenderer) Beneficiaries(w http.ResponseWriter, accounts []mobilityaccountsstorage.Account, geoInfoMap map[string]BeneficiaryGeoInfo) { + spreadsheet := r.NewSpreadsheet("Bénéficiaires") + + // Build headers dynamically based on configuration + beneficiaryOptionalFields := r.Config.Get("modules.beneficiaries.profile_optional_fields") + beneficiaryFields := []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 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 + } + headers = append(headers, label) + } + } + } + } + + headers = append(headers, "Adresse", "Commune", "EPCI", "Département", "Région", "Archivé") + + spreadsheet.SetHeaders(headers) + + for _, account := range accounts { + row := []interface{}{} + + row = append(row, account.ID) + + for _, field := range beneficiaryFields { + value := getAccountFieldValue(account.Data, field) + if field == "gender" && value != "" { + value = gender.ISO5218ToString(value) + } + row = append(row, value) + } + + // Address + address := "" + if addr, ok := account.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) + + // Geographic info (Commune, EPCI, Département, Région) + geoInfo := geoInfoMap[account.ID] + row = append(row, geoInfo.Commune) + row = append(row, geoInfo.EPCI) + row = append(row, geoInfo.Departement) + row = append(row, geoInfo.Region) + + // Archived status + archived := "Non" + if archivedVal, ok := account.Data["archived"].(bool); ok && archivedVal { + archived = "Oui" + } + row = append(row, archived) + + spreadsheet.AddRow(row) + } + + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"export-beneficiaires.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/servers/web/exports/beneficiaries.go b/servers/web/exports/beneficiaries.go new file mode 100644 index 0000000..c0169e3 --- /dev/null +++ b/servers/web/exports/beneficiaries.go @@ -0,0 +1,63 @@ +package exports + +import ( + "encoding/json" + "net/http" + "strings" + + xlsxrenderer "git.coopgo.io/coopgo-apps/parcoursmob/renderer/xlsx" + "github.com/paulmach/orb/geojson" + "github.com/rs/zerolog/log" +) + +func (h *Handler) Beneficiaries(w http.ResponseWriter, r *http.Request) { + archivedFilter := r.URL.Query().Get("archived") == "true" + beneficiaryAddressGeo := r.URL.Query().Get("beneficiary_address_geo") + + addressGeoLayer, addressGeoCode := "", "" + if beneficiaryAddressGeo != "" { + parts := strings.SplitN(beneficiaryAddressGeo, ":", 2) + if len(parts) == 2 { + addressGeoLayer, addressGeoCode = parts[0], parts[1] + } + } + + result, err := h.applicationHandler.GetBeneficiaries(r.Context(), "", archivedFilter, addressGeoLayer, addressGeoCode) + if err != nil { + log.Error().Err(err).Msg("Failed to get beneficiaries") + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Resolve geographic layers (EPCI, Département, Région) for each beneficiary + geoInfoMap := map[string]xlsxrenderer.BeneficiaryGeoInfo{} + for _, account := range result.Accounts { + if addr, ok := account.Data["address"]; ok { + jsonAddr, err := json.Marshal(addr) + if err == nil { + addrFeature, err := geojson.UnmarshalFeature(jsonAddr) + if err == nil && addrFeature.Geometry != nil { + geo, err := h.services.Geography.GeoSearch(addrFeature) + if err == nil { + info := xlsxrenderer.BeneficiaryGeoInfo{} + if commune, ok := geo["communes"]; ok { + info.Commune = commune.Properties.MustString("nom") + } + if epci, ok := geo["epci"]; ok { + info.EPCI = epci.Properties.MustString("nom") + } + if dept, ok := geo["departements"]; ok { + info.Departement = dept.Properties.MustString("nom") + } + if region, ok := geo["regions"]; ok { + info.Region = region.Properties.MustString("nom") + } + geoInfoMap[account.ID] = info + } + } + } + } + } + + h.renderer.XLSX.Beneficiaries(w, result.Accounts, geoInfoMap) +} diff --git a/servers/web/exports/handler.go b/servers/web/exports/handler.go index 0febe56..bb3a060 100644 --- a/servers/web/exports/handler.go +++ b/servers/web/exports/handler.go @@ -6,6 +6,7 @@ import ( "git.coopgo.io/coopgo-apps/parcoursmob/core/application" "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification" "git.coopgo.io/coopgo-apps/parcoursmob/renderer" + "git.coopgo.io/coopgo-apps/parcoursmob/services" "github.com/spf13/viper" ) @@ -14,14 +15,16 @@ type Handler struct { applicationHandler *application.ApplicationHandler idp *identification.IdentificationProvider renderer *renderer.Renderer + services *services.ServicesHandler } -func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer) *Handler { +func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer, services *services.ServicesHandler) *Handler { return &Handler{ config: cfg, applicationHandler: applicationHandler, idp: idp, renderer: renderer, + services: services, } } diff --git a/servers/web/exports_routes.go b/servers/web/exports_routes.go index 50c6005..6dabd5c 100644 --- a/servers/web/exports_routes.go +++ b/servers/web/exports_routes.go @@ -14,6 +14,7 @@ func (ws *WebServer) setupExportsRoutes(r *mux.Router) { 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.HandleFunc("/beneficiaries/beneficiaries.xlsx", ws.exportsHandler.Beneficiaries) export.Use(ws.idp.Middleware) export.Use(ws.idp.GroupsMiddleware) } diff --git a/servers/web/web.go b/servers/web/web.go index e63269d..4ba4227 100644 --- a/servers/web/web.go +++ b/servers/web/web.go @@ -58,7 +58,7 @@ func Run(cfg *viper.Viper, services *services.ServicesHandler, renderer *rendere // 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), + exportsHandler: webexports.NewHandler(cfg, applicationHandler, idp, renderer, services), extHandler: webexternal.NewHandler(cfg, applicationHandler, filestorage), protectedAPIHandler: webprotectedapi.NewHandler(cfg, applicationHandler), webAPIHandler: webapi.NewHandler(cfg, idp, applicationHandler, cacheHandler),