lot of new functionalities

This commit is contained in:
Arnaud Delcasse
2025-10-14 18:11:13 +02:00
parent a6f70a6e85
commit d992a7984f
164 changed files with 15113 additions and 9442 deletions

38
servers/web/api/auth.go Normal file
View File

@@ -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)
}

48
servers/web/api/cache.go Normal file
View File

@@ -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
}

View File

@@ -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))
}

31
servers/web/api/export.go Normal file
View File

@@ -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)
}

32
servers/web/api/geo.go Normal file
View File

@@ -0,0 +1,32 @@
package api
import (
"encoding/json"
"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]
result, err := h.geoService.Autocomplete(text)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
j, err := json.Marshal(result.Features)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
}

View File

@@ -0,0 +1,37 @@
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)
geoService := geo.NewGeoService(cfg.GetString("geo.pelias.url"))
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)
}

View File

@@ -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)
}

View File

@@ -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)
}

14
servers/web/api_routes.go Normal file
View File

@@ -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)
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -0,0 +1,10 @@
package web
import (
"github.com/gorilla/mux"
)
func (ws *WebServer) setupDashboardRoutes(appRouter *mux.Router) {
// Dashboard
appRouter.HandleFunc("/", ws.appHandler.DashboardHTTPHandler())
}

View File

@@ -0,0 +1,9 @@
package web
import (
"github.com/gorilla/mux"
)
func (ws *WebServer) setupDirectoryRoutes(appRouter *mux.Router) {
appRouter.HandleFunc("/directory/", ws.appHandler.DirectoryHomeHTTPHandler())
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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")
}

View File

@@ -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())
}

View File

@@ -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
}

View File

@@ -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"))
}

View File

@@ -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())
}

View File

@@ -0,0 +1,30 @@
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"))
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,418 @@
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,
)
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,
)
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,
)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,636 @@
package application
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
"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"]
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"
returnWaitingTimeMinutes := 0
if r.PostFormValue("return_waiting_time") != "" {
fmt.Sscanf(r.PostFormValue("return_waiting_time"), "%d", &returnWaitingTimeMinutes)
}
bookingID, err := h.applicationHandler.CreateSolidarityTransportJourneyBooking(r.Context(), driverID, journeyID, passengerID, motivation, message, doNotSend, returnWaitingTimeMinutes)
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)
}
}
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
}
http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s/journeys/%s", driverID, journeyID), 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
}
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance)
}
}
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)
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,161 @@
package application
import (
"fmt"
"io"
"net/http"
"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")
startDate := r.FormValue("startdate")
endDate := r.FormValue("enddate")
vehicleType := r.FormValue("type")
automatic := r.FormValue("automatic") == "on"
// 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
startDate := r.FormValue("startdate")
endDate := r.FormValue("enddate")
// 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
}
}
}

View File

@@ -0,0 +1,408 @@
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()
err := h.applicationHandler.UpdateBooking(r.Context(), bookingID,
r.FormValue("startdate"),
r.FormValue("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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -0,0 +1,13 @@
package auth
import "net/http"
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
session.Save(r, w)
}
http.Redirect(w, r, "/", http.StatusOK)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

21
servers/web/external/handler.go vendored Normal file
View File

@@ -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,
}
}

View File

@@ -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)
}
}

View File

@@ -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())
}

View File

@@ -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)
})
}

View File

@@ -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,
})
}
}

View File

@@ -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())
}

141
servers/web/web.go Normal file
View File

@@ -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
}