4 Commits

Author SHA1 Message Date
Arnaud Delcasse
b5e722ff9b Add public theme
Some checks failed
Build and Push Docker Image / build_and_push (push) Failing after 2m52s
2026-01-30 18:44:17 +01:00
2333bba79b Merge pull request 'solidarity-transport-2' (#2) from solidarity-transport-2 into main
All checks were successful
Build and Push Docker Image / build_and_push (push) Successful in 2m53s
Reviewed-on: #2
2026-01-05 16:52:36 +00:00
Arnaud Delcasse
b1323867c7 Add public web functionalities 2026-01-05 17:50:05 +01:00
Arnaud Delcasse
6cda7509c1 MMS43 changes 2025-12-29 10:50:58 +01:00
28 changed files with 722 additions and 190 deletions

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@ themes/*
.vscode
__debug_bin
parcoursmob
public_themes/
.idea

View File

@@ -30,6 +30,7 @@ FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /themes/ /themes/
COPY --from=builder /public_themes/ /public_themes/
COPY --from=builder /server /
EXPOSE 8080

View File

@@ -23,6 +23,12 @@ func ReadConfig() (*viper.Viper, error) {
"enabled": true,
"listen": "0.0.0.0:8081",
},
"publicweb": map[string]any{
"enabled": false,
"listen": "0.0.0.0:8082",
"root_dir": "public_themes/default",
"contact_email": "contact@example.com",
},
},
"identification": map[string]any{
"sessions": map[string]any{

View File

@@ -77,14 +77,14 @@ func (h *ApplicationHandler) GetAdministrationData(ctx context.Context) (*Admini
wg.Add(1)
go func() {
defer wg.Done()
accounts, accountsErr = h.services.GetAccounts()
accounts, accountsErr = h.services.GetAccounts(ctx)
}()
// Retrieve beneficiaries in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
beneficiaries, beneficiariesErr = h.services.GetBeneficiaries()
beneficiaries, beneficiariesErr = h.services.GetBeneficiaries(ctx)
}()
// Retrieve bookings in a goroutine
@@ -570,8 +570,8 @@ func (h *ApplicationHandler) GetBookingsStats(status, startDate, endDate string)
}, nil
}
func (h *ApplicationHandler) GetBeneficiariesStats() (*AdminBeneficiariesStatsResult, error) {
beneficiaries, err := h.services.GetBeneficiaries()
func (h *ApplicationHandler) GetBeneficiariesStats(ctx context.Context) (*AdminBeneficiariesStatsResult, error) {
beneficiaries, err := h.services.GetBeneficiaries(ctx)
if err != nil {
return nil, err
}

View File

@@ -62,14 +62,7 @@ func (h *ApplicationHandler) GetDashboardData(ctx context.Context, driverAddress
accounts := []mobilityaccountsstorage.Account{}
// We only display the 5 most recent here
count := len(resp.Accounts)
min := count - 5
if min < 0 {
min = 0
}
for _, account := range resp.Accounts[min:] {
for _, account := range resp.Accounts {
// Check if not archived
if archived, ok := account.Data.AsMap()["archived"].(bool); !ok || !archived {
a := account.ToStorageType()
@@ -225,4 +218,3 @@ func (h *ApplicationHandler) GetDashboardData(ctx context.Context, driverAddress
OrganizedCarpoolDrivers: organizedCarpoolDrivers,
}, nil
}

View File

@@ -179,7 +179,7 @@ func (h *ApplicationHandler) DisplayGroupModule(ctx context.Context, groupID str
h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour)
// Get beneficiaries in current user's group
accountsBeneficiaire, err := h.services.GetBeneficiariesInGroup(currentUserGroup)
accountsBeneficiaire, err := h.services.GetBeneficiariesInGroup(ctx, currentUserGroup)
if err != nil {
return nil, fmt.Errorf("failed to get beneficiaries in group: %w", err)
}

View File

@@ -91,7 +91,7 @@ func (h *ApplicationHandler) SearchJourneys(
// SOLIDARITY TRANSPORT
var err error
drivers, err = h.services.GetAccountsInNamespacesMap([]string{"solidarity_drivers", "organized_carpool_drivers"})
drivers, err = h.services.GetAccountsInNamespacesMap(ctx, []string{"solidarity_drivers", "organized_carpool_drivers"})
if err != nil {
drivers = map[string]mobilityaccountsstorage.Account{}
}
@@ -99,30 +99,20 @@ func (h *ApplicationHandler) SearchJourneys(
protodep, _ := transformers.GeoJsonToProto(departureGeo)
protodest, _ := transformers.GeoJsonToProto(destinationGeo)
// Get driver IDs to exclude based on group_id (drivers who already have bookings in this group)
excludedDriverIds := make(map[string]bool)
if solidarityExcludeGroupId != "" {
bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{
StartDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)),
EndDate: timestamppb.New(time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)),
})
if err == nil {
for _, booking := range bookingsResp.Bookings {
if booking.GroupId == solidarityExcludeGroupId {
excludedDriverIds[booking.DriverId] = true
}
}
}
}
if solidarityTransportEnabled {
log.Debug().Time("departure time", departureDateTime).Msg("calling driver journeys with ...")
res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, &gen.GetDriverJourneysRequest{
req := &gen.GetDriverJourneysRequest{
Departure: protodep,
Arrival: protodest,
DepartureDate: timestamppb.New(departureDateTime),
})
}
// Pass exclude_group_id to the service to filter out drivers with bookings in this group
if solidarityExcludeGroupId != "" {
req.ExcludeGroupId = &solidarityExcludeGroupId
}
res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, req)
if err != nil {
log.Error().Err(err).Msg("error in grpc call to GetDriverJourneys")
} else {
@@ -138,10 +128,6 @@ func (h *ApplicationHandler) SearchJourneys(
if dj.DriverId == solidarityTransportExcludeDriver {
continue
}
// Skip drivers who already have bookings in the same group
if excludedDriverIds[dj.DriverId] {
continue
}
if !yield(dj) {
return
}

View File

@@ -21,7 +21,7 @@ type MembersResult struct {
}
func (h *ApplicationHandler) GetMembers(ctx context.Context) (*MembersResult, error) {
accounts, err := h.services.GetAccounts()
accounts, err := h.services.GetAccounts(ctx)
if err != nil {
return nil, err
}

View File

@@ -212,7 +212,7 @@ func (h *ApplicationHandler) GetOrganizedCarpoolOverview(ctx context.Context, st
}
}
beneficiariesMap, err := h.services.GetBeneficiariesMap()
beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx)
if err != nil {
beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
}
@@ -403,12 +403,12 @@ func (h *ApplicationHandler) GetOrganizedCarpoolBookingData(ctx context.Context,
return nil, fmt.Errorf("carpool booking not found")
}
driver, err := h.services.GetAccount(resp.Booking.Driver.Id)
driver, err := h.services.GetAccount(ctx, resp.Booking.Driver.Id)
if err != nil {
return nil, fmt.Errorf("driver retrieval issue: %w", err)
}
passenger, err := h.services.GetAccount(resp.Booking.Passenger.Id)
passenger, err := h.services.GetAccount(ctx, resp.Booking.Passenger.Id)
if err != nil {
return nil, fmt.Errorf("passenger retrieval issue: %w", err)
}
@@ -585,7 +585,7 @@ type OrganizedCarpoolDriverDataResult struct {
func (h *ApplicationHandler) GetOrganizedCarpoolDriverData(ctx context.Context, driverID string) (*OrganizedCarpoolDriverDataResult, error) {
documents := h.filestorage.List(filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS + "/" + driverID)
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return nil, fmt.Errorf("issue retrieving driver account: %w", err)
}
@@ -686,7 +686,7 @@ type OrganizedCarpoolDriverResult struct {
}
func (h *ApplicationHandler) GetOrganizedCarpoolDriver(ctx context.Context, driverID string) (*OrganizedCarpoolDriverResult, error) {
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return nil, fmt.Errorf("issue retrieving driver account: %w", err)
}
@@ -703,7 +703,7 @@ func (h *ApplicationHandler) GetOrganizedCarpoolDriver(ctx context.Context, driv
func (h *ApplicationHandler) UpdateOrganizedCarpoolDriver(ctx context.Context, driverID, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address, addressDestination any, gender, otherProperties string) (string, error) {
// Security check: verify the account exists and is an organized carpool driver
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return "", fmt.Errorf("issue retrieving driver account: %w", err)
}
@@ -786,7 +786,7 @@ func (h *ApplicationHandler) UpdateOrganizedCarpoolDriver(ctx context.Context, d
func (h *ApplicationHandler) ArchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error {
// Security check: verify the account exists and is an organized carpool driver
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return fmt.Errorf("issue retrieving driver account: %w", err)
}
@@ -815,7 +815,7 @@ func (h *ApplicationHandler) ArchiveOrganizedCarpoolDriver(ctx context.Context,
func (h *ApplicationHandler) UnarchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error {
// Security check: verify the account exists and is an organized carpool driver
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return fmt.Errorf("issue retrieving driver account: %w", err)
}
@@ -844,7 +844,7 @@ func (h *ApplicationHandler) UnarchiveOrganizedCarpoolDriver(ctx context.Context
func (h *ApplicationHandler) AddOrganizedCarpoolDriverDocument(ctx context.Context, driverID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error {
// Security check: verify the account exists and is an organized carpool driver
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return fmt.Errorf("issue retrieving driver account: %w", err)
}
@@ -876,7 +876,7 @@ func (h *ApplicationHandler) DeleteOrganizedCarpoolDriverDocument(ctx context.Co
func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driverID, outwardtime, returntime string, departure, destination *geojson.Feature, days map[string]bool) error {
// Security check: verify the account exists and is an organized carpool driver
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return fmt.Errorf("issue retrieving driver account: %w", err)
}
@@ -911,16 +911,20 @@ func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driver
for day, enabled := range days {
if enabled {
dayCode := dayMap[day]
if outwardtime != "" {
outwardschedules = append(outwardschedules, map[string]any{
"day": dayCode,
"time_of_day": outwardtime,
})
}
if returntime != "" {
returnschedules = append(returnschedules, map[string]any{
"day": dayCode,
"time_of_day": returntime,
})
}
}
}
outward_fc := geojson.NewFeatureCollection()
outward_fc.Append(departure)
@@ -962,12 +966,24 @@ func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driver
return fmt.Errorf("failed marshaling return geojson: %w", err)
}
// Only add outward trip if outward time is provided
if outwardtime != "" {
trips = append(trips, &proto.CarpoolFeatureCollection{
Serialized: string(outwardtrip),
})
}
// Only add return trip if return time is provided
if returntime != "" {
trips = append(trips, &proto.CarpoolFeatureCollection{
Serialized: string(returntrip),
})
}
// If no trips to create, return early
if len(trips) == 0 {
return fmt.Errorf("at least one time (outward or return) must be provided")
}
req := &proto.CreateRegularRoutesRequest{
Routes: trips,
@@ -1016,21 +1032,21 @@ func (h *ApplicationHandler) GetOrganizedCarpoolJourneyData(ctx context.Context,
return nil, fmt.Errorf("could not unmarshal carpool journey: %w", err)
}
driver, err := h.services.GetAccount(driverID)
driver, err := h.services.GetAccount(ctx, driverID)
if err != nil {
return nil, fmt.Errorf("could not get driver: %w", err)
}
var passenger mobilityaccountsstorage.Account
if passengerID != "" {
passenger, err = h.services.GetAccount(passengerID)
passenger, err = h.services.GetAccount(ctx, passengerID)
if err != nil {
return nil, fmt.Errorf("could not get passenger account: %w", err)
}
}
// Get beneficiaries in current user's group
beneficiaries, err := h.services.GetBeneficiariesInGroup(currentUserGroup)
beneficiaries, err := h.services.GetBeneficiariesInGroup(ctx, currentUserGroup)
if err != nil {
return nil, fmt.Errorf("could not get beneficiaries: %w", err)
}
@@ -1192,7 +1208,7 @@ func (h *ApplicationHandler) CreateOrganizedCarpoolJourneyBooking(ctx context.Co
// Get passenger account and calculate pricing
var passenger mobilityaccountsstorage.Account
if passengerID != "" {
passenger, err = h.services.GetAccount(passengerID)
passenger, err = h.services.GetAccount(ctx, passengerID)
if err != nil {
return "", fmt.Errorf("could not get passenger account: %w", err)
}
@@ -1268,7 +1284,7 @@ func (h *ApplicationHandler) CreateOrganizedCarpoolJourneyBooking(ctx context.Co
if message != "" && !doNotSend {
send_message := strings.ReplaceAll(message, "{booking_id}", bookingRes.Booking.Id)
log.Debug().Str("message", send_message).Msg("Carpool booking created: sending message")
h.GenerateSMS(driverID, send_message)
h.GenerateSMS(ctx, driverID, send_message)
}
return bookingRes.Booking.Id, nil
@@ -1403,7 +1419,7 @@ func (h *ApplicationHandler) GetOrganizedCarpoolBookings(ctx context.Context, st
}
// Get beneficiaries
beneficiariesMap, err := h.services.GetBeneficiariesMap()
beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx)
if err != nil {
beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
}

View File

@@ -8,11 +8,11 @@ import (
)
func (h *ApplicationHandler) SendSMS(ctx context.Context, beneficiaryID, message string) error {
return h.GenerateSMS(beneficiaryID, message)
return h.GenerateSMS(ctx, beneficiaryID, message)
}
func (h *ApplicationHandler) GenerateSMS(recipientid string, message string) error {
recipient, err := h.services.GetAccount(recipientid)
func (h *ApplicationHandler) GenerateSMS(ctx context.Context, recipientid string, message string) error {
recipient, err := h.services.GetAccount(ctx, recipientid)
if err != nil {
log.Error().Err(err).Msg("user not found")
return err

View File

@@ -183,7 +183,7 @@ func filterBookingsByGeography(bookings []*solidaritytypes.Booking, departurePol
func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context, status, driverID, startDate, endDate, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode, histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeoLayer, histDepartureGeoCode, histDestinationGeoLayer, histDestinationGeoCode, histPassengerAddressGeoLayer, histPassengerAddressGeoCode string, archivedFilter bool, driverAddressGeoLayer, driverAddressGeoCode string) (*SolidarityTransportOverviewResult, error) {
// Get ALL drivers for the accountsMap (used in bookings display)
allDrivers, err := h.solidarityDrivers("", false)
allDrivers, err := h.solidarityDrivers(ctx, "", false)
if err != nil {
log.Error().Err(err).Msg("issue getting all solidarity drivers")
allDrivers = []mobilityaccountsstorage.Account{}
@@ -196,7 +196,7 @@ func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context,
}
// Get filtered drivers for the drivers tab display
accounts, err := h.solidarityDrivers("", archivedFilter)
accounts, err := h.solidarityDrivers(ctx, "", archivedFilter)
if err != nil {
log.Error().Err(err).Msg("issue getting solidarity drivers")
accounts = []mobilityaccountsstorage.Account{}
@@ -241,7 +241,7 @@ func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context,
return strings.Compare(firstNameA, firstNameB)
})
beneficiariesMap, err := h.services.GetBeneficiariesMap()
beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx)
if err != nil {
beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
}
@@ -435,7 +435,7 @@ type SolidarityTransportBookingsResult struct {
func (h *ApplicationHandler) GetSolidarityTransportBookings(ctx context.Context, startDate, endDate *time.Time, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode string) (*SolidarityTransportBookingsResult, error) {
// Get all drivers
drivers, err := h.solidarityDrivers("", false)
drivers, err := h.solidarityDrivers(ctx, "", false)
if err != nil {
log.Error().Err(err).Msg("issue getting solidarity drivers")
drivers = []mobilityaccountsstorage.Account{}
@@ -447,7 +447,7 @@ func (h *ApplicationHandler) GetSolidarityTransportBookings(ctx context.Context,
}
// Get beneficiaries
beneficiariesMap, err := h.services.GetBeneficiariesMap()
beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx)
if err != nil {
beneficiariesMap = map[string]mobilityaccountsstorage.Account{}
}
@@ -1030,7 +1030,7 @@ func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Conte
// Get passenger account
var passenger mobilityaccountsstorage.Account
if passengerID != "" {
passengerResp, err := h.services.GetAccount(passengerID)
passengerResp, err := h.services.GetAccount(ctx, passengerID)
if err != nil {
return nil, fmt.Errorf("could not get passenger account: %w", err)
}
@@ -1048,7 +1048,7 @@ func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Conte
}
// Get beneficiaries in current user's group
beneficiaries, err := h.services.GetBeneficiariesInGroup(currentUserGroup)
beneficiaries, err := h.services.GetBeneficiariesInGroup(ctx, currentUserGroup)
if err != nil {
return nil, fmt.Errorf("could not get beneficiaries: %w", err)
}
@@ -1093,7 +1093,7 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context
// Get passenger account for pricing
var passenger mobilityaccountsstorage.Account
if passengerID != "" {
passengerResp, err := h.services.GetAccount(passengerID)
passengerResp, err := h.services.GetAccount(ctx, passengerID)
if err != nil {
return "", fmt.Errorf("could not get passenger account: %w", err)
}
@@ -1171,7 +1171,7 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context
// Send SMS if not disabled
if !doNotSend && message != "" {
send_message := strings.ReplaceAll(message, "{booking_id}", resp.Booking.Id)
if err := h.GenerateSMS(driverID, send_message); err != nil {
if err := h.GenerateSMS(ctx, driverID, send_message); err != nil {
log.Error().Err(err).Msg("failed to send SMS")
}
}
@@ -1248,12 +1248,12 @@ func (h *ApplicationHandler) GetSolidarityTransportBookingData(ctx context.Conte
}, nil
}
func (h *ApplicationHandler) solidarityDrivers(searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) {
func (h *ApplicationHandler) solidarityDrivers(ctx context.Context, searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) {
request := &mobilityaccounts.GetAccountsRequest{
Namespaces: []string{"solidarity_drivers"},
}
resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, request)
if err != nil {
return nil, err
}
@@ -1320,7 +1320,7 @@ func (h *ApplicationHandler) CalculateSolidarityTransportPricing(ctx context.Con
// Get passenger account
var passenger mobilityaccountsstorage.Account
if passengerID != "" {
passengerResp, err := h.services.GetAccount(passengerID)
passengerResp, err := h.services.GetAccount(ctx, passengerID)
if err != nil {
return nil, fmt.Errorf("could not get passenger account: %w", err)
}
@@ -1376,6 +1376,17 @@ func (h *ApplicationHandler) calculateSolidarityTransportPricing(ctx context.Con
}
}
}
if pst, ok := op_map["previous_solidarity_transport_count"]; ok {
if pst_str, ok := pst.(string); ok {
if pst_str != "" {
if n, err := strconv.Atoi(pst_str); err == nil {
history = history + n
} else {
log.Error().Err(err).Str("n", pst_str).Msg("string to int conversion error")
}
}
}
}
}
}
@@ -1459,7 +1470,7 @@ func (h *ApplicationHandler) UpdateSolidarityTransportBookingStatus(ctx context.
if previousStatus != "VALIDATED" && status == "VALIDATED" {
if message != "" {
send_message := strings.ReplaceAll(message, "{booking_id}", bookingID)
h.GenerateSMS(passenger.ID, send_message)
h.GenerateSMS(ctx, passenger.ID, send_message)
}
if err := h.CreditWallet(ctx, passenger.ID, -1*booking.Journey.Price.Amount, "Transport solidaire", "Débit transport solidaire"); err != nil {
return fmt.Errorf("could not debit passenger wallet: %w", err)
@@ -1467,6 +1478,32 @@ func (h *ApplicationHandler) UpdateSolidarityTransportBookingStatus(ctx context.
if err := h.CreditWallet(ctx, driver.ID, booking.DriverCompensationAmount, "Transport solidaire", "Crédit transport solidaire"); err != nil {
return fmt.Errorf("could not credit driver wallet: %w", err)
}
if notify {
groupsrequest := &groupsmanagement.GetGroupsRequest{
Namespaces: []string{"parcoursmob_organizations"},
Member: booking.PassengerId,
}
groupsresp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, groupsrequest)
if err != nil {
log.Error().Err(err).Msg("")
} else if len(groupsresp.Groups) > 0 {
members, _, err := h.groupmembers(groupsresp.Groups[0].Id)
if err != nil {
log.Error().Err(err).Msg("could not retrieve groupe members")
} else {
for _, m := range members {
if email, ok := m.Data["email"].(string); ok {
h.emailing.Send("solidarity_transport.booking_driver_accept", email, map[string]string{
"bookingid": booking.Id,
"baseUrl": h.config.GetString("base_url"),
})
}
}
}
}
}
}
// Credit passenger / debit driver when previous status was VALIDATED and new status is not VALIDATED anymore

View File

@@ -62,7 +62,7 @@ func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context,
}
}
driversMap, _ := h.services.GetBeneficiariesMap()
driversMap, _ := h.services.GetBeneficiariesMap(ctx)
sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
sort.Sort(sorting.BookingsByStartdate(bookings))
@@ -180,7 +180,7 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte
cacheID := uuid.NewString()
h.cache.PutWithTTL(cacheID, bookings, 1*time.Hour)
driversMap, _ := h.services.GetBeneficiariesMap()
driversMap, _ := h.services.GetBeneficiariesMap(ctx)
return &VehiclesManagementBookingsListResult{
VehiclesMap: vehiclesMap,
@@ -263,7 +263,7 @@ func (h *ApplicationHandler) GetVehicleDisplay(ctx context.Context, vehicleID st
return nil, fmt.Errorf("failed to get vehicle: %w", err)
}
beneficiaries, err := h.services.GetBeneficiariesMap()
beneficiaries, err := h.services.GetBeneficiariesMap(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get beneficiaries: %w", err)
}
@@ -281,7 +281,7 @@ func (h *ApplicationHandler) GetVehicleDisplay(ctx context.Context, vehicleID st
}, nil
}
func (h *ApplicationHandler) UpdateBooking(ctx context.Context, bookingID, startdate, enddate, unavailablefrom, unavailableto string) error {
func (h *ApplicationHandler) UpdateBooking(ctx context.Context, bookingID string, startdate, enddate *time.Time, unavailablefrom, unavailableto string) error {
booking, err := h.services.GetBooking(bookingID)
if err != nil {
return fmt.Errorf("failed to get booking: %w", err)
@@ -289,21 +289,19 @@ func (h *ApplicationHandler) UpdateBooking(ctx context.Context, bookingID, start
newbooking, _ := fleets.BookingFromStorageType(&booking)
if startdate != "" {
newstartdate, _ := time.Parse("2006-01-02", startdate)
newbooking.Startdate = timestamppb.New(newstartdate)
if startdate != nil {
newbooking.Startdate = timestamppb.New(*startdate)
if newstartdate.Before(newbooking.Unavailablefrom.AsTime()) {
newbooking.Unavailablefrom = timestamppb.New(newstartdate)
if startdate.Before(newbooking.Unavailablefrom.AsTime()) {
newbooking.Unavailablefrom = timestamppb.New(*startdate)
}
}
if enddate != "" {
newenddate, _ := time.Parse("2006-01-02", enddate)
newbooking.Enddate = timestamppb.New(newenddate)
if enddate != nil {
newbooking.Enddate = timestamppb.New(*enddate)
if newenddate.After(newbooking.Unavailableto.AsTime()) || newenddate.Equal(newbooking.Unavailableto.AsTime()) {
newbooking.Unavailableto = timestamppb.New(newenddate.Add(24 * time.Hour))
if enddate.After(newbooking.Unavailableto.AsTime()) || enddate.Equal(newbooking.Unavailableto.AsTime()) {
newbooking.Unavailableto = timestamppb.New(enddate.Add(24 * time.Hour))
}
}

View File

@@ -31,8 +31,8 @@ type VehiclesSearchResult struct {
BeneficiaryDocuments []filestorage.FileInfo
Groups map[string]any
Searched bool
StartDate string
EndDate string
StartDate time.Time
EndDate time.Time
VehicleType string
Automatic bool
MandatoryDocuments []string
@@ -41,22 +41,13 @@ type VehiclesSearchResult struct {
Beneficiaries []mobilityaccountsstorage.Account
}
func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID, startDateStr, endDateStr, vehicleType string, automatic bool) (*VehiclesSearchResult, error) {
func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID string, startdate, enddate time.Time, vehicleType string, automatic bool) (*VehiclesSearchResult, error) {
var beneficiary mobilityaccountsstorage.Account
beneficiarydocuments := []filestorage.FileInfo{}
vehicles := []storage.Vehicle{}
searched := false
administrators := []string{}
startdate, err := time.Parse("2006-01-02", startDateStr)
if err != nil {
startdate = time.Time{}
}
enddate, err := time.Parse("2006-01-02", endDateStr)
if err != nil {
enddate = time.Time{}
}
if beneficiaryID != "" && startdate.After(time.Now().Add(-24*time.Hour)) && enddate.After(startdate) {
// Handler form
searched = true
@@ -128,7 +119,7 @@ func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID,
beneficiarydocuments = h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiary.ID)
}
accounts, err := h.services.GetBeneficiariesMap()
accounts, err := h.services.GetBeneficiariesMap(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get beneficiaries: %w", err)
}
@@ -165,8 +156,8 @@ func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID,
BeneficiaryDocuments: beneficiarydocuments,
Groups: groups,
Searched: searched,
StartDate: startDateStr,
EndDate: endDateStr,
StartDate: startdate,
EndDate: enddate,
VehicleType: vehicleType,
Automatic: automatic,
MandatoryDocuments: mandatoryDocuments,
@@ -180,7 +171,7 @@ type BookVehicleResult struct {
BookingID string
}
func (h *ApplicationHandler) BookVehicle(ctx context.Context, vehicleID, beneficiaryID, startDateStr, endDateStr string, documents map[string]io.Reader, documentHeaders map[string]string, existingDocs map[string]string, currentUserID string, currentUserClaims map[string]any, currentGroup any) (*BookVehicleResult, error) {
func (h *ApplicationHandler) BookVehicle(ctx context.Context, vehicleID, beneficiaryID string, startdate, enddate time.Time, documents map[string]io.Reader, documentHeaders map[string]string, existingDocs map[string]string, currentUserID string, currentUserClaims map[string]any, currentGroup any) (*BookVehicleResult, error) {
group := currentGroup.(groupsmanagementstorage.Group)
vehicle, err := h.services.GRPC.Fleets.GetVehicle(ctx, &fleets.GetVehicleRequest{
@@ -190,15 +181,6 @@ func (h *ApplicationHandler) BookVehicle(ctx context.Context, vehicleID, benefic
return nil, fmt.Errorf("vehicle not found: %w", err)
}
startdate, err := time.Parse("2006-01-02", startDateStr)
if err != nil {
return nil, fmt.Errorf("invalid start date: %w", err)
}
enddate, err := time.Parse("2006-01-02", endDateStr)
if err != nil {
return nil, fmt.Errorf("invalid end date: %w", err)
}
data := map[string]any{
"booked_by": map[string]any{
"user": map[string]any{

View File

@@ -10,7 +10,7 @@ import (
)
func (h *ApplicationHandler) CreditWallet(ctx context.Context, userid string, amount float64, paymentMethod string, description string) error {
account, err := h.services.GetAccount(userid)
account, err := h.services.GetAccount(ctx, userid)
if err != nil {
log.Error().Err(err).Msg("could not retrieve account")
return err

3
go.mod
View File

@@ -57,7 +57,7 @@ require (
git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536
git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee
github.com/arran4/golang-ical v0.3.1
github.com/coreos/go-oidc/v3 v3.11.0
github.com/go-viper/mapstructure/v2 v2.4.0
@@ -164,6 +164,7 @@ require (
go.etcd.io/etcd/api/v3 v3.5.12 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.1.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.21.0 // indirect

4
go.sum
View File

@@ -28,6 +28,8 @@ git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af h1:KxHim1dF
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af/go.mod h1:mad9D+WICDdpJzB+8H/wEVVbllK2mU6VLVByrppc9x0=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc h1:NLU5DUo5Kt3jkPhV3KkqQMahTHIrGildBvYlHwJ6JmM=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee h1:aoXSugsrZrM8E3WhqOCM+bLgGdxdf7dZAxx/vfbYzWQ=
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/DATA-DOG/go-sqlmock v1.3.2/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
@@ -360,6 +362,8 @@ go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.1.0 h1:/ELnVNjmfUKDsoBisXxuJL0noR9CfeUIrP7Yt3R+egg=
go.mongodb.org/mongo-driver/v2 v2.1.0/go.mod h1:AWiLRShSrk5RHQS3AEn3RL19rqOzVq49MCpWQ3x/huI=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=

10
main.go
View File

@@ -10,6 +10,7 @@ import (
"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
"git.coopgo.io/coopgo-apps/parcoursmob/servers/mcp"
"git.coopgo.io/coopgo-apps/parcoursmob/servers/publicweb"
"git.coopgo.io/coopgo-apps/parcoursmob/servers/web"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
@@ -27,6 +28,7 @@ func main() {
dev_env = cfg.GetBool("dev_env")
webEnabled = cfg.GetBool("server.web.enabled")
mcpEnabled = cfg.GetBool("server.mcp.enabled")
publicwebEnabled = cfg.GetBool("server.publicweb.enabled")
)
if dev_env {
@@ -80,5 +82,13 @@ func main() {
}()
}
if publicwebEnabled {
wg.Add(1)
go func() {
defer wg.Done()
publicweb.Run(cfg, svc, applicationHandler, kv, filestorage, emailing)
}()
}
wg.Wait()
}

View File

@@ -1,6 +1,7 @@
package renderer
import (
"bytes"
"fmt"
"html/template"
"net/http"
@@ -53,13 +54,22 @@ func (renderer *Renderer) Render(name string, w http.ResponseWriter, r *http.Req
prefixed_files = append(prefixed_files, renderer.templateFile(f))
}
w.WriteHeader(http.StatusOK)
t := template.New(name).Funcs(GetTemplateFuncMap(state.Group, renderer.GlobalConfig, renderer.FileStorage))
t = template.Must(t.ParseFiles(prefixed_files...))
err := t.ExecuteTemplate(w, "main", state)
// Render to buffer first to avoid write timeouts during template execution
var buf bytes.Buffer
err := t.ExecuteTemplate(&buf, "main", state)
if err != nil {
log.Error().Err(err).Msg("issue executing template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, err = buf.WriteTo(w)
if err != nil {
log.Error().Err(err).Msg("issue writing template to response")
}
}
@@ -69,13 +79,22 @@ func (renderer *Renderer) RenderNoLayout(name string, w http.ResponseWriter, r *
prefixed_files = append(prefixed_files, renderer.templateFile(f))
}
w.WriteHeader(http.StatusOK)
t := template.New(name).Funcs(GetTemplateFuncMap(state.Group, renderer.GlobalConfig, renderer.FileStorage))
t = template.Must(t.ParseFiles(prefixed_files...))
err := t.ExecuteTemplate(w, "main", state)
// Render to buffer first to avoid write timeouts during template execution
var buf bytes.Buffer
err := t.ExecuteTemplate(&buf, "main", state)
if err != nil {
log.Error().Err(err).Msg("issue executing template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, err = buf.WriteTo(w)
if err != nil {
log.Error().Err(err).Msg("issue writing template to response")
}
}

64
servers/publicweb/api.go Normal file
View File

@@ -0,0 +1,64 @@
package publicweb
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
func (s *PublicWebServer) setupAPIRoutes(r *mux.Router) {
api := r.PathPrefix("/api").Subrouter()
api.HandleFunc("/contact", s.contactHandler).Methods("POST", "OPTIONS")
}
func (s *PublicWebServer) contactHandler(w http.ResponseWriter, r *http.Request) {
// Handle CORS preflight
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
return
}
var data map[string]any
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
log.Error().Err(err).Msg("Failed to decode contact request")
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if len(data) == 0 {
http.Error(w, "Request body cannot be empty", http.StatusBadRequest)
return
}
// Structure data for email template
emailData := map[string]any{
"baseUrl": s.cfg.GetString("base_url"),
"fields": data,
}
// Send email using the mailer
contactEmail := s.cfg.GetString("server.publicweb.contact_email")
if contactEmail == "" {
log.Error().Msg("Contact email not configured")
http.Error(w, "Contact service not configured", http.StatusInternalServerError)
return
}
if err := s.mailer.Send("contact.request", contactEmail, emailData); err != nil {
log.Error().Err(err).Msg("Failed to send contact email")
http.Error(w, "Failed to send message", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"message": "Message sent successfully",
})
}

View File

@@ -0,0 +1,142 @@
package publicweb
import (
"net/http"
"time"
"github.com/paulmach/orb/geojson"
"github.com/rs/zerolog/log"
)
// JourneySearchResponse represents the search results for hydration
type JourneySearchResponse struct {
Searched bool `json:"searched"`
DepartureDate string `json:"departure_date,omitempty"`
DepartureTime string `json:"departure_time,omitempty"`
Departure any `json:"departure,omitempty"`
Destination any `json:"destination,omitempty"`
Error string `json:"error,omitempty"`
Results struct {
SolidarityDrivers struct {
Number int `json:"number"`
} `json:"solidarity_drivers"`
OrganizedCarpools struct {
Number int `json:"number"`
} `json:"organized_carpools"`
Carpools struct {
Number int `json:"number"`
Results any `json:"results,omitempty"`
} `json:"carpools"`
PublicTransit struct {
Number int `json:"number"`
Results any `json:"results,omitempty"`
} `json:"public_transit"`
Vehicles struct {
Number int `json:"number"`
Results any `json:"results,omitempty"`
} `json:"vehicles"`
LocalSolutions struct {
Number int `json:"number"`
Results any `json:"results,omitempty"`
} `json:"local_solutions"`
} `json:"results"`
}
// journeySearchDataProvider provides data for the journey search page
func (s *PublicWebServer) journeySearchDataProvider(r *http.Request) (any, error) {
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("error parsing form")
return JourneySearchResponse{Error: "invalid request"}, nil
}
departureDate := r.FormValue("departuredate")
departureTime := r.FormValue("departuretime")
departure := r.FormValue("departure")
destination := r.FormValue("destination")
response := JourneySearchResponse{
DepartureDate: departureDate,
DepartureTime: departureTime,
}
// If no search parameters, return empty response
if departure == "" || destination == "" || departureDate == "" || departureTime == "" {
return response, nil
}
// Parse timezone and datetime
locTime, err := time.LoadLocation("Europe/Paris")
if err != nil {
log.Error().Err(err).Msg("timezone error")
response.Error = "internal error"
return response, nil
}
departureDateTime, err := time.ParseInLocation("2006-01-02 15:04", departureDate+" "+departureTime, locTime)
if err != nil {
log.Error().Err(err).Msg("error parsing datetime")
response.Error = "invalid date/time format"
return response, nil
}
// Parse departure location
departureGeo, err := geojson.UnmarshalFeature([]byte(departure))
if err != nil {
log.Error().Err(err).Msg("error unmarshalling departure")
response.Error = "invalid departure location"
return response, nil
}
response.Departure = departureGeo
// Parse destination location
destinationGeo, err := geojson.UnmarshalFeature([]byte(destination))
if err != nil {
log.Error().Err(err).Msg("error unmarshalling destination")
response.Error = "invalid destination location"
return response, nil
}
response.Destination = destinationGeo
// Call business logic
result, err := s.applicationHandler.SearchJourneys(
r.Context(),
departureDateTime,
departureGeo,
destinationGeo,
"", // passengerID
"", // solidarityTransportExcludeDriver
"", // solidarityExcludeGroupId
nil, // options - use defaults
)
if err != nil {
log.Error().Err(err).Msg("error in journey search")
response.Error = "search failed"
return response, nil
}
response.Searched = result.Searched
// Solidarity drivers
response.Results.SolidarityDrivers.Number = len(result.DriverJourneys)
// Organized carpools
response.Results.OrganizedCarpools.Number = len(result.OrganizedCarpools)
// Carpools (from external operators like Movici)
response.Results.Carpools.Number = len(result.CarpoolResults)
response.Results.Carpools.Results = result.CarpoolResults
// Public transit
response.Results.PublicTransit.Number = len(result.TransitResults)
response.Results.PublicTransit.Results = result.TransitResults
// Fleet vehicles
response.Results.Vehicles.Number = len(result.VehicleResults)
response.Results.Vehicles.Results = result.VehicleResults
// Knowledge base / local solutions
response.Results.LocalSolutions.Number = len(result.KnowledgeBaseResults)
response.Results.LocalSolutions.Results = result.KnowledgeBaseResults
return response, nil
}

View File

@@ -0,0 +1,198 @@
package publicweb
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"git.coopgo.io/coopgo-platform/emailing"
)
// DataProvider returns data to hydrate a page
type DataProvider func(r *http.Request) (any, error)
// DynamicRoute defines a route with its HTML file and data provider
type DynamicRoute struct {
HTMLFile string
DataProvider DataProvider
}
// Regex to find the placeholder script tag
var dynamicDataRegex = regexp.MustCompile(`<script\s+id="dynamic-data"\s+type="application/json">\s*</script>`)
type PublicWebServer struct {
cfg *viper.Viper
services *services.ServicesHandler
kv cache.KVHandler
filestorage cache.FileStorage
applicationHandler *application.ApplicationHandler
mailer *emailing.Mailer
rootDir string
dynamicRoutes map[string]DynamicRoute
}
func Run(
cfg *viper.Viper,
svc *services.ServicesHandler,
applicationHandler *application.ApplicationHandler,
kv cache.KVHandler,
filestorage cache.FileStorage,
mailer *emailing.Mailer,
) {
address := cfg.GetString("server.publicweb.listen")
rootDir := cfg.GetString("server.publicweb.root_dir")
serviceName := cfg.GetString("service_name")
server := &PublicWebServer{
cfg: cfg,
services: svc,
kv: kv,
filestorage: filestorage,
applicationHandler: applicationHandler,
mailer: mailer,
rootDir: rootDir,
dynamicRoutes: make(map[string]DynamicRoute),
}
server.registerDynamicRoutes()
r := mux.NewRouter()
r.HandleFunc("/health", server.healthHandler).Methods("GET")
// Setup API routes
server.setupAPIRoutes(r)
for pattern := range server.dynamicRoutes {
r.HandleFunc(pattern, server.dynamicHandler).Methods("GET", "POST")
}
r.PathPrefix("/").Handler(server.fileServerHandler())
srv := &http.Server{
Handler: r,
Addr: address,
WriteTimeout: 30 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Info().
Str("service_name", serviceName).
Str("address", address).
Str("root_dir", rootDir).
Msg("Running Public Web HTTP server")
err := srv.ListenAndServe()
log.Error().Err(err).Msg("Public Web server error")
}
func (s *PublicWebServer) registerDynamicRoutes() {
s.RegisterDynamicRoute("/recherche/", "recherche/index.html", s.journeySearchDataProvider)
}
func (s *PublicWebServer) RegisterDynamicRoute(pattern, htmlFile string, provider DataProvider) {
s.dynamicRoutes[pattern] = DynamicRoute{
HTMLFile: htmlFile,
DataProvider: provider,
}
}
func (s *PublicWebServer) dynamicHandler(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
pattern, _ := route.GetPathTemplate()
dynRoute, exists := s.dynamicRoutes[pattern]
if !exists {
http.NotFound(w, r)
return
}
data, err := dynRoute.DataProvider(r)
if err != nil {
log.Error().Err(err).Str("route", pattern).Msg("Error getting data")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if err := s.hydrate(w, dynRoute.HTMLFile, data); err != nil {
http.NotFound(w, r)
}
}
// hydrate reads an HTML file and injects JSON data into <script id="dynamic-data" type="application/json"></script>
func (s *PublicWebServer) hydrate(w http.ResponseWriter, htmlFile string, data any) error {
htmlPath := filepath.Join(s.rootDir, htmlFile)
htmlContent, err := os.ReadFile(htmlPath)
if err != nil {
log.Error().Err(err).Str("file", htmlPath).Msg("Error reading HTML file")
return err
}
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msg("Error marshaling data to JSON")
return err
}
// Replace the placeholder with a script that assigns data to window.__PARCOURSMOB_DATA__
replacement := []byte(`<script id="dynamic-data">window.__PARCOURSMOB_DATA__ = ` + string(jsonData) + `;</script>`)
modifiedHTML := dynamicDataRegex.ReplaceAll(htmlContent, replacement)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(modifiedHTML)
return nil
}
func (s *PublicWebServer) fileServerHandler() http.Handler {
fs := http.FileServer(http.Dir(s.rootDir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := filepath.Join(s.rootDir, r.URL.Path)
if _, err := os.Stat(path); os.IsNotExist(err) {
if filepath.Ext(path) == "" {
if idx := filepath.Join(path, "index.html"); fileExists(idx) {
http.ServeFile(w, r, idx)
return
}
if idx := filepath.Join(s.rootDir, "index.html"); fileExists(idx) {
http.ServeFile(w, r, idx)
return
}
}
http.NotFound(w, r)
return
}
if info, _ := os.Stat(path); info != nil && info.IsDir() {
if idx := filepath.Join(path, "index.html"); fileExists(idx) && strings.HasSuffix(r.URL.Path, "/") {
http.ServeFile(w, r, idx)
return
}
}
fs.ServeHTTP(w, r)
})
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func (s *PublicWebServer) healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"healthy"}`))
}

View File

@@ -222,7 +222,7 @@ func (h *Handler) AdminStatsBookingsHTTPHandler() http.HandlerFunc {
func (h *Handler) AdminStatsBeneficiariesHTTPHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
result, err := h.applicationHandler.GetBeneficiariesStats()
result, err := h.applicationHandler.GetBeneficiariesStats(r.Context())
if err != nil {
log.Error().Err(err).Msg("error retrieving beneficiaries stats")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)

View File

@@ -59,7 +59,7 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
var departureGeo *geojson.Feature
if departure == "" && passengerID != "" {
// Get passenger address
p, err := h.services.GetAccount(passengerID)
p, err := h.services.GetAccount(r.Context(), passengerID)
if err != nil {
log.Error().Err(err).Msg("could not retrieve passenger")
http.Error(w, "Not Found", http.StatusNotFound)
@@ -123,7 +123,7 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
}
group := g.(groupstorage.Group)
beneficiaries, err := h.services.GetBeneficiariesInGroup(group)
beneficiaries, err := h.services.GetBeneficiariesInGroup(r.Context(), group)
if err != nil {
log.Error().Err(err).Msg("issue retrieving beneficiaries")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -345,7 +345,7 @@ func (h *Handler) JourneysSearchCompactHTTPHandler() http.HandlerFunc {
var departureGeo *geojson.Feature
if departure == "" && passengerID != "" {
// Get passenger address
p, err := h.services.GetAccount(passengerID)
p, err := h.services.GetAccount(r.Context(), passengerID)
if err != nil {
log.Error().Err(err).Msg("could not retrieve passenger")
http.Error(w, "Not Found", http.StatusNotFound)

View File

@@ -9,10 +9,10 @@ import (
"strings"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
gen "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
@@ -521,7 +521,11 @@ func (h *Handler) SolidarityTransportDriverJourneyHTTPHandler() http.HandlerFunc
vars := mux.Vars(r)
driverID := vars["driverid"]
journeyID := vars["journeyid"]
// Preserve passengerid from either query parameter or form data
passengerID := r.URL.Query().Get("passengerid")
if passengerID == "" {
passengerID = r.FormValue("passengerid")
}
replacesBookingID := r.URL.Query().Get("replaces_booking_id")
if r.Method == "POST" {
@@ -580,7 +584,16 @@ func (h *Handler) SolidarityTransportDriverJourneyToggleNoreturnHTTPHandler() ht
return
}
http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/drivers/%s/journeys/%s", driverID, journeyID), http.StatusFound)
// Preserve passengerid query parameter if present
passengerID := r.URL.Query().Get("passengerid")
if passengerID == "" {
passengerID = r.FormValue("passengerid")
}
redirectURL := fmt.Sprintf("/app/solidarity-transport/drivers/%s/journeys/%s", driverID, journeyID)
if passengerID != "" {
redirectURL += "?passengerid=" + passengerID
}
http.Redirect(w, r, redirectURL, http.StatusFound)
}
}
@@ -772,3 +785,4 @@ func (h *Handler) SolidarityTransportBookingStatusHTTPHandler(action string) htt
http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/bookings/%s", bookingID), http.StatusSeeOther)
}
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"net/http"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
"git.coopgo.io/coopgo-platform/groups-management/storage"
@@ -18,11 +19,31 @@ func (h *Handler) VehiclesSearchHTTPHandler() http.HandlerFunc {
// Extract parameters
beneficiaryID := r.FormValue("beneficiaryid")
startDate := r.FormValue("startdate")
endDate := r.FormValue("enddate")
startDateStr := r.FormValue("startdate")
endDateStr := r.FormValue("enddate")
startTimeStr := r.FormValue("starttime")
endTimeStr := r.FormValue("endtime")
vehicleType := r.FormValue("type")
automatic := r.FormValue("automatic") == "on"
// Default times if not provided
if startTimeStr == "" {
startTimeStr = "00:00"
}
if endTimeStr == "" {
endTimeStr = "23:59"
}
// Parse dates and times in Europe/Paris timezone
paris, _ := time.LoadLocation("Europe/Paris")
var startDate, endDate time.Time
if startDateStr != "" {
startDate, _ = time.ParseInLocation("2006-01-02 15:04", startDateStr+" "+startTimeStr, paris)
}
if endDateStr != "" {
endDate, _ = time.ParseInLocation("2006-01-02 15:04", endDateStr+" "+endTimeStr, paris)
}
// Call business logic
result, err := h.applicationHandler.SearchVehicles(r.Context(), beneficiaryID, startDate, endDate, vehicleType, automatic)
if err != nil {
@@ -58,8 +79,23 @@ func (h *Handler) BookVehicleHTTPHandler() http.HandlerFunc {
}
// Extract form data
startDate := r.FormValue("startdate")
endDate := r.FormValue("enddate")
startDateStr := r.FormValue("startdate")
endDateStr := r.FormValue("enddate")
startTimeStr := r.FormValue("starttime")
endTimeStr := r.FormValue("endtime")
// Default times if not provided
if startTimeStr == "" {
startTimeStr = "00:00"
}
if endTimeStr == "" {
endTimeStr = "23:59"
}
// Parse dates and times in Europe/Paris timezone
paris, _ := time.LoadLocation("Europe/Paris")
startDate, _ := time.ParseInLocation("2006-01-02 15:04", startDateStr+" "+startTimeStr, paris)
endDate, _ := time.ParseInLocation("2006-01-02 15:04", endDateStr+" "+endTimeStr, paris)
// Extract documents
documents := make(map[string]io.Reader)

View File

@@ -178,9 +178,34 @@ func (h *Handler) VehicleManagementBookingDisplayHTTPHandler() http.HandlerFunc
// Parse form data
r.ParseForm()
// Extract date and time values
startDateStr := r.FormValue("startdate")
startTimeStr := r.FormValue("starttime")
endDateStr := r.FormValue("enddate")
endTimeStr := r.FormValue("endtime")
// Parse dates with times in Europe/Paris timezone
paris, _ := time.LoadLocation("Europe/Paris")
var startDate, endDate *time.Time
if startDateStr != "" {
if startTimeStr == "" {
startTimeStr = "00:00"
}
t, _ := time.ParseInLocation("2006-01-02 15:04", startDateStr+" "+startTimeStr, paris)
startDate = &t
}
if endDateStr != "" {
if endTimeStr == "" {
endTimeStr = "23:59"
}
t, _ := time.ParseInLocation("2006-01-02 15:04", endDateStr+" "+endTimeStr, paris)
endDate = &t
}
err := h.applicationHandler.UpdateBooking(r.Context(), bookingID,
r.FormValue("startdate"),
r.FormValue("enddate"),
startDate,
endDate,
r.FormValue("unavailablefrom"),
r.FormValue("unavailableto"),
)

View File

@@ -89,8 +89,8 @@ func Run(cfg *viper.Viper, services *services.ServicesHandler, renderer *rendere
srv := &http.Server{
Handler: r,
Addr: address,
WriteTimeout: 30 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 120 * time.Second,
ReadTimeout: 30 * time.Second,
}
log.Info().Str("service_name", service_name).Str("address", address).Msg("Running HTTP server")

View File

@@ -26,12 +26,12 @@ func NewMobilityAccountService(mobilityAccountsDial string) (*MobilityAccountSer
}, nil
}
func (s *ServicesHandler) GetBeneficiaries() (accounts []storage.Account, err error) {
func (s *ServicesHandler) GetBeneficiaries(ctx context.Context) (accounts []storage.Account, err error) {
accounts = []storage.Account{}
request := &mobilityaccounts.GetAccountsRequest{
Namespaces: []string{"parcoursmob_beneficiaries"},
}
resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request)
if err == nil {
for _, v := range resp.Accounts {
@@ -43,12 +43,12 @@ func (s *ServicesHandler) GetBeneficiaries() (accounts []storage.Account, err er
return
}
func (s *ServicesHandler) GetBeneficiariesMap() (accounts map[string]storage.Account, err error) {
func (s *ServicesHandler) GetBeneficiariesMap(ctx context.Context) (accounts map[string]storage.Account, err error) {
accounts = map[string]storage.Account{}
request := &mobilityaccounts.GetAccountsRequest{
Namespaces: []string{"parcoursmob_beneficiaries"},
}
resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request)
if err == nil {
for _, v := range resp.Accounts {
@@ -59,12 +59,12 @@ func (s *ServicesHandler) GetBeneficiariesMap() (accounts map[string]storage.Acc
return
}
func (s *ServicesHandler) GetAccounts() (accounts []storage.Account, err error) {
func (s *ServicesHandler) GetAccounts(ctx context.Context) (accounts []storage.Account, err error) {
accounts = []storage.Account{}
request := &mobilityaccounts.GetAccountsRequest{
Namespaces: []string{"parcoursmob"},
}
resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request)
if err == nil {
for _, v := range resp.Accounts {
@@ -76,12 +76,12 @@ func (s *ServicesHandler) GetAccounts() (accounts []storage.Account, err error)
return
}
func (s *ServicesHandler) GetAccountsInNamespace(namespace string) (accounts []storage.Account, err error) {
func (s *ServicesHandler) GetAccountsInNamespace(ctx context.Context, namespace string) (accounts []storage.Account, err error) {
accounts = []storage.Account{}
request := &mobilityaccounts.GetAccountsRequest{
Namespaces: []string{namespace},
}
resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request)
if err == nil {
for _, v := range resp.Accounts {
@@ -92,12 +92,12 @@ func (s *ServicesHandler) GetAccountsInNamespace(namespace string) (accounts []s
return
}
func (s *ServicesHandler) GetAccountsInNamespaceMap(namespace string) (accounts map[string]storage.Account, err error) {
func (s *ServicesHandler) GetAccountsInNamespaceMap(ctx context.Context, namespace string) (accounts map[string]storage.Account, err error) {
accounts = map[string]storage.Account{}
request := &mobilityaccounts.GetAccountsRequest{
Namespaces: []string{namespace},
}
resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request)
if err == nil {
for _, v := range resp.Accounts {
@@ -108,12 +108,12 @@ func (s *ServicesHandler) GetAccountsInNamespaceMap(namespace string) (accounts
return
}
func (s *ServicesHandler) GetAccountsInNamespacesMap(namespaces []string) (accounts map[string]storage.Account, err error) {
func (s *ServicesHandler) GetAccountsInNamespacesMap(ctx context.Context, namespaces []string) (accounts map[string]storage.Account, err error) {
accounts = map[string]storage.Account{}
request := &mobilityaccounts.GetAccountsRequest{
Namespaces: namespaces,
}
resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request)
if err == nil {
for _, v := range resp.Accounts {
@@ -124,11 +124,11 @@ func (s *ServicesHandler) GetAccountsInNamespacesMap(namespaces []string) (accou
return
}
func (s *ServicesHandler) GetAccount(id string) (account storage.Account, err error) {
func (s *ServicesHandler) GetAccount(ctx context.Context, id string) (account storage.Account, err error) {
request := &mobilityaccounts.GetAccountRequest{
Id: id,
}
resp, err := s.GRPC.MobilityAccounts.GetAccount(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccount(ctx, request)
if err != nil {
return storage.Account{}, err
}
@@ -136,7 +136,7 @@ func (s *ServicesHandler) GetAccount(id string) (account storage.Account, err er
return resp.Account.ToStorageType(), nil
}
func (s *ServicesHandler) GetBeneficiariesInGroup(group groupstorage.Group) (accounts []storage.Account, err error) {
func (s *ServicesHandler) GetBeneficiariesInGroup(ctx context.Context, group groupstorage.Group) (accounts []storage.Account, err error) {
accounts = []storage.Account{}
if len(group.Members) == 0 {
@@ -147,7 +147,7 @@ func (s *ServicesHandler) GetBeneficiariesInGroup(group groupstorage.Group) (acc
Accountids: group.Members,
}
resp, err := s.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), request)
resp, err := s.GRPC.MobilityAccounts.GetAccountsBatch(ctx, request)
if err != nil {
return accounts, err
}