Compare commits
11 Commits
solidarity
...
039111c36c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
039111c36c | ||
|
|
95365ff8ce | ||
|
|
b79cc08b06 | ||
|
|
bb525f174d | ||
|
|
8d89306a90 | ||
|
|
549ea35a8c | ||
|
|
a60466d891 | ||
|
|
092d1acfbd | ||
|
|
1b1c4443fc | ||
|
|
b5e722ff9b | ||
| 2333bba79b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,5 @@ themes/*
|
||||
.vscode
|
||||
__debug_bin
|
||||
parcoursmob
|
||||
public_themes/
|
||||
.idea
|
||||
|
||||
@@ -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
|
||||
|
||||
24
config.go
24
config.go
@@ -27,6 +27,7 @@ func ReadConfig() (*viper.Viper, error) {
|
||||
"enabled": false,
|
||||
"listen": "0.0.0.0:8082",
|
||||
"root_dir": "public_themes/default",
|
||||
"contact_email": "contact@example.com",
|
||||
},
|
||||
},
|
||||
"identification": map[string]any{
|
||||
@@ -231,6 +232,25 @@ func ReadConfig() (*viper.Viper, error) {
|
||||
},
|
||||
"vehicles": map[string]any{
|
||||
"enabled": true,
|
||||
"default_booking_duration_days": 90,
|
||||
"status_management": "automatic",
|
||||
"booking_extra_properties": []map[string]any{
|
||||
{"name": "start_kilometers", "label": "Kilométrage de départ", "type": "number"},
|
||||
{"name": "enddate", "label": "Date et heure de restitution", "type": "datetime-local", "target": "enddate"},
|
||||
{"name": "end_kilometers", "label": "Kilométrage de fin", "type": "number"},
|
||||
{"name": "kilometers_done", "label": "Kilomètres réalisés", "type": "computed", "operation": "subtract", "operands": []string{"end_kilometers", "start_kilometers"}, "unit": "km"},
|
||||
{"name": "loan_duration", "label": "Durée du prêt", "type": "computed", "operation": "duration", "operands": []string{"booking.startdate", "booking.enddate"}},
|
||||
{"name": "unavailableto", "label": "Sera à nouveau disponible le", "type": "date", "target": "unavailableto"},
|
||||
},
|
||||
"status_options": []map[string]any{
|
||||
{"name": "requested", "label": "Demandé", "initial": true, "meta_status": "open"},
|
||||
{"name": "accepted", "label": "Accepté", "meta_status": "active"},
|
||||
{"name": "en_pret", "label": "En prêt", "meta_status": "active", "requested_properties": []map[string]any{{"name": "start_kilometers", "required": true}, {"name": "enddate"}}},
|
||||
{"name": "completed", "label": "Terminé", "meta_status": "closed", "requested_properties": []map[string]any{{"name": "end_kilometers", "required": true}, {"name": "unavailableto"}}},
|
||||
{"name": "refused", "label": "Refusé", "meta_status": "closed"},
|
||||
{"name": "cancelled", "label": "Annulé", "meta_status": "closed"},
|
||||
{"name": "not_completed", "label": "Non réalisé", "meta_status": "closed"},
|
||||
},
|
||||
},
|
||||
"vehicles_management": map[string]any{
|
||||
"enabled": true,
|
||||
@@ -253,11 +273,11 @@ func ReadConfig() (*viper.Viper, error) {
|
||||
"geo": map[string]any{
|
||||
"type": "addok", // Options: "pelias", "addok"
|
||||
"pelias": map[string]any{
|
||||
"url": "https://geocode.ridygo.fr",
|
||||
"url": "http://57.128.110.46:4000/V1",
|
||||
"autocomplete": "/autocomplete?text=",
|
||||
},
|
||||
"addok": map[string]any{
|
||||
"url": "https://api-adresse.data.gouv.fr",
|
||||
"url": "https://data.geopf.fr/geocodage/",
|
||||
"autocomplete": "/search/?q=",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import (
|
||||
solidaritytransformers "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
|
||||
solidaritytypes "git.coopgo.io/coopgo-platform/solidarity-transport/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/paulmach/orb"
|
||||
"github.com/paulmach/orb/geojson"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
@@ -41,12 +43,38 @@ type BeneficiariesResult struct {
|
||||
CacheID string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetBeneficiaries(ctx context.Context, searchFilter string, archivedFilter bool) (*BeneficiariesResult, error) {
|
||||
func (h *ApplicationHandler) GetBeneficiaries(ctx context.Context, searchFilter string, archivedFilter bool, addressGeoLayer, addressGeoCode string) (*BeneficiariesResult, error) {
|
||||
accounts, err := h.getBeneficiariesWithFilters(ctx, searchFilter, archivedFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply address geography filtering
|
||||
if addressGeoLayer != "" && addressGeoCode != "" {
|
||||
addressPolygons, err := h.loadGeographyPolygon(addressGeoLayer, addressGeoCode)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to load beneficiary address geography filter")
|
||||
} else {
|
||||
filtered := []mobilityaccountsstorage.Account{}
|
||||
for _, account := range accounts {
|
||||
if addr, ok := account.Data["address"]; ok {
|
||||
jsonAddr, err := json.Marshal(addr)
|
||||
if err == nil {
|
||||
addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
|
||||
if err == nil && addrGeojson.Geometry != nil {
|
||||
if point, ok := addrGeojson.Geometry.(orb.Point); ok {
|
||||
if isPointInGeographies(point, addressPolygons) {
|
||||
filtered = append(filtered, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
accounts = filtered
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sorting.BeneficiariesByName(accounts))
|
||||
|
||||
cacheID := uuid.NewString()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ type SearchJourneysResult struct {
|
||||
Drivers map[string]mobilityaccountsstorage.Account
|
||||
OrganizedCarpools []*carpoolproto.CarpoolServiceDriverJourney
|
||||
KnowledgeBaseResults []any
|
||||
DriverLastTrips map[string]time.Time // Map of driver ID to their last completed trip date
|
||||
LastTripDays int // Number of days to look back for last trips
|
||||
}
|
||||
|
||||
// SearchJourneyOptions contains per-request options for journey search
|
||||
@@ -46,6 +48,7 @@ type SearchJourneyOptions struct {
|
||||
DisableTransit bool
|
||||
DisableFleetVehicles bool
|
||||
DisableKnowledgeBase bool
|
||||
SolidarityTransportNoreturn *bool
|
||||
}
|
||||
|
||||
// SearchJourneys performs the business logic for journey search
|
||||
@@ -91,7 +94,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 +102,24 @@ 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
|
||||
}
|
||||
// Pass noreturn to filter journeys by type (one-way vs round-trip)
|
||||
if options.SolidarityTransportNoreturn != nil {
|
||||
req.Noreturn = *options.SolidarityTransportNoreturn
|
||||
}
|
||||
|
||||
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 +135,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
|
||||
}
|
||||
@@ -305,6 +298,33 @@ func (h *ApplicationHandler) SearchJourneys(
|
||||
}
|
||||
}
|
||||
|
||||
// Get last trip dates for solidarity transport drivers
|
||||
driverLastTrips := make(map[string]time.Time)
|
||||
lastTripDays := h.config.GetInt("modules.journeys.solutions.solidarity_transport.last_trip_days")
|
||||
if lastTripDays <= 0 {
|
||||
lastTripDays = 15
|
||||
}
|
||||
if len(solidarityTransportResults) > 0 {
|
||||
// Get all validated bookings from the past N days to find last trips
|
||||
bookingsRequest := &gen.GetSolidarityTransportBookingsRequest{
|
||||
StartDate: timestamppb.New(departureDateTime.Add(-time.Duration(lastTripDays) * 24 * time.Hour)),
|
||||
EndDate: timestamppb.New(departureDateTime.Add(24 * time.Hour)),
|
||||
Status: "VALIDATED",
|
||||
}
|
||||
bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, bookingsRequest)
|
||||
if err == nil {
|
||||
for _, booking := range bookingsResp.Bookings {
|
||||
if booking.Journey != nil {
|
||||
tripDate := booking.Journey.PassengerPickupDate.AsTime()
|
||||
// Only consider trips that have already happened
|
||||
if lastTrip, exists := driverLastTrips[booking.DriverId]; !exists || tripDate.After(lastTrip) {
|
||||
driverLastTrips[booking.DriverId] = tripDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &SearchJourneysResult{
|
||||
CarpoolResults: carpoolResults,
|
||||
TransitResults: transitResults,
|
||||
@@ -314,6 +334,8 @@ func (h *ApplicationHandler) SearchJourneys(
|
||||
Drivers: drivers,
|
||||
OrganizedCarpools: organizedCarpoolResults,
|
||||
KnowledgeBaseResults: knowledgeBaseResults,
|
||||
DriverLastTrips: driverLastTrips,
|
||||
LastTripDays: lastTripDays,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
47
core/application/search.go
Normal file
47
core/application/search.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GlobalSearchResult struct {
|
||||
Beneficiaries []mobilityaccountsstorage.Account
|
||||
SolidarityDrivers []mobilityaccountsstorage.Account
|
||||
OrganizedCarpoolDrivers []mobilityaccountsstorage.Account
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GlobalSearch(ctx context.Context, query string) (*GlobalSearchResult, error) {
|
||||
result := &GlobalSearchResult{}
|
||||
|
||||
if h.config.GetBool("modules.beneficiaries.enabled") {
|
||||
beneficiaries, err := h.getBeneficiariesWithFilters(ctx, query, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("global search: error retrieving beneficiaries")
|
||||
} else {
|
||||
result.Beneficiaries = beneficiaries
|
||||
}
|
||||
}
|
||||
|
||||
if h.config.GetBool("modules.solidarity_transport.enabled") {
|
||||
drivers, err := h.solidarityDrivers(ctx, query, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("global search: error retrieving solidarity drivers")
|
||||
} else {
|
||||
result.SolidarityDrivers = drivers
|
||||
}
|
||||
}
|
||||
|
||||
if h.config.GetBool("modules.organized_carpool.enabled") {
|
||||
drivers, err := h.getOrganizedCarpoolDrivers(ctx, query, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("global search: error retrieving organized carpool drivers")
|
||||
} else {
|
||||
result.OrganizedCarpoolDrivers = drivers
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -689,7 +689,7 @@ func (h *ApplicationHandler) UpdateSolidarityTransportDriver(ctx context.Context
|
||||
|
||||
type SolidarityTransportDriverDataResult struct {
|
||||
Driver mobilityaccountsstorage.Account
|
||||
Availabilities []*gen.DriverRegularAvailability
|
||||
Availabilities []*solidaritytypes.DriverRegularAvailability
|
||||
Documents []filestorage.FileInfo
|
||||
Bookings []*solidaritytypes.Booking
|
||||
BeneficiariesMap map[string]mobilityaccountsstorage.Account
|
||||
@@ -733,6 +733,12 @@ func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Contex
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert proto availabilities to types with deserialized geojson address
|
||||
availabilities := []*solidaritytypes.DriverRegularAvailability{}
|
||||
for _, protoAvail := range availResp.Results {
|
||||
availabilities = append(availabilities, solidaritytransformers.DriverRegularAvailabilityProtoToType(protoAvail))
|
||||
}
|
||||
|
||||
// Get documents
|
||||
documents := h.filestorage.List(filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS + "/" + driverID)
|
||||
|
||||
@@ -812,7 +818,7 @@ func (h *ApplicationHandler) GetSolidarityTransportDriverData(ctx context.Contex
|
||||
|
||||
return &SolidarityTransportDriverDataResult{
|
||||
Driver: driver,
|
||||
Availabilities: availResp.Results,
|
||||
Availabilities: availabilities,
|
||||
Documents: documents,
|
||||
Bookings: bookings,
|
||||
BeneficiariesMap: beneficiariesMap,
|
||||
@@ -1030,7 +1036,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 +1054,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 +1099,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 +1177,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 +1254,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 +1326,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,11 +1382,26 @@ 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
history = history + len(solidarity.Bookings)
|
||||
for _, booking := range solidarity.Bookings {
|
||||
if booking.Status == "VALIDATED" || booking.Status == "WAITING_CONFIRMATION" {
|
||||
history++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var passengerGeo pricing.GeographyParams
|
||||
@@ -1459,7 +1480,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 +1488,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
|
||||
|
||||
@@ -3,7 +3,10 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
@@ -28,7 +31,7 @@ type VehiclesManagementOverviewResult struct {
|
||||
Bookings []fleetsstorage.Booking
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context, groupID string) (*VehiclesManagementOverviewResult, error) {
|
||||
func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context, groupID, vehicleType, status, dateStart, dateEnd, vType, vStatus string) (*VehiclesManagementOverviewResult, error) {
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
}
|
||||
@@ -41,14 +44,82 @@ func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context,
|
||||
bookings := []fleetsstorage.Booking{}
|
||||
vehiclesMap := map[string]fleetsstorage.Vehicle{}
|
||||
|
||||
isManualStatus := h.config.GetString("modules.vehicles.status_management") == "manual"
|
||||
|
||||
// Parse date filters
|
||||
var startdate time.Time
|
||||
if dateStart != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", dateStart); err == nil {
|
||||
startdate = parsed
|
||||
}
|
||||
}
|
||||
var enddate time.Time
|
||||
if dateEnd != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", dateEnd); err == nil {
|
||||
enddate = parsed.Add(24 * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
for _, vehicle := range resp.Vehicles {
|
||||
if h.filterVehicleByGroup(vehicle, groupID) {
|
||||
v := vehicle.ToStorageType()
|
||||
vehicleBookings := []fleetsstorage.Booking{}
|
||||
for _, b := range v.Bookings {
|
||||
log.Debug().Any("booking", b).Msg("debug")
|
||||
if b.Status() != fleetsstorage.StatusOld {
|
||||
if !b.Deleted {
|
||||
include := true
|
||||
|
||||
// Apply date filters (intersection: booking overlaps with [startdate, enddate])
|
||||
if !startdate.IsZero() && b.Enddate.Before(startdate) {
|
||||
include = false
|
||||
}
|
||||
if !enddate.IsZero() && b.Startdate.After(enddate) {
|
||||
include = false
|
||||
}
|
||||
|
||||
// Apply vehicle type filter on bookings
|
||||
if include && vehicleType != "" && v.Type != vehicleType {
|
||||
include = false
|
||||
}
|
||||
|
||||
// Apply status filter on bookings
|
||||
if include && status != "" {
|
||||
isAdminUnavail, _ := b.Data["administrator_unavailability"].(bool)
|
||||
if isAdminUnavail {
|
||||
include = false
|
||||
} else if isManualStatus {
|
||||
if strings.HasPrefix(status, "meta:") {
|
||||
metaParts := strings.Split(strings.TrimPrefix(status, "meta:"), ",")
|
||||
metaSet := map[string]bool{}
|
||||
for _, ms := range metaParts {
|
||||
metaSet[ms] = true
|
||||
}
|
||||
allowedStatuses := h.resolveMetaStatuses(metaSet)
|
||||
if !allowedStatuses[b.ManualStatus] {
|
||||
include = false
|
||||
}
|
||||
} else if b.ManualStatus != status {
|
||||
include = false
|
||||
}
|
||||
} else {
|
||||
var filterStatusInt int
|
||||
switch status {
|
||||
case "FORTHCOMING":
|
||||
filterStatusInt = 1
|
||||
case "ONGOING":
|
||||
filterStatusInt = 0
|
||||
case "TERMINATED":
|
||||
filterStatusInt = -1
|
||||
default:
|
||||
filterStatusInt = 999
|
||||
}
|
||||
if b.Status() != filterStatusInt {
|
||||
include = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if include {
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
}
|
||||
@@ -57,12 +128,52 @@ func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context,
|
||||
}
|
||||
}
|
||||
v.Bookings = vehicleBookings
|
||||
vehicles = append(vehicles, v)
|
||||
|
||||
// Always add to vehiclesMap for booking lookups
|
||||
vehiclesMap[v.ID] = v
|
||||
|
||||
// Apply vehicle type filter
|
||||
if vType != "" && v.Type != vType {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply vehicle reservation status filter
|
||||
if vStatus != "" {
|
||||
hasActive := false
|
||||
hasUpcoming := false
|
||||
hasRetired := false
|
||||
for _, b := range v.Bookings {
|
||||
isAdminUnavail, _ := b.Data["administrator_unavailability"].(bool)
|
||||
if isAdminUnavail {
|
||||
hasRetired = true
|
||||
} else if b.Status() == 0 {
|
||||
hasActive = true
|
||||
} else if b.Status() == 1 {
|
||||
hasUpcoming = true
|
||||
}
|
||||
}
|
||||
|
||||
driversMap, _ := h.services.GetBeneficiariesMap()
|
||||
match := false
|
||||
switch vStatus {
|
||||
case "DISPONIBLE":
|
||||
match = !hasActive && !hasUpcoming && !hasRetired
|
||||
case "RESERVE":
|
||||
match = hasUpcoming
|
||||
case "EN_PRET":
|
||||
match = hasActive
|
||||
case "RETIRE":
|
||||
match = hasRetired
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
vehicles = append(vehicles, v)
|
||||
}
|
||||
}
|
||||
|
||||
driversMap, _ := h.services.GetBeneficiariesMap(ctx)
|
||||
|
||||
sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
|
||||
sort.Sort(sorting.BookingsByStartdate(bookings))
|
||||
@@ -95,7 +206,7 @@ type VehiclesManagementBookingsListResult struct {
|
||||
CacheID string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Context, groupID, status, startDate, endDate string) (*VehiclesManagementBookingsListResult, error) {
|
||||
func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Context, groupID, status, startDate, endDate, vehicleType string) (*VehiclesManagementBookingsListResult, error) {
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
IncludeDeleted: true,
|
||||
@@ -129,9 +240,29 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte
|
||||
v := vehicle.ToStorageType()
|
||||
vehiclesMap[v.ID] = v
|
||||
for _, b := range v.Bookings {
|
||||
if v, ok := b.Data["administrator_unavailability"].(bool); !ok || !v {
|
||||
if isAdminUnavail, ok := b.Data["administrator_unavailability"].(bool); !ok || !isAdminUnavail {
|
||||
// Apply vehicle type filter
|
||||
if vehicleType != "" && v.Type != vehicleType {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply status filter
|
||||
if status != "" {
|
||||
if h.config.GetString("modules.vehicles.status_management") == "manual" {
|
||||
if strings.HasPrefix(status, "meta:") {
|
||||
metaParts := strings.Split(strings.TrimPrefix(status, "meta:"), ",")
|
||||
metaSet := map[string]bool{}
|
||||
for _, ms := range metaParts {
|
||||
metaSet[ms] = true
|
||||
}
|
||||
allowedStatuses := h.resolveMetaStatuses(metaSet)
|
||||
if !allowedStatuses[b.ManualStatus] {
|
||||
continue
|
||||
}
|
||||
} else if b.ManualStatus != status {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
bookingStatus := b.Status()
|
||||
statusInt := 0
|
||||
|
||||
@@ -160,9 +291,10 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply date filter (on startdate)
|
||||
if !startdate.IsZero() && b.Startdate.Before(startdate) {
|
||||
// Apply date filter (intersection: booking overlaps with [startdate, enddate])
|
||||
if !startdate.IsZero() && b.Enddate.Before(startdate) {
|
||||
continue
|
||||
}
|
||||
if !enddate.IsZero() && b.Startdate.After(enddate) {
|
||||
@@ -180,7 +312,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 +395,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)
|
||||
}
|
||||
@@ -331,6 +463,7 @@ type BookingDisplayResult struct {
|
||||
Documents []filestorage.FileInfo
|
||||
FileTypesMap map[string]string
|
||||
Alternatives []any
|
||||
ComputedExtraProperties map[string]string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID string) (*BookingDisplayResult, error) {
|
||||
@@ -382,6 +515,8 @@ func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID st
|
||||
documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingID)
|
||||
fileTypesMap := h.config.GetStringMapString("storage.files.file_types")
|
||||
|
||||
computedProps := h.computeExtraProperties(booking)
|
||||
|
||||
return &BookingDisplayResult{
|
||||
Booking: booking,
|
||||
Vehicle: booking.Vehicle,
|
||||
@@ -390,6 +525,7 @@ func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID st
|
||||
Documents: documents,
|
||||
FileTypesMap: fileTypesMap,
|
||||
Alternatives: alternatives,
|
||||
ComputedExtraProperties: computedProps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -596,3 +732,351 @@ func (h *ApplicationHandler) UpdateVehicle(ctx context.Context, vehicleID, name,
|
||||
return updateResp.Vehicle.Id, nil
|
||||
}
|
||||
|
||||
// getStatusOptions extracts status options from Viper config, handling both
|
||||
// Go defaults ([]map[string]any) and YAML-parsed ([]interface{}) types.
|
||||
func getStatusOptions(raw interface{}) []map[string]any {
|
||||
if options, ok := raw.([]map[string]any); ok {
|
||||
return options
|
||||
}
|
||||
if rawSlice, ok := raw.([]interface{}); ok {
|
||||
result := make([]map[string]any, 0, len(rawSlice))
|
||||
for _, item := range rawSlice {
|
||||
if opt, ok := item.(map[string]any); ok {
|
||||
result = append(result, opt)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBookingExtraProperties extracts booking extra property definitions from Viper config,
|
||||
// handling both Go defaults ([]map[string]any) and YAML-parsed ([]interface{}) types.
|
||||
func getBookingExtraProperties(raw interface{}) []map[string]any {
|
||||
if props, ok := raw.([]map[string]any); ok {
|
||||
return props
|
||||
}
|
||||
if rawSlice, ok := raw.([]interface{}); ok {
|
||||
result := make([]map[string]any, 0, len(rawSlice))
|
||||
for _, item := range rawSlice {
|
||||
if prop, ok := item.(map[string]any); ok {
|
||||
result = append(result, prop)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) UpdateBookingStatus(ctx context.Context, bookingID, newStatus, comment, userID string, userClaims map[string]any, currentGroup any, extraProperties map[string]string) error {
|
||||
group := currentGroup.(storage.Group)
|
||||
|
||||
// Validate that newStatus is a valid status option
|
||||
options := getStatusOptions(h.config.Get("modules.vehicles.status_options"))
|
||||
validStatus := false
|
||||
for _, opt := range options {
|
||||
if name, ok := opt["name"].(string); ok && name == newStatus {
|
||||
validStatus = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validStatus {
|
||||
return fmt.Errorf("invalid status: %s", newStatus)
|
||||
}
|
||||
|
||||
booking, err := h.services.GetBooking(bookingID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get booking: %w", err)
|
||||
}
|
||||
|
||||
oldStatus := booking.ManualStatus
|
||||
|
||||
booking.ManualStatus = newStatus
|
||||
booking.StatusHistory = append(booking.StatusHistory, fleetsstorage.StatusHistoryEntry{
|
||||
FromStatus: oldStatus,
|
||||
ToStatus: newStatus,
|
||||
UserID: userID,
|
||||
UserName: fmt.Sprintf("%s %s", userClaims["first_name"], userClaims["last_name"]),
|
||||
GroupID: group.ID,
|
||||
GroupName: fmt.Sprintf("%s", group.Data["name"]),
|
||||
Date: time.Now(),
|
||||
Comment: comment,
|
||||
})
|
||||
|
||||
// Process extra properties
|
||||
if len(extraProperties) > 0 {
|
||||
propDefs := getBookingExtraProperties(h.config.Get("modules.vehicles.booking_extra_properties"))
|
||||
|
||||
// Build a lookup map of property definitions by name
|
||||
propDefsMap := map[string]map[string]any{}
|
||||
for _, def := range propDefs {
|
||||
if name, ok := def["name"].(string); ok {
|
||||
propDefsMap[name] = def
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize extra_properties in booking data if needed
|
||||
storedExtras, _ := booking.Data["extra_properties"].(map[string]any)
|
||||
if storedExtras == nil {
|
||||
storedExtras = map[string]any{}
|
||||
}
|
||||
|
||||
for propName, propValue := range extraProperties {
|
||||
if propValue == "" {
|
||||
continue
|
||||
}
|
||||
def, exists := propDefsMap[propName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if target, ok := def["target"].(string); ok && target != "" {
|
||||
// Update structural booking field
|
||||
switch target {
|
||||
case "unavailableto":
|
||||
if t, err := time.Parse("2006-01-02", propValue); err == nil {
|
||||
booking.Unavailableto = t
|
||||
}
|
||||
case "unavailablefrom":
|
||||
if t, err := time.Parse("2006-01-02", propValue); err == nil {
|
||||
booking.Unavailablefrom = t
|
||||
}
|
||||
case "enddate":
|
||||
paris, _ := time.LoadLocation("Europe/Paris")
|
||||
if t, err := time.ParseInLocation("2006-01-02T15:04", propValue, paris); err == nil {
|
||||
booking.Enddate = t
|
||||
if t.After(booking.Unavailableto) || t.Equal(booking.Unavailableto) {
|
||||
booking.Unavailableto = t.Add(24 * time.Hour)
|
||||
}
|
||||
}
|
||||
case "startdate":
|
||||
paris, _ := time.LoadLocation("Europe/Paris")
|
||||
if t, err := time.ParseInLocation("2006-01-02T15:04", propValue, paris); err == nil {
|
||||
booking.Startdate = t
|
||||
if t.Before(booking.Unavailablefrom) {
|
||||
booking.Unavailablefrom = t
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Store in extra_properties
|
||||
storedExtras[propName] = propValue
|
||||
}
|
||||
}
|
||||
|
||||
booking.Data["extra_properties"] = storedExtras
|
||||
}
|
||||
|
||||
b, err := fleets.BookingFromStorageType(&booking)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert booking: %w", err)
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Fleets.UpdateBooking(ctx, &fleets.UpdateBookingRequest{
|
||||
Booking: b,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Computed extra properties ---
|
||||
|
||||
func (h *ApplicationHandler) computeExtraProperties(booking fleetsstorage.Booking) map[string]string {
|
||||
defs := getBookingExtraProperties(h.config.Get("modules.vehicles.booking_extra_properties"))
|
||||
extras, _ := booking.Data["extra_properties"].(map[string]any)
|
||||
if extras == nil {
|
||||
extras = map[string]any{}
|
||||
}
|
||||
|
||||
result := map[string]string{}
|
||||
for _, def := range defs {
|
||||
defType, _ := def["type"].(string)
|
||||
if defType != "computed" {
|
||||
continue
|
||||
}
|
||||
name, _ := def["name"].(string)
|
||||
operation, _ := def["operation"].(string)
|
||||
unit, _ := def["unit"].(string)
|
||||
operands := resolveStringSlice(def["operands"])
|
||||
if name == "" || operation == "" || len(operands) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
val := evaluateOperation(operation, operands, booking, extras)
|
||||
if val != "" {
|
||||
if unit != "" {
|
||||
val = val + " " + unit
|
||||
}
|
||||
result[name] = val
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// resolveStringSlice handles both Go defaults ([]string) and YAML-parsed ([]interface{}) types.
|
||||
func resolveStringSlice(raw interface{}) []string {
|
||||
if s, ok := raw.([]string); ok {
|
||||
return s
|
||||
}
|
||||
if rawSlice, ok := raw.([]interface{}); ok {
|
||||
result := make([]string, 0, len(rawSlice))
|
||||
for _, item := range rawSlice {
|
||||
if str, ok := item.(string); ok {
|
||||
result = append(result, str)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func evaluateOperation(operation string, operands []string, booking fleetsstorage.Booking, extras map[string]any) string {
|
||||
switch operation {
|
||||
case "add":
|
||||
if len(operands) < 2 {
|
||||
return ""
|
||||
}
|
||||
a, okA := resolveNumericOperand(operands[0], booking, extras)
|
||||
b, okB := resolveNumericOperand(operands[1], booking, extras)
|
||||
if !okA || !okB {
|
||||
return ""
|
||||
}
|
||||
return formatNumber(a + b)
|
||||
|
||||
case "subtract":
|
||||
if len(operands) < 2 {
|
||||
return ""
|
||||
}
|
||||
a, okA := resolveNumericOperand(operands[0], booking, extras)
|
||||
b, okB := resolveNumericOperand(operands[1], booking, extras)
|
||||
if !okA || !okB {
|
||||
return ""
|
||||
}
|
||||
return formatNumber(a - b)
|
||||
|
||||
case "multiply":
|
||||
if len(operands) < 2 {
|
||||
return ""
|
||||
}
|
||||
a, okA := resolveNumericOperand(operands[0], booking, extras)
|
||||
b, okB := resolveNumericOperand(operands[1], booking, extras)
|
||||
if !okA || !okB {
|
||||
return ""
|
||||
}
|
||||
return formatNumber(a * b)
|
||||
|
||||
case "divide":
|
||||
if len(operands) < 2 {
|
||||
return ""
|
||||
}
|
||||
a, okA := resolveNumericOperand(operands[0], booking, extras)
|
||||
b, okB := resolveNumericOperand(operands[1], booking, extras)
|
||||
if !okA || !okB || b == 0 {
|
||||
return ""
|
||||
}
|
||||
return formatNumber(a / b)
|
||||
|
||||
case "duration":
|
||||
if len(operands) < 2 {
|
||||
return ""
|
||||
}
|
||||
d1, ok1 := resolveDateOperand(operands[0], booking, extras)
|
||||
d2, ok2 := resolveDateOperand(operands[1], booking, extras)
|
||||
if !ok1 || !ok2 {
|
||||
return ""
|
||||
}
|
||||
return formatDuration(d2.Sub(d1))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveNumericOperand(name string, booking fleetsstorage.Booking, extras map[string]any) (float64, bool) {
|
||||
if v, ok := extras[name]; ok {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val, true
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(val, 64); err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func resolveDateOperand(name string, booking fleetsstorage.Booking, extras map[string]any) (time.Time, bool) {
|
||||
if strings.HasPrefix(name, "booking.") {
|
||||
field := strings.TrimPrefix(name, "booking.")
|
||||
switch field {
|
||||
case "startdate":
|
||||
return booking.Startdate, !booking.Startdate.IsZero()
|
||||
case "enddate":
|
||||
return booking.Enddate, !booking.Enddate.IsZero()
|
||||
case "unavailablefrom":
|
||||
return booking.Unavailablefrom, !booking.Unavailablefrom.IsZero()
|
||||
case "unavailableto":
|
||||
return booking.Unavailableto, !booking.Unavailableto.IsZero()
|
||||
}
|
||||
}
|
||||
if v, ok := extras[name]; ok {
|
||||
if dateStr, ok := v.(string); ok {
|
||||
for _, layout := range []string{"2006-01-02T15:04", "2006-01-02"} {
|
||||
if t, err := time.Parse(layout, dateStr); err == nil {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func formatNumber(v float64) string {
|
||||
if v == math.Trunc(v) {
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
return strconv.FormatFloat(v, 'f', 2, 64)
|
||||
}
|
||||
|
||||
// resolveMetaStatuses returns a set of status names matching the given meta_status values.
|
||||
// metaValues is a set like {"open": true, "active": true}.
|
||||
func (h *ApplicationHandler) resolveMetaStatuses(metaValues map[string]bool) map[string]bool {
|
||||
result := map[string]bool{}
|
||||
options := getStatusOptions(h.config.Get("modules.vehicles.status_options"))
|
||||
for _, opt := range options {
|
||||
ms, _ := opt["meta_status"].(string)
|
||||
name, _ := opt["name"].(string)
|
||||
if metaValues[ms] {
|
||||
result[name] = true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < 0 {
|
||||
d = -d
|
||||
}
|
||||
totalHours := int(d.Hours())
|
||||
days := totalHours / 24
|
||||
hours := totalHours % 24
|
||||
|
||||
parts := []string{}
|
||||
if days > 0 {
|
||||
s := "jour"
|
||||
if days > 1 {
|
||||
s = "jours"
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%d %s", days, s))
|
||||
}
|
||||
if hours > 0 {
|
||||
s := "heure"
|
||||
if hours > 1 {
|
||||
s = "heures"
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%d %s", hours, s))
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "0 heure"
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID s
|
||||
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)
|
||||
}
|
||||
@@ -210,6 +210,29 @@ func (h *ApplicationHandler) BookVehicle(ctx context.Context, vehicleID, benefic
|
||||
Data: datapb,
|
||||
}
|
||||
|
||||
if h.config.GetString("modules.vehicles.status_management") == "manual" {
|
||||
options := getStatusOptions(h.config.Get("modules.vehicles.status_options"))
|
||||
for _, opt := range options {
|
||||
if initial, ok := opt["initial"].(bool); ok && initial {
|
||||
if name, ok := opt["name"].(string); ok {
|
||||
booking.ManualStatus = name
|
||||
booking.StatusHistory = []*fleets.StatusHistoryEntry{
|
||||
{
|
||||
ToStatus: name,
|
||||
UserId: currentUserID,
|
||||
UserName: fmt.Sprintf("%s %s", currentUserClaims["first_name"], currentUserClaims["last_name"]),
|
||||
GroupId: group.ID,
|
||||
GroupName: fmt.Sprintf("%s", group.Data["name"]),
|
||||
Date: timestamppb.Now(),
|
||||
Comment: "Création de la réservation",
|
||||
},
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request := &fleets.CreateBookingRequest{
|
||||
Booking: booking,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
4
go.mod
4
go.mod
@@ -48,7 +48,7 @@ require (
|
||||
git.coopgo.io/coopgo-platform/agenda v1.0.0
|
||||
git.coopgo.io/coopgo-platform/carpool-service v0.0.0-20251008165122-38cb3c5ad9b4
|
||||
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.0
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.1-0.20260226165510-6007cffdf152
|
||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858
|
||||
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c
|
||||
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386
|
||||
@@ -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
|
||||
|
||||
8
go.sum
8
go.sum
@@ -8,8 +8,8 @@ git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss v0.0.0-20251
|
||||
git.coopgo.io/coopgo-platform/carpool-service/interoperability/ocss v0.0.0-20251008142525-4392f227836a/go.mod h1:c9aJwNtY4PJuqAFYZ9afnx46UAZtWJ3P8ICZM02/DBA=
|
||||
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260 h1:Li3dotY6raKu9+oxEgICU7nwdomYpjgu19i3mZNiqTc=
|
||||
git.coopgo.io/coopgo-platform/emailing v0.0.0-20250212064257-167ef5864260/go.mod h1:6cvvjv0RLSwBthIQ4TiuZoXFGvQXZ55hNSJchWXAgB4=
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.0 h1:pfW/K3fWfap54yNfkLzBXjvOjjoTaEGFEqS/+VkHv7s=
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.0/go.mod h1:nuK2mi1M2+DdntinqK/8C4ttW4WWyKCCY/xD1D7XjkE=
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.1-0.20260226165510-6007cffdf152 h1:kczmeGHnihYQSopDzbQ23B+P8Fw3GuB6iACW7SuDE+s=
|
||||
git.coopgo.io/coopgo-platform/fleets v1.1.1-0.20260226165510-6007cffdf152/go.mod h1:nuK2mi1M2+DdntinqK/8C4ttW4WWyKCCY/xD1D7XjkE=
|
||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858 h1:4E0tbT8jj5oxaK66Ny61o7zqPaVc0qRN2cZG9IUR4Es=
|
||||
git.coopgo.io/coopgo-platform/geography v0.0.0-20251010131258-ec939649e858/go.mod h1:TbR3g1Awa8hpAe6LR1z1EQbv2IBVgN5JQ/FjXfKX4K0=
|
||||
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c h1:bY7PyrAgYY02f5IpDyf1WVfRqvWzivu31K6aEAYbWCw=
|
||||
@@ -26,8 +26,8 @@ git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463 h1
|
||||
git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463/go.mod h1:0fuGuYub5CBy9NB6YMqxawE0HoBaxPb9gmSw1gjfDy0=
|
||||
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af h1:KxHim1dFcOVbFhRqelec8cJ65QBD2cma6eytW8llgYY=
|
||||
git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af/go.mod h1:mad9D+WICDdpJzB+8H/wEVVbllK2mU6VLVByrppc9x0=
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc h1:NLU5DUo5Kt3jkPhV3KkqQMahTHIrGildBvYlHwJ6JmM=
|
||||
git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ=
|
||||
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=
|
||||
|
||||
2
main.go
2
main.go
@@ -86,7 +86,7 @@ func main() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
publicweb.Run(cfg, svc, applicationHandler, kv, filestorage)
|
||||
publicweb.Run(cfg, svc, applicationHandler, kv, filestorage, emailing)
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,15 +30,25 @@ func (s BeneficiariesListState) JSONWithLimits(a int, b int) template.JS {
|
||||
return s.JSON()
|
||||
}
|
||||
|
||||
func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string, archived bool) {
|
||||
func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string, archived bool, enrichedGeoFilters []map[string]string, selectedAddressGeo string) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.list.files")
|
||||
|
||||
geoFiltersEnabled := len(enrichedGeoFilters) > 0
|
||||
|
||||
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
|
||||
state.ViewState = BeneficiariesListState{
|
||||
state.ViewState = map[string]any{
|
||||
"list": BeneficiariesListState{
|
||||
Count: len(accounts),
|
||||
CacheId: cacheid,
|
||||
Beneficiaries: accounts,
|
||||
Archived: archived,
|
||||
},
|
||||
"geography_filters_enabled": geoFiltersEnabled,
|
||||
"geography_filters_list": enrichedGeoFilters,
|
||||
"archived": archived,
|
||||
"filters": map[string]any{
|
||||
"beneficiary_address_geo": selectedAddressGeo,
|
||||
},
|
||||
}
|
||||
|
||||
renderer.Render("beneficiaries_list", w, r, files, state)
|
||||
@@ -87,7 +97,7 @@ func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Requ
|
||||
renderer.Render("beneficiaries_display", w, r, files, state)
|
||||
}
|
||||
|
||||
func (renderer *Renderer) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request, beneficiary any) {
|
||||
func (renderer *Renderer) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request, beneficiary mobilityaccountsstorage.Account) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.update.files")
|
||||
profileFields := renderer.GlobalConfig.Get("modules.beneficiaries.profile_optional_fields")
|
||||
|
||||
@@ -96,6 +106,7 @@ func (renderer *Renderer) BeneficiaryUpdate(w http.ResponseWriter, r *http.Reque
|
||||
"beneficiary": beneficiary,
|
||||
"profile_optional_fields": profileFields,
|
||||
}
|
||||
state.DynamicData = beneficiary.Data
|
||||
|
||||
renderer.Render("beneficiaries_update", w, r, files, state)
|
||||
}
|
||||
|
||||
@@ -170,6 +170,14 @@ func IsGuaranteedTripMotivation(globalConfig *viper.Viper) func(string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsPast returns true if the given time is before the current time
|
||||
func IsPast(d any) bool {
|
||||
if date, ok := d.(time.Time); ok {
|
||||
return date.Before(time.Now())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTemplateFuncMap returns the common template functions for rendering
|
||||
func GetTemplateFuncMap(group groupsstorage.Group, globalConfig *viper.Viper, fileStorage filestorage.FileStorage) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
@@ -191,6 +199,7 @@ func GetTemplateFuncMap(group groupsstorage.Group, globalConfig *viper.Viper, fi
|
||||
"beneficiaryValidatedProfile": validatedprofile.ValidateProfile(globalConfig.Sub("modules.beneficiaries.validated_profile")),
|
||||
"solidarityDriverValidatedProfile": validatedprofile.ValidateProfile(globalConfig.Sub("modules.solidarity_transport.drivers.validated_profile")),
|
||||
"carpoolDriverValidatedProfile": validatedprofile.ValidateProfile(globalConfig.Sub("modules.organized_carpool.drivers.validated_profile")),
|
||||
"isPast": IsPast,
|
||||
"isGuaranteedTripMotivation": IsGuaranteedTripMotivation(globalConfig),
|
||||
"beneficiaryDocuments": func(id string) []filestorage.FileInfo {
|
||||
return fileStorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + id)
|
||||
|
||||
@@ -36,7 +36,7 @@ func (s BeneficiariesCovoiturage) JSONWithLimits(a int, b int) template.JS {
|
||||
return s.JSON()
|
||||
}
|
||||
|
||||
func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request, carpools []*geojson.FeatureCollection, transitjourneys any, vehicles any, searched bool, departure any, destination any, departuredate string, departuretime string, driverJourneys any, solidarityDrivers any, organizedCarpools any, beneficiaries any, kbData any, passengerid string, savedSearches any, beneficiariesMap any) {
|
||||
func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request, carpools []*geojson.FeatureCollection, transitjourneys any, vehicles any, searched bool, departure any, destination any, departuredate string, departuretime string, departuredatetime any, driverJourneys any, solidarityDrivers any, organizedCarpools any, beneficiaries any, kbData any, passengerid string, savedSearches any, beneficiariesMap any, driverLastTrips any, lastTripDays int) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.journeys.search.files")
|
||||
state := NewState(r, renderer.ThemeConfig, journeysMenu)
|
||||
journeyTabs := renderer.ThemeConfig.Get("journey_tabs")
|
||||
@@ -44,6 +44,7 @@ func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request,
|
||||
"searched": searched,
|
||||
"departuredate": departuredate,
|
||||
"departuretime": departuretime,
|
||||
"departuredatetime": departuredatetime,
|
||||
"departure": departure,
|
||||
"destination": destination,
|
||||
"journeys": transitjourneys,
|
||||
@@ -52,6 +53,8 @@ func (renderer *Renderer) JourneysSearch(w http.ResponseWriter, r *http.Request,
|
||||
"vehicles": vehicles,
|
||||
"driver_journeys": driverJourneys,
|
||||
"solidarity_drivers": solidarityDrivers,
|
||||
"driver_last_trips": driverLastTrips,
|
||||
"last_trip_days": lastTripDays,
|
||||
"querystring": r.URL.RawQuery,
|
||||
"beneficiaries": beneficiariesMap,
|
||||
"beneficiaries_list": beneficiaries,
|
||||
|
||||
@@ -56,6 +56,7 @@ func (renderer *Renderer) OrganizedCarpoolUpdateDriver(w http.ResponseWriter, r
|
||||
|
||||
func (renderer *Renderer) OrganizedCarpoolDriverDisplay(w http.ResponseWriter, r *http.Request, driver mobilityaccountsstorage.Account, trips any, documents any, bookings any, beneficiariesMap any, stats map[string]any, walletBalance float64, tab string) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.organized_carpool.driver_display.files")
|
||||
profileFields := renderer.GlobalConfig.Get("modules.organized_carpool.drivers.profile_optional_fields")
|
||||
state := NewState(r, renderer.ThemeConfig, organizedCarpoolMenu)
|
||||
|
||||
drivers_file_types := renderer.GlobalConfig.GetStringSlice("modules.organized_carpool.drivers.documents_types")
|
||||
@@ -70,6 +71,7 @@ func (renderer *Renderer) OrganizedCarpoolDriverDisplay(w http.ResponseWriter, r
|
||||
"stats": stats,
|
||||
"drivers_file_types": drivers_file_types,
|
||||
"file_types_map": file_types_map,
|
||||
"profile_optional_fields": profileFields,
|
||||
"wallet_balance": walletBalance,
|
||||
"tab": tab,
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +110,7 @@ type RenderState struct {
|
||||
Group storage.Group
|
||||
Roles any
|
||||
ViewState any // This is a state specific to a given view
|
||||
DynamicData any // Data to be serialized as JSON in a <script> tag for safe JS consumption
|
||||
}
|
||||
|
||||
func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) RenderState {
|
||||
|
||||
20
renderer/search.go
Normal file
20
renderer/search.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
)
|
||||
|
||||
func (renderer *Renderer) GlobalSearchResults(w http.ResponseWriter, r *http.Request, query string, beneficiaries []mobilityaccountsstorage.Account, solidarityDrivers []mobilityaccountsstorage.Account, organizedCarpoolDrivers []mobilityaccountsstorage.Account) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.search.results.files")
|
||||
state := NewState(r, renderer.ThemeConfig, "")
|
||||
state.ViewState = map[string]any{
|
||||
"query": query,
|
||||
"beneficiaries": beneficiaries,
|
||||
"solidarity_drivers": solidarityDrivers,
|
||||
"organized_carpool_drivers": organizedCarpoolDrivers,
|
||||
}
|
||||
|
||||
renderer.Render("search results", w, r, files, state)
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func (renderer *Renderer) SolidarityTransportDriverJourney(w http.ResponseWriter
|
||||
renderer.Render("solidarity transport driver creation", w, r, files, state)
|
||||
}
|
||||
|
||||
func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, passengerWalletBalance float64, replacementDrivers any, replacementDriversMap any, replacementPricing any, replacementLocations any) {
|
||||
func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, driver any, passenger any, passengerWalletBalance float64, replacementDrivers any, replacementDriversMap any, replacementPricing any, replacementLocations any, driverLastTrips any, lastTripDays int) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.solidarity_transport.booking_display.files")
|
||||
bookingMotivations := renderer.GlobalConfig.Get("modules.solidarity_transport.booking_motivations")
|
||||
state := NewState(r, renderer.ThemeConfig, solidarityTransportMenu)
|
||||
@@ -124,6 +124,8 @@ func (renderer *Renderer) SolidarityTransportBookingDisplay(w http.ResponseWrite
|
||||
"replacement_drivers_map": replacementDriversMap,
|
||||
"replacement_pricing": replacementPricing,
|
||||
"replacement_locations": replacementLocations,
|
||||
"driver_last_trips": driverLastTrips,
|
||||
"last_trip_days": lastTripDays,
|
||||
}
|
||||
|
||||
renderer.Render("booking display", w, r, files, state)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
const vehiclesmanagementMenu = "vehicles_management"
|
||||
|
||||
func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request, vehicles []fleetsstorage.Vehicle, vehicles_map map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking) {
|
||||
func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request, vehicles []fleetsstorage.Vehicle, vehicles_map map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking, filters map[string]string, vehicleTypes []string, tab string) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.overview.files")
|
||||
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
|
||||
state.ViewState = map[string]any{
|
||||
@@ -18,12 +18,18 @@ func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *h
|
||||
"bookings": bookings,
|
||||
"vehicles_map": vehicles_map,
|
||||
"drivers_map": driversMap,
|
||||
"tab": tab,
|
||||
"filters": filters,
|
||||
"vehicle_types": vehicleTypes,
|
||||
"hide_date_filters": false,
|
||||
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
|
||||
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
|
||||
}
|
||||
|
||||
renderer.Render("fleet overview", w, r, files, state)
|
||||
}
|
||||
|
||||
func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request, vehiclesMap map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking, cacheid string, filters map[string]string) {
|
||||
func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request, vehiclesMap map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account, bookings []fleetsstorage.Booking, cacheid string, filters map[string]string, vehicleTypes []string) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.bookings_list.files")
|
||||
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
|
||||
state.ViewState = map[string]any{
|
||||
@@ -32,6 +38,9 @@ func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter,
|
||||
"drivers_map": driversMap,
|
||||
"cacheid": cacheid,
|
||||
"filters": filters,
|
||||
"vehicle_types": vehicleTypes,
|
||||
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
|
||||
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
|
||||
}
|
||||
|
||||
renderer.Render("fleet overview", w, r, files, state)
|
||||
@@ -78,7 +87,7 @@ func (renderer *Renderer) VehiclesFleetUpdate(w http.ResponseWriter, r *http.Req
|
||||
renderer.Render("fleet display vehicle", w, r, files, state)
|
||||
}
|
||||
|
||||
func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any, documents []filestorage.FileInfo, file_types_map map[string]string, alternative_vehicles []any) {
|
||||
func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter, r *http.Request, booking any, vehicle any, beneficiary any, group any, documents []filestorage.FileInfo, file_types_map map[string]string, alternative_vehicles []any, computed_extra_properties map[string]string) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.booking_display.files")
|
||||
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
|
||||
state.ViewState = map[string]any{
|
||||
@@ -89,6 +98,10 @@ func (renderer *Renderer) VehicleManagementBookingDisplay(w http.ResponseWriter,
|
||||
"documents": documents,
|
||||
"file_types_map": file_types_map,
|
||||
"alternative_vehicles": alternative_vehicles,
|
||||
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
|
||||
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
|
||||
"booking_extra_properties": renderer.GlobalConfig.Get("modules.vehicles.booking_extra_properties"),
|
||||
"computed_extra_properties": computed_extra_properties,
|
||||
}
|
||||
|
||||
renderer.Render("vehicles search", w, r, files, state)
|
||||
|
||||
@@ -26,10 +26,13 @@ func selectDocumentsDefaults(beneficiarydocuments []filestorage.FileInfo, mandat
|
||||
func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []fleetsstorage.Vehicle, beneficiary any, startdate any, enddate any, mandatory_documents []string, file_types_map map[string]string, beneficiarydocuments []filestorage.FileInfo, selected_type string, automatic bool, vehicles_types []string, admingroups map[string]any) {
|
||||
files := renderer.ThemeConfig.GetStringSlice("views.vehicles.search.files")
|
||||
state := NewState(r, renderer.ThemeConfig, vehiclesMenu)
|
||||
defaultBookingDurationDays := renderer.GlobalConfig.GetInt("modules.vehicles.default_booking_duration_days")
|
||||
|
||||
viewstate := map[string]any{
|
||||
"beneficiaries": beneficiaries,
|
||||
"searched": searched,
|
||||
"vehicles_types": vehicles_types,
|
||||
"default_booking_duration_days": defaultBookingDurationDays,
|
||||
}
|
||||
|
||||
if searched {
|
||||
@@ -63,6 +66,8 @@ func (renderer *Renderer) VehicleBookingDisplay(w http.ResponseWriter, r *http.R
|
||||
"group": group,
|
||||
"documents": documents,
|
||||
"file_types_map": file_types_map,
|
||||
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
|
||||
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
|
||||
}
|
||||
|
||||
renderer.Render("vehicles search", w, r, files, state)
|
||||
@@ -75,6 +80,8 @@ func (renderer *Renderer) VehicleBookingsList(w http.ResponseWriter, r *http.Req
|
||||
"bookings": bookings,
|
||||
"vehicles_map": vehiclesMap,
|
||||
"groups_map": groupsMap,
|
||||
"status_management": renderer.GlobalConfig.GetString("modules.vehicles.status_management"),
|
||||
"status_options": renderer.GlobalConfig.Get("modules.vehicles.status_options"),
|
||||
}
|
||||
|
||||
renderer.Render("vehicles search", w, r, files, state)
|
||||
|
||||
99
renderer/xlsx/beneficiaries.go
Normal file
99
renderer/xlsx/beneficiaries.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package xlsx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/gender"
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type BeneficiaryGeoInfo struct {
|
||||
Commune string
|
||||
EPCI string
|
||||
Departement string
|
||||
Region string
|
||||
}
|
||||
|
||||
func (r *XLSXRenderer) Beneficiaries(w http.ResponseWriter, accounts []mobilityaccountsstorage.Account, geoInfoMap map[string]BeneficiaryGeoInfo) {
|
||||
spreadsheet := r.NewSpreadsheet("Bénéficiaires")
|
||||
|
||||
// Build headers dynamically based on configuration
|
||||
beneficiaryOptionalFields := r.Config.Get("modules.beneficiaries.profile_optional_fields")
|
||||
beneficiaryFields := []string{"last_name", "first_name", "email", "phone_number", "birthdate", "gender", "file_number"}
|
||||
headers := []string{"ID", "Nom", "Prénom", "Email", "Téléphone", "Date de naissance", "Genre", "Numéro de dossier"}
|
||||
|
||||
if beneficiaryOptionalFieldsList, ok := beneficiaryOptionalFields.([]interface{}); ok {
|
||||
for _, field := range beneficiaryOptionalFieldsList {
|
||||
if fieldMap, ok := field.(map[string]interface{}); ok {
|
||||
if name, ok := fieldMap["name"].(string); ok {
|
||||
beneficiaryFields = append(beneficiaryFields, name)
|
||||
label := name
|
||||
if labelVal, ok := fieldMap["label"].(string); ok {
|
||||
label = labelVal
|
||||
}
|
||||
headers = append(headers, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headers = append(headers, "Adresse", "Commune", "EPCI", "Département", "Région", "Archivé")
|
||||
|
||||
spreadsheet.SetHeaders(headers)
|
||||
|
||||
for _, account := range accounts {
|
||||
row := []interface{}{}
|
||||
|
||||
row = append(row, account.ID)
|
||||
|
||||
for _, field := range beneficiaryFields {
|
||||
value := getAccountFieldValue(account.Data, field)
|
||||
if field == "gender" && value != "" {
|
||||
value = gender.ISO5218ToString(value)
|
||||
}
|
||||
row = append(row, value)
|
||||
}
|
||||
|
||||
// Address
|
||||
address := ""
|
||||
if addr, ok := account.Data["address"]; ok {
|
||||
if addrMap, ok := addr.(map[string]interface{}); ok {
|
||||
if props, ok := addrMap["properties"]; ok {
|
||||
if propsMap, ok := props.(map[string]interface{}); ok {
|
||||
if label, ok := propsMap["label"].(string); ok {
|
||||
address = label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
row = append(row, address)
|
||||
|
||||
// Geographic info (Commune, EPCI, Département, Région)
|
||||
geoInfo := geoInfoMap[account.ID]
|
||||
row = append(row, geoInfo.Commune)
|
||||
row = append(row, geoInfo.EPCI)
|
||||
row = append(row, geoInfo.Departement)
|
||||
row = append(row, geoInfo.Region)
|
||||
|
||||
// Archived status
|
||||
archived := "Non"
|
||||
if archivedVal, ok := account.Data["archived"].(bool); ok && archivedVal {
|
||||
archived = "Oui"
|
||||
}
|
||||
row = append(row, archived)
|
||||
|
||||
spreadsheet.AddRow(row)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"export-beneficiaires.xlsx\""))
|
||||
|
||||
if err := spreadsheet.GetFile().Write(w); err != nil {
|
||||
log.Error().Err(err).Msg("Error generating Excel file")
|
||||
http.Error(w, "Error generating Excel file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,31 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// resolveStatusLabel returns the display label for a manual status name
|
||||
func resolveStatusLabel(statusOptions interface{}, manualStatus string) string {
|
||||
switch opts := statusOptions.(type) {
|
||||
case []map[string]any:
|
||||
for _, opt := range opts {
|
||||
if name, _ := opt["name"].(string); name == manualStatus {
|
||||
if label, ok := opt["label"].(string); ok {
|
||||
return label
|
||||
}
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, opt := range opts {
|
||||
if optMap, ok := opt.(map[string]interface{}); ok {
|
||||
if name, _ := optMap["name"].(string); name == manualStatus {
|
||||
if label, ok := optMap["label"].(string); ok {
|
||||
return label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return manualStatus
|
||||
}
|
||||
|
||||
func (r *XLSXRenderer) VehicleBookings(w http.ResponseWriter, bookings []fleetsstorage.Booking, vehiclesMap map[string]fleetsstorage.Vehicle, driversMap map[string]mobilityaccountsstorage.Account) {
|
||||
// Create Excel spreadsheet
|
||||
spreadsheet := r.NewSpreadsheet("Réservations véhicules")
|
||||
@@ -55,6 +80,10 @@ func (r *XLSXRenderer) VehicleBookings(w http.ResponseWriter, bookings []fleetss
|
||||
|
||||
spreadsheet.SetHeaders(headers)
|
||||
|
||||
// Read status management config
|
||||
isManualStatus := r.Config.GetString("modules.vehicles.status_management") == "manual"
|
||||
statusOptions := r.Config.Get("modules.vehicles.status_options")
|
||||
|
||||
// Add data rows
|
||||
for _, booking := range bookings {
|
||||
vehicle := vehiclesMap[booking.Vehicleid]
|
||||
@@ -69,6 +98,8 @@ func (r *XLSXRenderer) VehicleBookings(w http.ResponseWriter, bookings []fleetss
|
||||
status := ""
|
||||
if booking.Deleted {
|
||||
status = "Annulé"
|
||||
} else if isManualStatus {
|
||||
status = resolveStatusLabel(statusOptions, booking.ManualStatus)
|
||||
} else {
|
||||
switch booking.Status() {
|
||||
case 1:
|
||||
@@ -216,6 +247,10 @@ func (r *XLSXRenderer) VehicleBookingsAdmin(w http.ResponseWriter, bookings []fl
|
||||
|
||||
spreadsheet.SetHeaders(headers)
|
||||
|
||||
// Read status management config
|
||||
isManualStatusAdmin := r.Config.GetString("modules.vehicles.status_management") == "manual"
|
||||
statusOptionsAdmin := r.Config.Get("modules.vehicles.status_options")
|
||||
|
||||
// Add data rows
|
||||
for _, booking := range bookings {
|
||||
// Get vehicle from map
|
||||
@@ -243,6 +278,8 @@ func (r *XLSXRenderer) VehicleBookingsAdmin(w http.ResponseWriter, bookings []fl
|
||||
status := ""
|
||||
if booking.Deleted {
|
||||
status = "Annulé"
|
||||
} else if isManualStatusAdmin {
|
||||
status = resolveStatusLabel(statusOptionsAdmin, booking.ManualStatus)
|
||||
} else {
|
||||
switch booking.Status() {
|
||||
case 1:
|
||||
|
||||
64
servers/publicweb/api.go
Normal file
64
servers/publicweb/api.go
Normal 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",
|
||||
})
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"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
|
||||
@@ -36,6 +37,7 @@ type PublicWebServer struct {
|
||||
kv cache.KVHandler
|
||||
filestorage cache.FileStorage
|
||||
applicationHandler *application.ApplicationHandler
|
||||
mailer *emailing.Mailer
|
||||
rootDir string
|
||||
dynamicRoutes map[string]DynamicRoute
|
||||
}
|
||||
@@ -46,6 +48,7 @@ func Run(
|
||||
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")
|
||||
@@ -57,6 +60,7 @@ func Run(
|
||||
kv: kv,
|
||||
filestorage: filestorage,
|
||||
applicationHandler: applicationHandler,
|
||||
mailer: mailer,
|
||||
rootDir: rootDir,
|
||||
dynamicRoutes: make(map[string]DynamicRoute),
|
||||
}
|
||||
@@ -67,6 +71,9 @@ func Run(
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
9
servers/web/app_search_routes.go
Normal file
9
servers/web/app_search_routes.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (ws *WebServer) setupSearchRoutes(appRouter *mux.Router) {
|
||||
appRouter.HandleFunc("/search", ws.appHandler.GlobalSearchHTTPHandler())
|
||||
}
|
||||
@@ -17,6 +17,7 @@ func (ws *WebServer) setupVehiclesManagementRoutes(appRouter *mux.Router) {
|
||||
// Bookings
|
||||
vehiclesManagement.HandleFunc("/bookings/", ws.appHandler.VehiclesManagementBookingsListHTTPHandler())
|
||||
vehiclesManagement.HandleFunc("/bookings/{bookingid}", ws.appHandler.VehicleManagementBookingDisplayHTTPHandler())
|
||||
vehiclesManagement.HandleFunc("/bookings/{bookingid}/status", ws.appHandler.VehicleManagementUpdateBookingStatusHTTPHandler()).Methods("POST")
|
||||
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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
|
||||
@@ -83,14 +85,64 @@ func (h *Handler) BeneficiariesListHTTPHandler() http.HandlerFunc {
|
||||
archivedFilter = true
|
||||
}
|
||||
|
||||
result, err := h.applicationHandler.GetBeneficiaries(r.Context(), searchFilter, archivedFilter)
|
||||
// Extract beneficiary address geography filter
|
||||
beneficiaryAddressGeo := r.URL.Query().Get("beneficiary_address_geo")
|
||||
addressGeoLayer, addressGeoCode := "", ""
|
||||
if beneficiaryAddressGeo != "" {
|
||||
parts := strings.SplitN(beneficiaryAddressGeo, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
addressGeoLayer, addressGeoCode = parts[0], parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.applicationHandler.GetBeneficiaries(r.Context(), searchFilter, archivedFilter, addressGeoLayer, addressGeoCode)
|
||||
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)
|
||||
// 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,
|
||||
}
|
||||
|
||||
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.Slice(enrichedGeoFilters, func(i, j int) bool {
|
||||
return enrichedGeoFilters[i]["name"] < enrichedGeoFilters[j]["name"]
|
||||
})
|
||||
}
|
||||
|
||||
h.renderer.BeneficiariesList(w, r, result.Accounts, result.CacheID, archivedFilter, enrichedGeoFilters, beneficiaryAddressGeo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -159,6 +159,7 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
|
||||
destinationGeo,
|
||||
departureDate,
|
||||
departureTime,
|
||||
departureDateTime,
|
||||
result.DriverJourneys,
|
||||
result.Drivers,
|
||||
result.OrganizedCarpools,
|
||||
@@ -167,6 +168,8 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc {
|
||||
passengerID,
|
||||
savedSearches,
|
||||
beneficiariesMap,
|
||||
result.DriverLastTrips,
|
||||
result.LastTripDays,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -345,7 +348,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)
|
||||
|
||||
26
servers/web/application/search.go
Normal file
26
servers/web/application/search.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (h *Handler) GlobalSearchHTTPHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
http.Redirect(w, r, "/app/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.applicationHandler.GlobalSearch(r.Context(), query)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error performing global search")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.renderer.GlobalSearchResults(w, r, query, result.Beneficiaries, result.SolidarityDrivers, result.OrganizedCarpoolDrivers)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coreapplication "git.coopgo.io/coopgo-apps/parcoursmob/core/application"
|
||||
"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"
|
||||
@@ -614,12 +615,17 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
|
||||
var replacementDriversMap map[string]mobilityaccountsstorage.Account
|
||||
var replacementPricing map[string]map[string]interface{}
|
||||
var replacementLocations map[string]string
|
||||
var driverLastTrips map[string]time.Time
|
||||
var lastTripDays int
|
||||
if result.Booking.Status == "CANCELLED" {
|
||||
// Initialize maps to avoid nil pointer in template
|
||||
replacementDriversMap = make(map[string]mobilityaccountsstorage.Account)
|
||||
replacementPricing = make(map[string]map[string]interface{})
|
||||
replacementLocations = make(map[string]string)
|
||||
driverLastTrips = make(map[string]time.Time)
|
||||
|
||||
// Preserve the original booking's noreturn status when searching for replacement drivers
|
||||
noreturn := result.Booking.Journey.Noreturn
|
||||
searchResult, err := h.applicationHandler.SearchJourneys(
|
||||
r.Context(),
|
||||
result.Booking.Journey.PassengerPickupDate,
|
||||
@@ -628,11 +634,15 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
|
||||
result.Booking.PassengerId,
|
||||
result.Booking.DriverId, // Exclude the original driver
|
||||
result.Booking.GroupId, // Exclude drivers with bookings in this group
|
||||
nil, // options - use defaults
|
||||
&coreapplication.SearchJourneyOptions{
|
||||
SolidarityTransportNoreturn: &noreturn,
|
||||
},
|
||||
)
|
||||
if err == nil {
|
||||
replacementDrivers = searchResult.DriverJourneys
|
||||
replacementDriversMap = searchResult.Drivers
|
||||
driverLastTrips = searchResult.DriverLastTrips
|
||||
lastTripDays = searchResult.LastTripDays
|
||||
|
||||
// Calculate pricing for each replacement driver journey
|
||||
for _, dj := range searchResult.DriverJourneys {
|
||||
@@ -668,7 +678,7 @@ func (h *Handler) SolidarityTransportBookingDisplayHTTPHandler() http.HandlerFun
|
||||
}
|
||||
}
|
||||
|
||||
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance, replacementDrivers, replacementDriversMap, replacementPricing, replacementLocations)
|
||||
h.renderer.SolidarityTransportBookingDisplay(w, r, result.Booking, result.Driver, result.Passenger, result.PassengerWalletBalance, replacementDrivers, replacementDriversMap, replacementPricing, replacementLocations, driverLastTrips, lastTripDays)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
@@ -22,14 +23,37 @@ func (h *Handler) VehiclesManagementOverviewHTTPHandler() http.HandlerFunc {
|
||||
groupID = group.ID
|
||||
}
|
||||
|
||||
result, err := h.applicationHandler.GetVehiclesManagementOverview(r.Context(), groupID)
|
||||
// Extract tab and filter parameters from query
|
||||
tab := r.URL.Query().Get("tab")
|
||||
vehicleType := r.URL.Query().Get("vehicle_type")
|
||||
status := r.URL.Query().Get("status")
|
||||
if _, hasStatus := r.URL.Query()["status"]; !hasStatus {
|
||||
status = "meta:open,active"
|
||||
}
|
||||
dateStart := r.URL.Query().Get("date_start")
|
||||
dateEnd := r.URL.Query().Get("date_end")
|
||||
vType := r.URL.Query().Get("v_type")
|
||||
vStatus := r.URL.Query().Get("v_status")
|
||||
|
||||
result, err := h.applicationHandler.GetVehiclesManagementOverview(r.Context(), groupID, vehicleType, status, dateStart, dateEnd, vType, vStatus)
|
||||
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)
|
||||
filters := map[string]string{
|
||||
"vehicle_type": vehicleType,
|
||||
"status": status,
|
||||
"date_start": dateStart,
|
||||
"date_end": dateEnd,
|
||||
"v_type": vType,
|
||||
"v_status": vStatus,
|
||||
}
|
||||
|
||||
vehicleTypes, _ := h.applicationHandler.GetVehicleTypes(r.Context())
|
||||
|
||||
h.renderer.VehiclesManagementOverview(w, r, result.Vehicles, result.VehiclesMap, result.DriversMap, result.Bookings, filters, vehicleTypes, tab)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +70,7 @@ func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc {
|
||||
status := r.URL.Query().Get("status")
|
||||
dateStart := r.URL.Query().Get("date_start")
|
||||
dateEnd := r.URL.Query().Get("date_end")
|
||||
vehicleType := r.URL.Query().Get("vehicle_type")
|
||||
|
||||
// Default to last month if no dates specified
|
||||
if dateStart == "" {
|
||||
@@ -55,7 +80,7 @@ func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc {
|
||||
dateEnd = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), groupID, status, dateStart, dateEnd)
|
||||
result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), groupID, status, dateStart, dateEnd, vehicleType)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving vehicles management bookings list")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
@@ -67,9 +92,12 @@ func (h *Handler) VehiclesManagementBookingsListHTTPHandler() http.HandlerFunc {
|
||||
"status": status,
|
||||
"date_start": dateStart,
|
||||
"date_end": dateEnd,
|
||||
"vehicle_type": vehicleType,
|
||||
}
|
||||
|
||||
h.renderer.VehiclesManagementBookingsList(w, r, result.VehiclesMap, result.DriversMap, result.Bookings, result.CacheID, filters)
|
||||
vehicleTypes, _ := h.applicationHandler.GetVehicleTypes(r.Context())
|
||||
|
||||
h.renderer.VehiclesManagementBookingsList(w, r, result.VehiclesMap, result.DriversMap, result.Bookings, result.CacheID, filters, vehicleTypes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +251,7 @@ func (h *Handler) VehicleManagementBookingDisplayHTTPHandler() http.HandlerFunc
|
||||
return
|
||||
}
|
||||
|
||||
h.renderer.VehicleManagementBookingDisplay(w, r, result.Booking, result.Vehicle, result.Beneficiary, result.Group, result.Documents, result.FileTypesMap, result.Alternatives)
|
||||
h.renderer.VehicleManagementBookingDisplay(w, r, result.Booking, result.Vehicle, result.Beneficiary, result.Group, result.Documents, result.FileTypesMap, result.Alternatives, result.ComputedExtraProperties)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +383,38 @@ func (h *Handler) UnbookingVehicleHTTPHandler() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) VehicleManagementUpdateBookingStatusHTTPHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bookingID := vars["bookingid"]
|
||||
|
||||
r.ParseForm()
|
||||
newStatus := r.FormValue("new_status")
|
||||
comment := r.FormValue("comment")
|
||||
|
||||
// Collect extra properties from prop_* form fields
|
||||
extraProperties := map[string]string{}
|
||||
for key, values := range r.Form {
|
||||
if strings.HasPrefix(key, "prop_") && len(values) > 0 {
|
||||
extraProperties[strings.TrimPrefix(key, "prop_")] = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
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.UpdateBookingStatus(r.Context(), bookingID, newStatus, comment, currentUserToken.Subject, currentUserClaims, currentGroup, extraProperties)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error updating booking status")
|
||||
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) VehiclesFleetUpdateHTTPHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -8,6 +8,7 @@ func (ws *WebServer) setupApplicationRoutes(r *mux.Router) {
|
||||
application := r.PathPrefix("/app").Subrouter()
|
||||
|
||||
// Setup all application route groups
|
||||
ws.setupSearchRoutes(application)
|
||||
ws.setupDashboardRoutes(application)
|
||||
setupMiscRoutes(application, ws.applicationHandler)
|
||||
ws.setupDirectoryRoutes(application)
|
||||
|
||||
63
servers/web/exports/beneficiaries.go
Normal file
63
servers/web/exports/beneficiaries.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package exports
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
xlsxrenderer "git.coopgo.io/coopgo-apps/parcoursmob/renderer/xlsx"
|
||||
"github.com/paulmach/orb/geojson"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (h *Handler) Beneficiaries(w http.ResponseWriter, r *http.Request) {
|
||||
archivedFilter := r.URL.Query().Get("archived") == "true"
|
||||
beneficiaryAddressGeo := r.URL.Query().Get("beneficiary_address_geo")
|
||||
|
||||
addressGeoLayer, addressGeoCode := "", ""
|
||||
if beneficiaryAddressGeo != "" {
|
||||
parts := strings.SplitN(beneficiaryAddressGeo, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
addressGeoLayer, addressGeoCode = parts[0], parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.applicationHandler.GetBeneficiaries(r.Context(), "", archivedFilter, addressGeoLayer, addressGeoCode)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get beneficiaries")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve geographic layers (EPCI, Département, Région) for each beneficiary
|
||||
geoInfoMap := map[string]xlsxrenderer.BeneficiaryGeoInfo{}
|
||||
for _, account := range result.Accounts {
|
||||
if addr, ok := account.Data["address"]; ok {
|
||||
jsonAddr, err := json.Marshal(addr)
|
||||
if err == nil {
|
||||
addrFeature, err := geojson.UnmarshalFeature(jsonAddr)
|
||||
if err == nil && addrFeature.Geometry != nil {
|
||||
geo, err := h.services.Geography.GeoSearch(addrFeature)
|
||||
if err == nil {
|
||||
info := xlsxrenderer.BeneficiaryGeoInfo{}
|
||||
if commune, ok := geo["communes"]; ok {
|
||||
info.Commune = commune.Properties.MustString("nom")
|
||||
}
|
||||
if epci, ok := geo["epci"]; ok {
|
||||
info.EPCI = epci.Properties.MustString("nom")
|
||||
}
|
||||
if dept, ok := geo["departements"]; ok {
|
||||
info.Departement = dept.Properties.MustString("nom")
|
||||
}
|
||||
if region, ok := geo["regions"]; ok {
|
||||
info.Region = region.Properties.MustString("nom")
|
||||
}
|
||||
geoInfoMap[account.ID] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.renderer.XLSX.Beneficiaries(w, result.Accounts, geoInfoMap)
|
||||
}
|
||||
@@ -53,7 +53,9 @@ func (h *Handler) FleetBookingsInGroup(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
vehicleType := r.URL.Query().Get("vehicle_type")
|
||||
|
||||
result, err := h.applicationHandler.GetVehiclesManagementBookingsList(r.Context(), group.ID, status, dateStart, dateEnd, vehicleType)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get vehicle bookings for export")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/application"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/services"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -14,14 +15,16 @@ type Handler struct {
|
||||
applicationHandler *application.ApplicationHandler
|
||||
idp *identification.IdentificationProvider
|
||||
renderer *renderer.Renderer
|
||||
services *services.ServicesHandler
|
||||
}
|
||||
|
||||
func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer) *Handler {
|
||||
func NewHandler(cfg *viper.Viper, applicationHandler *application.ApplicationHandler, idp *identification.IdentificationProvider, renderer *renderer.Renderer, services *services.ServicesHandler) *Handler {
|
||||
return &Handler{
|
||||
config: cfg,
|
||||
applicationHandler: applicationHandler,
|
||||
idp: idp,
|
||||
renderer: renderer,
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ func (ws *WebServer) setupExportsRoutes(r *mux.Router) {
|
||||
export.HandleFunc("/solidarity-transport/drivers.xlsx", ws.exportsHandler.SolidarityTransportDrivers)
|
||||
export.HandleFunc("/organized-carpool/bookings.xlsx", ws.exportsHandler.OrganizedCarpoolBookings)
|
||||
export.HandleFunc("/organized-carpool/drivers.xlsx", ws.exportsHandler.OrganizedCarpoolDrivers)
|
||||
export.HandleFunc("/beneficiaries/beneficiaries.xlsx", ws.exportsHandler.Beneficiaries)
|
||||
export.Use(ws.idp.Middleware)
|
||||
export.Use(ws.idp.GroupsMiddleware)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func Run(cfg *viper.Viper, services *services.ServicesHandler, renderer *rendere
|
||||
// Initialize web handler subpackages
|
||||
appHandler: webapplication.NewHandler(cfg, renderer, applicationHandler, idp, services),
|
||||
authHandler: webauth.NewHandler(cfg, applicationHandler, idp, renderer),
|
||||
exportsHandler: webexports.NewHandler(cfg, applicationHandler, idp, renderer),
|
||||
exportsHandler: webexports.NewHandler(cfg, applicationHandler, idp, renderer, services),
|
||||
extHandler: webexternal.NewHandler(cfg, applicationHandler, filestorage),
|
||||
protectedAPIHandler: webprotectedapi.NewHandler(cfg, applicationHandler),
|
||||
webAPIHandler: webapi.NewHandler(cfg, idp, applicationHandler, cacheHandler),
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user