Add dispositifs

This commit is contained in:
Arnaud Delcasse 2022-09-05 07:25:05 +02:00
parent 0bb915059d
commit 8aa8fc927b
29 changed files with 1425 additions and 329 deletions

4
go.mod
View File

@ -8,7 +8,10 @@ replace git.coopgo.io/coopgo-platform/groups-management => ../../coopgo-platform
replace git.coopgo.io/coopgo-platform/fleets => ../../coopgo-platform/fleets/
replace git.coopgo.io/coopgo-platform/agenda => ../../coopgo-platform/agenda/
require (
git.coopgo.io/coopgo-platform/agenda v0.0.0-00010101000000-000000000000
git.coopgo.io/coopgo-platform/fleets v0.0.0-00010101000000-000000000000
git.coopgo.io/coopgo-platform/groups-management v0.0.0-00010101000000-000000000000
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-00010101000000-000000000000
@ -71,6 +74,7 @@ require (
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

2
go.sum
View File

@ -667,6 +667,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614 h1:lwJmuuJQGclcankpPJwh8rorzB0bNbVALv8phDGh8TQ=
gopkg.in/square/go-jose.v2 v2.5.2-0.20210529014059-a5c7eec3c614/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -0,0 +1,304 @@
package application
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"time"
formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/utils/form-validators"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
"github.com/gorilla/mux"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
type EventsByStartdate []agendastorage.Event
func (e EventsByStartdate) Len() int { return len(e) }
func (e EventsByStartdate) Less(i, j int) bool { return e[i].Startdate.Before(e[j].Startdate) }
func (e EventsByStartdate) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
type EventsForm struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
Description string `json:"description"`
Address any `json:"address,omitempty"`
Allday bool `json:"allday"`
Startdate *time.Time `json:"startdate"`
Enddate *time.Time `json:"enddate"`
Starttime string `json:"starttime"`
Endtime string `json:"endtime"`
MaxSubscribers int `json:"max_subscribers" validate:"required"`
}
func (h *ApplicationHandler) AgendaHome(w http.ResponseWriter, r *http.Request) {
resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
Namespaces: []string{"parcoursmob_dispositifs"},
Mindate: timestamppb.New(time.Now().Add(-24 * time.Hour)),
})
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
responses := []agendastorage.Event{}
groupids := []string{}
for _, e := range resp.Events {
groupids = append(groupids, e.Owners...)
responses = append(responses, e.ToStorageType())
// fmt.Println(e)
// fmt.Println(e.ToStorageType())
}
sort.Sort(EventsByStartdate(responses))
groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
Groupids: groupids,
})
groups := map[string]any{}
if err == nil {
for _, g := range groupsresp.Groups {
groups[g.Id] = g.ToStorageType()
}
}
h.Renderer.AgendaHome(w, r, responses, groups)
}
func (h *ApplicationHandler) AgendaCreateEvent(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
// Get current group
g := r.Context().Value(identification.GroupKey)
if g == nil {
w.WriteHeader(http.StatusBadRequest)
return
}
group := g.(storage.Group)
eventForm, err := parseEventsForm(r)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Println(eventForm)
data, _ := structpb.NewStruct(map[string]any{
"address": eventForm.Address,
})
request := &agenda.CreateEventRequest{
Event: &agenda.Event{
Namespace: "parcoursmob_dispositifs",
Owners: []string{group.ID},
Type: eventForm.Type,
Name: eventForm.Name,
Description: eventForm.Description,
Startdate: timestamppb.New(*eventForm.Startdate),
Enddate: timestamppb.New(*eventForm.Enddate),
Starttime: eventForm.Starttime,
Endtime: eventForm.Endtime,
Allday: eventForm.Allday,
MaxSubscribers: int64(eventForm.MaxSubscribers),
Data: data,
},
}
resp, err := h.services.GRPC.Agenda.CreateEvent(context.TODO(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", resp.Event.Id), http.StatusFound)
}
h.Renderer.AgendaCreateEvent(w, r)
}
func (h *ApplicationHandler) AgendaDisplayEvent(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
eventid := vars["eventid"]
request := &agenda.GetEventRequest{
Id: eventid,
}
resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
grouprequest := &groupsmanagement.GetGroupRequest{
Id: resp.Event.Owners[0],
}
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), grouprequest)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
subscribers := map[string]any{}
subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
context.TODO(),
&mobilityaccounts.GetAccountsBatchRequest{
Accountids: resp.Event.Subscribers,
},
)
if err == nil {
for _, sub := range subscriberresp.Accounts {
subscribers[sub.Id] = sub.ToStorageType()
}
}
g := r.Context().Value(identification.GroupKey)
if g == nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
group := g.(storage.Group)
accountids := []string{}
for _, m := range group.Members {
if !contains(resp.Event.Subscribers, m) {
accountids = append(accountids, m)
}
}
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
context.TODO(),
&mobilityaccounts.GetAccountsBatchRequest{
Accountids: accountids,
},
)
accounts := []any{}
if err == nil {
for _, acc := range accountresp.Accounts {
accounts = append(accounts, acc)
}
}
h.Renderer.AgendaDisplayEvent(w, r, resp.Event.ToStorageType(), groupresp.Group.ToStorageType(), subscribers, accounts)
}
func (h *ApplicationHandler) AgendaSubscribeEvent(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
eventid := vars["eventid"]
if err := r.ParseForm(); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
subscriber := r.FormValue("subscriber")
request := &agenda.SubscribeEventRequest{
Eventid: eventid,
Subscriber: subscriber,
}
_, err := h.services.GRPC.Agenda.SubscribeEvent(context.TODO(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/app/agenda/%s", eventid), http.StatusFound)
}
func parseEventsForm(r *http.Request) (*EventsForm, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}
var startdate *time.Time
var enddate *time.Time
if r.PostFormValue("startdate") != "" {
d, err := time.Parse("2006-01-02", r.PostFormValue("startdate"))
if err != nil {
return nil, err
}
startdate = &d
}
if r.PostFormValue("enddate") != "" {
d, err := time.Parse("2006-01-02", r.PostFormValue("enddate"))
if err != nil {
return nil, err
}
enddate = &d
}
max_subscribers, err := strconv.Atoi(r.PostFormValue("max_subscribers"))
if err != nil {
return nil, err
}
formData := &EventsForm{
Name: r.PostFormValue("name"),
Type: r.PostFormValue("type"),
Description: r.PostFormValue("description"),
Startdate: startdate,
Enddate: enddate,
Starttime: r.PostFormValue("starttime"),
Endtime: r.PostFormValue("endtime"),
MaxSubscribers: max_subscribers,
}
if r.PostFormValue("allday") == "true" {
formData.Allday = true
}
if r.PostFormValue("address") != "" {
var a any
json.Unmarshal([]byte(r.PostFormValue("address")), &a)
formData.Address = a
}
validate := formvalidators.New()
if err := validate.Struct(formData); err != nil {
return nil, err
}
return formData, nil
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

View File

@ -16,6 +16,7 @@ import (
formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/utils/form-validators"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
profilepictures "git.coopgo.io/coopgo-apps/parcoursmob/utils/profile-pictures"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
@ -61,7 +62,7 @@ func (h *ApplicationHandler) BeneficiaryCreate(w http.ResponseWriter, r *http.Re
if r.Method == "POST" {
dataMap, err := parseForm(r)
dataMap, err := parseBeneficiariesForm(r)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
@ -128,7 +129,23 @@ func (h *ApplicationHandler) BeneficiaryDisplay(w http.ResponseWriter, r *http.R
//TODO filter namespaces
//TODO filter groups
h.Renderer.BeneficiaryDisplay(w, r, resp.Account.ToStorageType())
bookingsrequest := &fleets.GetDriverBookingsRequest{
Driver: beneficiaryID,
}
bookingsresp, err := h.services.GRPC.Fleets.GetDriverBookings(context.TODO(), bookingsrequest)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
bookings := []any{}
for _, b := range bookingsresp.Bookings {
bookings = append(bookings, b.ToStorageType())
}
h.Renderer.BeneficiaryDisplay(w, r, resp.Account.ToStorageType(), bookings)
}
func (h *ApplicationHandler) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request) {
@ -137,7 +154,7 @@ func (h *ApplicationHandler) BeneficiaryUpdate(w http.ResponseWriter, r *http.Re
if r.Method == "POST" {
dataMap, err := parseForm(r)
dataMap, err := parseBeneficiariesForm(r)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
@ -189,56 +206,6 @@ func (h *ApplicationHandler) BeneficiaryUpdate(w http.ResponseWriter, r *http.Re
h.Renderer.BeneficiaryUpdate(w, r, resp.Account.ToStorageType())
}
func parseForm(r *http.Request) (map[string]any, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}
var date *time.Time
if r.PostFormValue("birthdate") != "" {
d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate"))
if err != nil {
return nil, err
}
date = &d
}
formData := BeneficiariesForm{
FirstName: r.PostFormValue("first_name"),
LastName: r.PostFormValue("last_name"),
Email: r.PostFormValue("email"),
Birthdate: date,
PhoneNumber: r.PostFormValue("phone_number"),
Gender: r.PostFormValue("gender"),
}
if r.PostFormValue("address") != "" {
var a any
json.Unmarshal([]byte(r.PostFormValue("address")), &a)
formData.Address = a
}
validate := formvalidators.New()
if err := validate.Struct(formData); err != nil {
return nil, err
}
d, err := json.Marshal(formData)
if err != nil {
return nil, err
}
var dataMap map[string]any
err = json.Unmarshal(d, &dataMap)
if err != nil {
return nil, err
}
return dataMap, nil
}
func (h *ApplicationHandler) BeneficiaryPicture(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
beneficiaryID := vars["beneficiaryid"]
@ -312,3 +279,53 @@ func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]any, error) {
return accounts, err
}
func parseBeneficiariesForm(r *http.Request) (map[string]any, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}
var date *time.Time
if r.PostFormValue("birthdate") != "" {
d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate"))
if err != nil {
return nil, err
}
date = &d
}
formData := BeneficiariesForm{
FirstName: r.PostFormValue("first_name"),
LastName: r.PostFormValue("last_name"),
Email: r.PostFormValue("email"),
Birthdate: date,
PhoneNumber: r.PostFormValue("phone_number"),
Gender: r.PostFormValue("gender"),
}
if r.PostFormValue("address") != "" {
var a any
json.Unmarshal([]byte(r.PostFormValue("address")), &a)
formData.Address = a
}
validate := formvalidators.New()
if err := validate.Struct(formData); err != nil {
return nil, err
}
d, err := json.Marshal(formData)
if err != nil {
return nil, err
}
var dataMap map[string]any
err = json.Unmarshal(d, &dataMap)
if err != nil {
return nil, err
}
return dataMap, nil
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
@ -14,6 +15,7 @@ import (
"github.com/google/uuid"
"github.com/gorilla/mux"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func (h *ApplicationHandler) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request) {
@ -182,6 +184,57 @@ func (h ApplicationHandler) VehicleManagementBookingDisplay(w http.ResponseWrite
booking := resp.Booking.ToStorageType()
if r.Method == "POST" {
r.ParseForm()
newbooking := resp.Booking
startdate := r.FormValue("startdate")
if startdate != "" {
newstartdate, _ := time.Parse("2006-01-02", startdate)
newbooking.Startdate = timestamppb.New(newstartdate)
if newstartdate.Before(newbooking.Unavailablefrom.AsTime()) {
newbooking.Unavailablefrom = timestamppb.New(newstartdate)
}
}
enddate := r.FormValue("enddate")
if enddate != "" {
newenddate, _ := time.Parse("2006-01-02", enddate)
newbooking.Enddate = timestamppb.New(newenddate)
if newenddate.After(newbooking.Unavailableto.AsTime()) || newenddate.Equal(newbooking.Unavailableto.AsTime()) {
newbooking.Unavailableto = timestamppb.New(newenddate.Add(24 * time.Hour))
}
}
unavailablefrom := r.FormValue("unavailablefrom")
if unavailablefrom != "" {
newunavailablefrom, _ := time.Parse("2006-01-02", unavailablefrom)
newbooking.Unavailablefrom = timestamppb.New(newunavailablefrom)
}
unavailableto := r.FormValue("unavailableto")
if unavailableto != "" {
newunavailableto, _ := time.Parse("2006-01-02", unavailableto)
newbooking.Unavailableto = timestamppb.New(newunavailableto)
}
request := &fleets.UpdateBookingRequest{
Booking: newbooking,
}
_, err := h.services.GRPC.Fleets.UpdateBooking(context.TODO(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
booking = newbooking.ToStorageType()
}
beneficiaryrequest := &mobilityaccounts.GetAccountRequest{
Id: booking.Driver,
}

View File

@ -6,10 +6,15 @@ import (
"net/http"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
"github.com/coreos/go-oidc"
"github.com/google/uuid"
"github.com/gorilla/mux"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
@ -71,6 +76,32 @@ func (h ApplicationHandler) VehiclesSearch(w http.ResponseWriter, r *http.Reques
}
func (h ApplicationHandler) Book(w http.ResponseWriter, r *http.Request) {
// Get Group
g := r.Context().Value(identification.GroupKey)
if g == nil {
fmt.Println("no current group")
w.WriteHeader(http.StatusInternalServerError)
return
}
current_group := g.(storage.Group)
// Get current user ID
u := r.Context().Value(identification.IdtokenKey)
if u == nil {
fmt.Println("no current user")
w.WriteHeader(http.StatusInternalServerError)
return
}
current_user_token := u.(*oidc.IDToken)
// Get current user claims
c := r.Context().Value(identification.ClaimsKey)
if c == nil {
fmt.Println("no current user claims")
w.WriteHeader(http.StatusInternalServerError)
return
}
current_user_claims := c.(map[string]any)
vars := mux.Vars(r)
vehicleid := vars["vehicleid"]
@ -84,6 +115,25 @@ func (h ApplicationHandler) Book(w http.ResponseWriter, r *http.Request) {
startdate, _ := time.Parse("2006-01-02", start)
enddate, _ := time.Parse("2006-01-02", end)
data := map[string]any{
"booked_by": map[string]any{
"user": map[string]any{
"id": current_user_token.Subject,
"display_name": current_user_claims["display_name"],
},
"group": map[string]any{
"id": current_group.ID,
"name": current_group.Data["name"],
},
},
}
datapb, err := structpb.NewStruct(data)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
booking := &fleets.Booking{
Id: uuid.NewString(),
Vehicleid: vehicleid,
@ -92,13 +142,14 @@ func (h ApplicationHandler) Book(w http.ResponseWriter, r *http.Request) {
Enddate: timestamppb.New(enddate),
Unavailablefrom: timestamppb.New(startdate),
Unavailableto: timestamppb.New(enddate.Add(72 * time.Hour)),
Data: datapb,
}
request := &fleets.CreateBookingRequest{
Booking: booking,
}
_, err := h.services.GRPC.Fleets.CreateBooking(context.TODO(), request)
_, err = h.services.GRPC.Fleets.CreateBooking(context.TODO(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
@ -136,16 +187,34 @@ func (h ApplicationHandler) VehicleBookingDisplay(w http.ResponseWriter, r *http
return
}
// grouprequest := &groupsmanagement.GetGroupRequest{
// Id: booking.Vehicle.Administrators[0],
// }
grouprequest := &groupsmanagement.GetGroupRequest{
Id: booking.Vehicle.Administrators[0],
}
// groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), grouprequest)
// if err != nil {
// fmt.Println(err)
// w.WriteHeader(http.StatusInternalServerError)
// return
// }
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), grouprequest)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
h.Renderer.VehicleBookingDisplay(w, r, booking, booking.Vehicle, beneficiaryresp.Account.ToStorageType(), "")
h.Renderer.VehicleBookingDisplay(w, r, booking, booking.Vehicle, beneficiaryresp.Account.ToStorageType(), groupresp.Group.ToStorageType())
}
func (h ApplicationHandler) VehiclesBookingsList(w http.ResponseWriter, r *http.Request) {
request := &fleets.GetBookingsRequest{}
resp, err := h.services.GRPC.Fleets.GetBookings(context.TODO(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusNotFound)
return
}
bookings := []any{}
for _, b := range resp.Bookings {
bookings = append(bookings, b.ToStorageType())
}
h.Renderer.VehicleBookingsList(w, r, bookings)
}

11
main.go
View File

@ -54,6 +54,7 @@ func main() {
r.HandleFunc("/auth/groups/", authHandler.Groups)
r.HandleFunc("/auth/groups/switch", authHandler.GroupSwitch)
r.HandleFunc("/", redirectApp)
api_router := r.PathPrefix("/api").Subrouter()
api_router.HandleFunc("/", apiHandler.NotFound)
@ -68,7 +69,9 @@ func main() {
application.HandleFunc("/beneficiaries/{beneficiaryid}", applicationHandler.BeneficiaryDisplay)
application.HandleFunc("/beneficiaries/{beneficiaryid}/update", applicationHandler.BeneficiaryUpdate)
application.HandleFunc("/beneficiaries/{beneficiaryid}/picture", applicationHandler.BeneficiaryPicture)
application.HandleFunc("/members/{beneficiaryid}/picture", applicationHandler.BeneficiaryPicture)
application.HandleFunc("/vehicles/", applicationHandler.VehiclesSearch)
application.HandleFunc("/vehicles/bookings/", applicationHandler.VehiclesBookingsList)
application.HandleFunc("/vehicles/bookings/{bookingid}", applicationHandler.VehicleBookingDisplay)
application.HandleFunc("/vehicles/v/{vehicleid}/b/{beneficiaryid}", applicationHandler.Book)
application.HandleFunc("/vehicles-management/", applicationHandler.VehiclesManagementOverview)
@ -76,6 +79,10 @@ func main() {
application.HandleFunc("/vehicles-management/fleet/{vehicleid}", applicationHandler.VehiclesFleetDisplay)
application.HandleFunc("/vehicles-management/fleet/{vehicleid}/update", applicationHandler.VehiclesFleetUpdate)
application.HandleFunc("/vehicles-management/bookings/{bookingid}", applicationHandler.VehicleManagementBookingDisplay)
application.HandleFunc("/agenda/", applicationHandler.AgendaHome)
application.HandleFunc("/agenda/create-event", applicationHandler.AgendaCreateEvent)
application.HandleFunc("/agenda/{eventid}", applicationHandler.AgendaDisplayEvent)
application.HandleFunc("/agenda/{eventid}/subscribe", applicationHandler.AgendaSubscribeEvent)
//TODO Subrouters with middlewares checking security for each module ?
application.Use(idp.Middleware)
application.Use(idp.GroupsMiddleware)
@ -98,3 +105,7 @@ func main() {
log.Fatal(srv.ListenAndServe())
}
func redirectApp(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/app/", http.StatusFound)
}

40
renderer/agenda.go Normal file
View File

@ -0,0 +1,40 @@
package renderer
import (
"net/http"
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
)
const agendaMenu = "events"
func (renderer *Renderer) AgendaHome(w http.ResponseWriter, r *http.Request, events []agendastorage.Event, groups map[string]any) {
files := renderer.ThemeConfig.GetStringSlice("views.agenda.list.files")
state := NewState(r, renderer.ThemeConfig, agendaMenu)
state.ViewState = map[string]any{
"events": events,
"groups": groups,
}
renderer.Render("agenda home", w, r, files, state)
}
func (renderer *Renderer) AgendaCreateEvent(w http.ResponseWriter, r *http.Request) {
files := renderer.ThemeConfig.GetStringSlice("views.agenda.create_event.files")
state := NewState(r, renderer.ThemeConfig, agendaMenu)
renderer.Render("agenda create event", w, r, files, state)
}
func (renderer *Renderer) AgendaDisplayEvent(w http.ResponseWriter, r *http.Request, event any, group any, subscribers map[string]any, beneficiaries any) {
files := renderer.ThemeConfig.GetStringSlice("views.agenda.display_event.files")
state := NewState(r, renderer.ThemeConfig, agendaMenu)
state.ViewState = map[string]any{
"event": event,
"group": group,
"subscribers": subscribers,
"beneficiaries": beneficiaries,
}
renderer.Render("agenda create event", w, r, files, state)
}

View File

@ -50,10 +50,13 @@ type BeneficiariesDisplayState struct {
Beneficiary any
}
func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Request, beneficiary any) {
func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Request, beneficiary any, bookings []any) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.display.files")
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
state.ViewState = beneficiary
state.ViewState = map[string]any{
"beneficiary": beneficiary,
"bookings": bookings,
}
renderer.Render("beneficiaries_display", w, r, files, state)
}

View File

@ -122,6 +122,8 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
Display: modules["administration"].(bool),
Active: menuState == administrationMenu,
},
//TODO from configuration for icons at least
MenuItems: []MenuItem{
{
Title: "Tableau de bord",
@ -155,7 +157,7 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
Title: "Véhicules partagés",
Link: "/app/vehicles/",
Active: menuState == "vehicles",
Icon: "hero:outline/user-group",
Icon: "tabler-icons:car",
})
}
@ -168,11 +170,11 @@ func NewState(r *http.Request, themeConfig *viper.Viper, menuState string) Rende
})
}
if modules["events"].(bool) {
if modules["agenda"].(bool) {
ls.MenuItems = append(ls.MenuItems, MenuItem{
Title: "Dispositifs",
Link: "/app/events/",
Active: menuState == "events",
Title: "Agenda dispositifs",
Link: "/app/agenda/",
Active: menuState == "agenda",
Icon: "hero:outline/calendar",
})
}

View File

@ -38,3 +38,13 @@ func (renderer *Renderer) VehicleBookingDisplay(w http.ResponseWriter, r *http.R
renderer.Render("vehicles search", w, r, files, state)
}
func (renderer *Renderer) VehicleBookingsList(w http.ResponseWriter, r *http.Request, bookings []any) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles.bookings_list.files")
state := NewState(r, renderer.ThemeConfig, vehiclesMenu)
state.ViewState = map[string]any{
"bookings": bookings,
}
renderer.Render("vehicles search", w, r, files, state)
}

23
services/agenda.go Normal file
View File

@ -0,0 +1,23 @@
package services
import (
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
"google.golang.org/grpc"
)
type AgendaService struct {
agenda.AgendaClient
}
func NewAgendaService(dial string) (*AgendaService, error) {
conn, err := grpc.Dial(dial, grpc.WithInsecure())
client := agenda.NewAgendaClient(conn)
if err != nil {
return nil, err
}
return &AgendaService{
AgendaClient: client,
}, nil
}

View File

@ -1,6 +1,7 @@
package services
import (
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
@ -15,6 +16,7 @@ type GRPCServices struct {
MobilityAccounts mobilityaccounts.MobilityAccountsClient
GroupsManagement groupsmanagement.GroupsManagementClient
Fleets fleets.FleetsClient
Agenda agenda.AgendaClient
}
func NewServicesHandler(cfg *viper.Viper) (*ServicesHandler, error) {
@ -22,6 +24,7 @@ func NewServicesHandler(cfg *viper.Viper) (*ServicesHandler, error) {
mobilityAccountsDial = cfg.GetString("services.grpc.mobilityaccounts.dial")
groupsManagementDial = cfg.GetString("services.grpc.groupsmanagement.dial")
fleetsDial = cfg.GetString("services.grpc.fleets.dial")
agendaDial = cfg.GetString("services.grpc.agenda.dial")
)
mobilityAccounts, err := NewMobilityAccountService(mobilityAccountsDial)
if err != nil {
@ -38,11 +41,17 @@ func NewServicesHandler(cfg *viper.Viper) (*ServicesHandler, error) {
return nil, err
}
agendaSvc, err := NewAgendaService(agendaDial)
if err != nil {
return nil, err
}
return &ServicesHandler{
GRPC: GRPCServices{
MobilityAccounts: mobilityAccounts,
GroupsManagement: groupsManagement,
Fleets: fleetsSvc,
Agenda: agendaSvc,
},
}, nil
}

View File

@ -39,6 +39,10 @@ views:
booking_display:
files:
- layouts/vehicles/booking-display.html
bookings_list:
files:
- layouts/vehicles_management/_partials/bookings-list.html
- layouts/vehicles/bookings-list.html
vehicles_management:
overview:
files:
@ -62,6 +66,17 @@ views:
booking_display:
files:
- layouts/vehicles_management/booking-display.html
agenda:
list:
files:
- layouts/agenda/home.html
display_event:
files:
- layouts/agenda/display-event.html
create_event:
files:
- layouts/_partials/address_autocomplete.html
- layouts/agenda/create-event.html
administration:
home:
files:
@ -96,4 +111,4 @@ icons:
hero:solid/search: <svg class="%s" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /></svg>
hero:solid/selector: <svg class="%s" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
img:profile-picture-placeholder: <svg class="%s" fill="currentColor" viewBox="0 0 24 24"><path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
tabler-icons:car: <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-car %s" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="7" cy="17" r="2"></circle><circle cx="17" cy="17" r="2"></circle><path d="M5 17h-2v-6l2 -5h9l4 5h1a2 2 0 0 1 2 2v4h-2m-4 0h-6m-6 -6h15m-6 0v-5"></path></svg>

View File

@ -117,8 +117,8 @@
class="focus:ring-co-blue h-4 w-4 text-co-blue border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="modules.events" class="font-medium text-gray-700">Dispositifs</label>
<p class="text-gray-500">Gestion des dispositifs pour les bénéficiaires (auto-écoles sociales, événements, ...)
<label for="modules.events" class="font-medium text-gray-700">Agenda dispositifs</label>
<p class="text-gray-500">Agenda des dispositifs pour les bénéficiaires (sessions permis, événements, ...)
</p>
</div>
</div>

View File

@ -88,8 +88,8 @@
class="focus:ring-co-blue h-4 w-4 text-co-blue border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="modules.events" class="font-medium text-gray-700">Dispositifs</label>
<p class="text-gray-500">Gestion des dispositifs pour les bénéficiaires (auto-écoles sociales, événements, ...)
<label for="modules.events" class="font-medium text-gray-700">Agenda dispositifs</label>
<p class="text-gray-500">Agenda des dispositifs pour les bénéficiaires (sessions permis, événements, ...)
</p>
</div>
</div>

View File

@ -0,0 +1,212 @@
{{ define "content" }}
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h1 class="text-2xl font-semibold text-gray-900">Ajouter à l'agenda</h1>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 mt-8"
x-data="{
fields: {
name: null,
type: null,
description: null,
allday: false,
startdate: null,
enddate: null,
starttime: null,
endtime: null,
max_subscribers: 0,
},
rules: {
name: ['required'],
type: ['required'],
startdate: ['required'],
enddate: ['required'],
starttime: ['optional'],
endtime: ['optional'],
description: ['optional'],
max_subscribers: ['required', 'min:0']
},
formValidation: {
valid: false,
fields: {
name: {valid: null},
type: {valid: null},
description: {valid: null},
startdate: {valid: null},
enddate: {valid: null},
starttime: {valid: null},
endtime: {valid: null},
allday: {valid: null},
max_subscribers: {valid: null},
}
},
isFormValid: true,
validate() {
this.formValidation = Iodine.assert(this.fields, this.rules)
},
validateField(field) {
this.formValidation.fields[field] = Iodine.assert(this.fields[field], this.rules[field])
},
submit(event) {
this.validate()
if(!this.formValidation.valid) {
this.isFormValid = false
event.preventDefault()
}
return this.formValidation.valid
}
}">
<form class="space-y-6" method="POST" @submit="submit">
<div class="bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<h3 class="text-lg font-medium leading-6 text-gray-900">Informations sur le dispositif</h3>
<p class="mt-1 text-sm text-gray-500">Informations générales sur le dispositif d'accompagnement à ajouter à l'agenda</p>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="name" class="block text-sm font-medium text-gray-700">Nom</label>
<input type="text" name="name" id="name"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
x-model="fields.name" @blur="validateField('name')"
:class="formValidation.fields.name.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="sm:col-span-3">
<label for="type" class="block text-sm font-medium text-gray-700">Type de dispositif</label>
<select id="type" name="type"
x-model="fields.type" @blur="validateField('type')"
class="max-w-lg mt-1 block focus:ring-co-blue focus:border-co-blue w-full shadow-sm sm:max-w-xs sm:text-sm rounded-2xl"
:class="formValidation.fields.type.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
<option></option>
<option value="Permis accéléré">Permis accéléré</option>
<option value="Auto-école sociale (classique)">Auto-école sociale (classique)</option>
<option value="Information collective">Information collective</option>
<option value="Autres">Autres</option>
</select>
</div>
<div class="col-span-6">
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<div class="mt-1">
<textarea rows="4" name="description" id="descrpition"
x-model="fields.description" @blur="validateField('description')"
:class="formValidation.fields.description.valid == false ? 'border-co-red border-2' : 'border-gray-300'"
class="shadow-sm focus:ring-co-blue focus:border-co-blue block w-full sm:text-sm border-gray-300 rounded-2xl"></textarea>
</div>
</div>
{{ $fieldName := "address" }}
{{ template "address_autocomplete" dict "FieldName" $fieldName }}
</div>
</div>
</div>
</div>
<div class="bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<h3 class="text-lg font-medium leading-6 text-gray-900">Planification</h3>
<p class="mt-1 text-sm text-gray-500">Dates et horaires de l'événement</p>
</div>
<div class="mt-5 space-y-6 md:mt-0 md:col-span-2 align-middle">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6">
<button type="button" class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2" role="switch" aria-checked="false"
:class="fields.allday ? 'bg-co-blue' : 'bg-gray-200'"
@click="fields.allday = ! fields.allday">
<span class="sr-only">Use setting</span>
<!-- Enabled: "translate-x-5", Not Enabled: "translate-x-0" -->
<span aria-hidden="true" class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="fields.allday ? 'translate-x-5' : 'translate-x-0'"></span>
</button> <span class="text-md font-medium text-gray-700 ml-2">Toute la journée</span>
<input type="hidden" name="allday" x-model="fields.allday">
</div>
<div class="sm:col-span-6">
<div class="inline-flex w-full">
<div class="flex-1">
<label for="startdate" class="block text-sm font-medium text-gray-700">Date de début</label>
<input type="date" name="startdate" id="startdate" placeholder="JJ/MM/AAAA"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-l-2xl"
x-model="fields.startdate" @blur="validateField('startdate')"
:class="formValidation.fields.startdate.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="flex-1">
<label for="enddate" class="block text-sm font-medium text-gray-700">Date de fin</label>
<input type="date" name="enddate" id="enddate" placeholder="JJ/MM/AAAA"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-r-2xl"
x-model="fields.enddate" @blur="validateField('enddate')"
:class="formValidation.fields.enddate.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
</div>
</div>
<div class="sm:col-span-6" x-show="!fields.allday">
<div class="inline-flex w-full">
<div class="flex-1">
<label for="startdate" class="block text-sm font-medium text-gray-700">Horaire de début</label>
<input type="time" name="starttime" id="starttime" placeholder="00:00"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-l-2xl"
x-model="fields.starttime" @blur="validateField('starttime')"
:class="formValidation.fields.starttime.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="flex-1">
<label for="endtime" class="block text-sm font-medium text-gray-700">Horaire de fin</label>
<input type="time" name="endtime" id="endtime" placeholder="00:00"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-r-2xl"
x-model="fields.endtime" @blur="validateField('endtime')"
:class="formValidation.fields.endtime.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<h3 class="text-lg font-medium leading-6 text-gray-900">Paramètres</h3>
<p class="mt-1 text-sm text-gray-500">Paramètres du dispositift (nombre de places disponibles, etc...)</p>
</div>
<div class="mt-5 space-y-6 md:mt-0 md:col-span-2 align-middle">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="max_subscribers" class="block text-sm font-medium text-gray-700">Places disponibles (0 = illimité)</label>
<input type="number" name="max_subscribers" id="max_subscribers"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
x-model="fields.max_subscribers" @blur="validateField('max_subscribers')"
:class="formValidation.fields.max_subscribers.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end">
<p x-show="! isFormValid" class="px-4 py-2 text-sm text-co-red">Certains champs de sont pas valides.</p>
<a href="/app/agenda/">
<button type="button"
class="bg-white py-2 px-4 border border-gray-300 rounded-2xl shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">Annuler</button>
</a>
<button type="submit"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-2xl text-white bg-co-blue hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">Ajouter</button>
</div>
</form>
</div>
{{end}}

View File

@ -0,0 +1,140 @@
{{ define "content" }}
<main class="py-10">
<div class="max-w-3xl mx-auto px-4 sm:px-6 md:flex md:items-center md:justify-between md:space-x-5 lg:max-w-7xl lg:px-8">
<div class="flex items-center space-x-5">
<div>
<h1 class="text-2xl font-bold text-gray-900">{{.ViewState.event.Name}}</h1>
<p class="text-m font-medium text-gray-500">
{{if eq .ViewState.event.Startdate .ViewState.event.Enddate}}
Le {{(timeFrom .ViewState.event.Startdate).Format "02/01/2006"}}
{{else}}
Du {{(timeFrom .ViewState.event.Startdate).Format "02/01/2006"}} au {{(timeFrom .ViewState.event.Enddate).Format "02/01/2006"}}
{{end}}
</p>
</div>
</div>
</div>
<div class="mt-8 max-w-3xl mx-auto grid grid-cols-1 gap-6 sm:px-6 lg:max-w-7xl lg:grid-flow-col-dense lg:grid-cols-3">
<div class="space-y-6 lg:col-start-1 lg:col-span-2">
<section aria-labelledby="event-information-title">
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h2 id="event-information-title" class="text-lg leading-6 font-medium text-gray-900">Informations</h2>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Informations sur le dispositif.</p>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<dl class="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
{{if .ViewState.event.Type}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Type</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.event.Type}}</dd>
</div>
{{end}}
{{if .ViewState.group}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Structure gestionnaire</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.group.Data.name}}</dd>
</div>
{{end}}
{{if .ViewState.event.MaxSubscribers}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Total places</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.event.MaxSubscribers}}</dd>
</div>
{{end}}
{{if ne .ViewState.event.MaxSubscribers 0}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Places restantes</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.event.RemainingSubscriptions}}</dd>
</div>
{{end}}
{{if .ViewState.event.Data.address}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Adresse</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.event.Data.address.properties.label}}</dd>
</div>
{{end}}
{{if .ViewState.event.Description}}
<div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">Description</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.event.Description}}</dd>
</div>
{{end}}
</dl>
</div>
</div>
</section>
</div>
<section aria-labelledby="subscribers-title" class="lg:col-start-3 lg:col-span-1">
<div class="bg-white px-4 py-5 shadow sm:rounded-lg sm:px-6">
<h2 id="subscribers-title" class="text-lg font-medium text-gray-900">Inscrire un bénéficiaire</h2>
{{if gt .ViewState.event.RemainingSubscriptions 0}}
<form class="mt-4" action="/app/agenda/{{.ViewState.event.ID}}/subscribe" method="POST">
<div class="relative mt-1 mb-4" x-data="{
text: '',
beneficiariesListOpen: false,
beneficiaries: {{json .ViewState.beneficiaries}},
filteredBeneficiaries: (text) => {
if(text=='') return beneficiaries
return this.beneficiaries.filter(b => b['data']['first_name'].includes(text) || b['data']['last_name'].includes(text))
},
fields: {
beneficiaryid: {{if .ViewState.search}}'{{.ViewState.search.beneficiary.ID}}'{{else}}null{{end}},
},
selectbeneficiary(beneficiary) {
console.log(beneficiary)
this.fields.beneficiaryid = beneficiary.id
this.text = beneficiary.data.first_name + ' ' + beneficiary.data.last_name
this.beneficiariesListOpen = false
}
}">
<input @focus="beneficiariesListOpen = true" x-model="text" id="combobox" type="text" class="w-full rounded-2xl border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-co-blue focus:outline-none focus:ring-1 focus:ring-co-blue sm:text-sm" role="combobox" aria-controls="options" aria-expanded="false">
<button @click="beneficiariesListOpen = ! beneficiariesListOpen" type="button" class="absolute inset-y-0 right-0 flex items-center rounded-r-2xl px-2 focus:outline-none">
<!-- Heroicon name: solid/selector -->
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<ul x-show="beneficiariesListOpen" class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" id="options" role="listbox">
<template x-for="beneficiary in beneficiaries">
<li @click="selectbeneficiary(beneficiary)" class="relative cursor-default hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900" id="option-0" role="option" tabindex="-1">
<span class="truncate" x-text="beneficiary.data.first_name"></span> <span class="truncate" x-text="beneficiary.data.last_name"></span>
<span class="absolute inset-y-0 right-0 flex items-center pr-4 text-co-blue">
</span>
</li>
</template>
</ul>
<input type="hidden" name="subscriber" x-model="fields.beneficiaryid">
</div>
<button type="submit"
class="rounded-2xl border border-transparent bg-co-blue px-4 py-2 w-full text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 sm:w-auto">
Inscrire le bénéficiaire
</button>
</form>
{{else}}
<p class="p-12 text-gray-500 text-center text-md">Il n'y a plus de place disponible</p>
{{end}}
{{if .ViewState.subscribers}}
<h2 id="subscribers-title" class="text-lg font-medium text-gray-900 mt-10">Inscrits</h2>
<div class="mt-2">
{{range .ViewState.subscribers}}
<ul class="p-1">
<a href="/app/beneficiaries/{{.ID}}">
<li class="inline-flex text-sm p-2"><img class="h-6 w-6 rounded-co mr-2" src="/app/beneficiaries/{{.ID}}/picture"> {{.Data.first_name}} {{.Data.last_name}}</li>
</a>
</ul>
{{end}}
</div>
{{end}}
</div>
</section>
</div>
</main>
{{ end }}

View File

@ -0,0 +1,110 @@
{{define "content"}}
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h1 class="text-2xl font-semibold text-gray-900">Agenda dispositifs</h1>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<p class="mt-2 text-sm text-gray-700"></p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<a href="/app/agenda/create-event">
<button type="button"
class="inline-flex items-center justify-center rounded-2xl border border-transparent bg-co-blue px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-ci-blue focus:ring-offset-2 sm:w-auto">
{{$.IconSet.Icon "hero:outline/plus-circle" "h-5 w-5 mr-3"}}
Ajouter un dispositif
</button>
</a>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<div class="mt-8 flex flex-col">
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Type de dispositif
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Structure
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Nom
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Lieu
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Dates
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Places disponibles
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Bénéficiaires positionnés
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
<tbody class="divide-y divide-gray-200 bg-white">
{{range .ViewState.events}}
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >{{.Type}}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >
{{range .Owners}}
{{if (index $.ViewState.groups .)}}
{{(index $.ViewState.groups .).Data.name}}
{{end}}
{{end}}
</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >{{.Name}}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >{{if .Data.address}}{{.Data.address.properties.label}}{{end}}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >
{{if eq .Startdate .Enddate}}
Le {{(timeFrom .Startdate).Format "02/01/2006"}}
{{else}}
Du {{(timeFrom .Startdate).Format "02/01/2006"}} <br />Au {{(timeFrom .Enddate).Format "02/01/2006"}}
{{end}}
</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >{{.RemainingSubscriptions}}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="flex -space-x-1 overflow-hidden" >
{{range .Subscribers}}
<img class="inline-block h-6 w-6 rounded-co ring-2 ring-white" src="/app/beneficiaries/{{.}}/picture" >
{{end}}
</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<a href="/app/agenda/{{.ID}}" class="text-co-blue hover:text-co-blue">Voir</a>
</td>
</tr>
{{end}}
</tbody>
</thead>
</table>
</div>
</div>
</div>
</div>
</div>
{{end}}

View File

@ -1,8 +1,16 @@
{{define "beneficiary_vehicles"}}
<div class="px-4 py-6 sm:px-6">
{{if .ViewState.bookings}}
<h3 class="text-lg">Mises à disposition réalisées</h3>
<ul class="my-8">
{{range .ViewState.bookings}}
<li class="text-sm">Du {{(timeFrom .Startdate).Format "02/01/2006"}} au {{(timeFrom .Enddate).Format "02/01/2006"}}</li>
{{end}}
</ul>
{{end}}
<h3 class="text-lg">Réserver un véhicule</h3>
<form method="GET" action="/app/vehicles/">
<input type="hidden" name="beneficiaryid" value="{{.ViewState.ID}}">
<input type="hidden" name="beneficiaryid" value="{{.ViewState.beneficiary.ID}}">
<div class="py-4 grid grid-cols-2">
<div class="lg:col-span-1">
<label for="startdate" class="block text-sm font-medium text-gray-700">Du</label>

View File

@ -5,24 +5,24 @@
<div class="flex items-center space-x-5">
<div class="flex-shrink-0">
<div class="relative">
<img class="h-16 w-16 rounded-co" src="/app/beneficiaries/{{.ViewState.ID}}/picture" alt="">
<img class="h-16 w-16 rounded-co" src="/app/beneficiaries/{{.ViewState.beneficiary.ID}}/picture" alt="">
<span class="absolute inset-0 shadow-inner rounded-full" aria-hidden="true"></span>
</div>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-900">{{.ViewState.Data.first_name}}
{{.ViewState.Data.last_name}}</h1>
<p class="text-sm font-medium text-gray-500">{{if .ViewState.Metadata.created}}Ajouté le <time
datetime="2022-07-25">{{.ViewState.Metadata.created}}</time> par
<h1 class="text-2xl font-bold text-gray-900">{{.ViewState.beneficiary.Data.first_name}}
{{.ViewState.beneficiary.Data.last_name}}</h1>
<p class="text-sm font-medium text-gray-500">{{if .ViewState.beneficiary.Metadata.created}}Ajouté le <time
datetime="2022-07-25">{{.ViewState.beneficiary.Metadata.created}}</time> par
<a href="#" class="text-gray-900">Conseiller 1</a>{{end}}
</p>
</div>
</div>
<div
class="mt-6 flex flex-col-reverse justify-stretch space-y-4 space-y-reverse sm:flex-row-reverse sm:justify-end sm:space-x-reverse sm:space-y-0 sm:space-x-3 md:mt-0 md:flex-row md:space-x-3">
<button type="button"
class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-2xl text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-co-blue">Supprimer</button>
<a href="/app/beneficiaries/{{.ViewState.ID}}/update" class="inline-flex"><button type="button"
<!-- <button type="button"
class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-2xl text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-co-blue">Supprimer</button> -->
<a href="/app/beneficiaries/{{.ViewState.beneficiary.ID}}/update" class="inline-flex"><button type="button"
class="w-full px-4 py-2 border border-transparent text-sm font-medium rounded-2xl shadow-sm text-white bg-co-blue hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-co-blue">Modifier</button></a>
</div>
</div>
@ -38,44 +38,44 @@
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<dl class="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
{{if .ViewState.Data.email}}
{{if .ViewState.beneficiary.Data.email}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Email</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.Data.email}}</dd>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.beneficiary.Data.email}}</dd>
</div>
{{end}}
{{if .ViewState.Data.phone_number}}
{{if .ViewState.beneficiary.Data.phone_number}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Téléphone</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.Data.phone_number}}</dd>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.beneficiary.Data.phone_number}}</dd>
</div>
{{end}}
{{if .ViewState.Data.birthdate}}
{{if .ViewState.beneficiary.Data.birthdate}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Date de naissance</dt>
<dd class="mt-1 text-sm text-gray-900">{{(timeFrom .ViewState.Data.birthdate).Format
<dd class="mt-1 text-sm text-gray-900">{{(timeFrom .ViewState.beneficiary.Data.birthdate).Format
"02/01/2006"}}</dd>
</div>
{{end}}
{{if and .ViewState.Data.gender (ne .ViewState.Data.gender "0")}}
{{if and .ViewState.beneficiary.Data.gender (ne .ViewState.beneficiary.Data.gender "0")}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Genre</dt>
<dd class="mt-1 text-sm text-gray-900">{{genderISO5218 .ViewState.Data.gender}}</dd>
<dd class="mt-1 text-sm text-gray-900">{{genderISO5218 .ViewState.beneficiary.Data.gender}}</dd>
</div>
{{end}}
{{if .ViewState.Data.address}}
{{if .ViewState.beneficiary.Data.address}}
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-gray-500">Adresse</dt>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.Data.address.properties.label}}</dd>
<dd class="mt-1 text-sm text-gray-900">{{.ViewState.beneficiary.Data.address.properties.label}}</dd>
</div>
{{end}}
</dl>
</div>
</div>
</section>
<section aria-labelledby="functionalities-title" x-data="{
tab: 'notes',
tab: 'vehicles',
to(event) {
this.tab = event.target.value
}
@ -87,7 +87,7 @@
<label for="tabs" class="sr-only">Select a tab</label>
<select id="tabs" name="tabs" @change="to"
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
<option value="notes">Notes</option>
<!-- <option value="notes">Notes</option> -->
<option value="journeys">Déplacements</option>
@ -100,10 +100,10 @@
<div class="border-b border-gray-200 pl-4">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<!-- Current: "border-indigo-500 text-indigo-600", Default: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" -->
<a href="#" @click="tab = 'notes'"
<!-- <a href="#" @click="tab = 'notes'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
:class="tab == 'notes' ? 'border-co-blue text-co-blue' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
Notes </a>
Notes </a> -->
<a href="#" @click="tab = 'journeys'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
@ -129,11 +129,11 @@
</div>
</div>
<div x-show="tab == 'notes'">{{template "beneficiary_notes" .}}</div>
<div x-show="tab == 'journeys'">{{template "beneficiary_journeys" .}}</div>
<div x-show="tab == 'vehicles'">{{template "beneficiary_vehicles" .}}</div>
<div x-show="tab == 'events'">{{template "beneficiary_events" .}}</div>
<div x-show="tab == 'files'">{{template "beneficiary_files" .}}</div>
<div x-show="tab == 'notes'">{{template "beneficiary_notes" .}}</div>
</div>
</div>
</section>

View File

@ -92,9 +92,24 @@
<div class="sm:pb-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Gestionnaire</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
COOPGO
{{.ViewState.group.Data.name}}
</dd>
</div>
{{if .ViewState.booking.Data.booked_by}}
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<!-- <div class="sm:pb-5 sm:grid sm:grid-cols-3 sm:gap-4"> -->
<dt class="text-sm font-medium text-gray-500">Réservé par</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{if .ViewState.booking.Data.booked_by.user}}
<a href="/app/members/{{.ViewState.booking.Data.booked_by.user.id}}" class="flex inline">
<img class="h-5 w-5 rounded-co mr-1"
src="/app/members/{{.ViewState.booking.Data.booked_by.user.id}}/picture" alt="">
{{.ViewState.booking.Data.booked_by.user.display_name}}
</a>
{{end}}
</dd>
</div>
{{end}}
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Véhicule</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
@ -122,7 +137,7 @@
"02/01/2006"}}</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Date de fin</dt>
<dt class="text-sm font-medium text-gray-500">Date de retour</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{(timeFrom .ViewState.booking.Enddate).Format
"02/01/2006"}}</dd>
</div>

View File

@ -0,0 +1,68 @@
{{define "content"}}
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h1 class="text-2xl font-semibold text-gray-900">Réservations</h1>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<div class="mt-8 flex flex-col">
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<!-- <th scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Statut
</th> -->
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Type
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Beneficiaire
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Dates
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{{range .ViewState.bookings}}
<tr>
<!-- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >aa</div>
</td> -->
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >Voiture</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" ><img class="h-6 w-6 rounded-co"
src="/app/beneficiaries/{{.Driver}}/picture" alt=""></div>
</td>
<!-- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >aa</div>
</td> -->
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >Du {{(timeFrom .Startdate).Format "02/01/2006"}} au {{(timeFrom .Enddate).Format "02/01/2006"}}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<a href="/app/vehicles/bookings/{{.ID}}"
class="text-co-blue hover:text-co-blue">Voir</a>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{{end}}

View File

@ -7,7 +7,7 @@
<p class="mt-2 text-sm text-gray-700"></p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<a href="/app/vehicles-management/fleet/add">
<a href="/app/vehicles/bookings/">
<button type="button"
class="inline-flex items-center justify-center rounded-2xl border border-transparent bg-co-blue px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 sm:w-auto">
Voir les prêts de véhicules
@ -33,7 +33,7 @@
return this.beneficiaries.filter(b => b['data']['first_name'].includes(text) || b['data']['last_name'].includes(text))
},
fields: {
beneficiaryid: null,
beneficiaryid: {{if .ViewState.search}}'{{.ViewState.search.beneficiary.ID}}'{{else}}null{{end}},
},
selectbeneficiary(beneficiary) {
console.log(beneficiary)

View File

@ -46,7 +46,8 @@
<div class="text-gray-900" >{{$vehicle.Data.licence_plate}}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" ></div>
<div class="text-gray-900" ><img class="h-6 w-6 rounded-co"
src="/app/beneficiaries/{{.Driver}}/picture" alt=""></div>
</td>
<!-- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >aa</div>

View File

@ -7,6 +7,10 @@
<p class="mt-2 text-sm text-gray-700"></p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button type="button"
class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-2xl text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-co-blue">
Changer de véhicule
</button>
<a href="/app/vehicles-management/bookings/{{.ViewState.booking.ID}}/delete">
<button type="button"
class="inline-flex items-center justify-center rounded-2xl border border-transparent bg-co-red px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-co-red focus:ring-offset-2 sm:w-auto">
@ -58,7 +62,8 @@
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Adresse</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{.ViewState.Data.address.properties.label}}</dd>
{{.ViewState.Data.address.properties.label}}
</dd>
</div>
{{end}}
</dl>
@ -89,12 +94,28 @@
<div>
<div class="mt-5 border-gray-200">
<dl class="sm:divide-y sm:divide-gray-200">
<div class="sm:pb-5 sm:grid sm:grid-cols-3 sm:gap-4">
<!-- <div class="sm:pb-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Gestionnaire</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
COOPGO
</dd>
</div> -->
{{if .ViewState.booking.Data.booked_by}}
<!-- <div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4"> -->
<div class="sm:pb-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Prescripteur</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{if .ViewState.booking.Data.booked_by.user}}
<a href="/app/members/{{.ViewState.booking.Data.booked_by.user.id}}" class="flex inline">
<img class="h-5 w-5 rounded-co mr-1"
src="/app/members/{{.ViewState.booking.Data.booked_by.user.id}}/picture" alt="">
{{.ViewState.booking.Data.booked_by.user.display_name}}
</a>
{{end}}
{{if .ViewState.booking.Data.booked_by.group}}{{.ViewState.booking.Data.booked_by.group.name}}{{end}}
</dd>
</div>
{{end}}
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Véhicule</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
@ -118,23 +139,82 @@
{{end}}
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Date de récupération</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{(timeFrom .ViewState.booking.Startdate).Format
"02/01/2006"}}</dd>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2 inline-flex"
x-data="{ updateOpen: false }">
<div class="w-full inline-flex" x-show="!updateOpen">
<div class="flex-1">{{(timeFrom .ViewState.booking.Startdate).Format "02/01/2006"}}</div>
<a href="#" class="text-co-blue hover:text-co-blue ml-5" @click="updateOpen = ! updateOpen">Modifier</a>
</div>
<form method="POST" class="inline-flex" x-show="updateOpen">
<div class="flex-1">
<input type="date" name="startdate" value="{{(timeFrom .ViewState.booking.Startdate).Format "2006-01-02"}}"
class="shadow-sm focus:ring-co-blue focus:border-co-blue block sm:text-sm border-gray-300 rounded-2xl">
</div>
<button
class=" justify-center text-co-blue px-4">
OK
</button>
</form>
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Date de fin</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{(timeFrom .ViewState.booking.Enddate).Format
"02/01/2006"}}</dd>
<dt class="text-sm font-medium text-gray-500">Date de retour</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"
x-data="{ updateOpen: false }">
<div class="w-full inline-flex" x-show="!updateOpen">
<div class="flex-1">{{(timeFrom .ViewState.booking.Enddate).Format "02/01/2006"}}</div>
<a href="#" class="text-co-blue hover:text-co-blue ml-5" @click="updateOpen = ! updateOpen">Modifier</a>
</div>
<form method="POST" class="inline-flex" x-show="updateOpen">
<div class="flex-1">
<input type="date" name="enddate" value="{{(timeFrom .ViewState.booking.Enddate).Format "2006-01-02"}}"
class="shadow-sm focus:ring-co-blue focus:border-co-blue block sm:text-sm border-gray-300 rounded-2xl">
</div>
<button
class=" justify-center text-co-blue px-4">
OK
</button>
</form>
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Indisponible à partir du</dt>
<dd class="mt-1 text-sm font-bold text-co-red sm:mt-0 sm:col-span-2">{{(timeFrom .ViewState.booking.Unavailablefrom).Format
"02/01/2006"}}</dd>
<dd class="mt-1 text-sm font-bold text-co-red sm:mt-0 sm:col-span-2 inline-flex"
x-data="{ updateOpen: false }">
<div class="w-full inline-flex" x-show="!updateOpen">
<div class="flex-1">{{(timeFrom .ViewState.booking.Unavailablefrom).Format "02/01/2006"}}</div>
<a href="#" class="text-co-blue hover:text-co-blue ml-5" @click="updateOpen = ! updateOpen">Modifier</a>
</div>
<form method="POST" class="inline-flex" x-show="updateOpen">
<div class="flex-1">
<input type="date" name="unavailablefrom" value="{{(timeFrom .ViewState.booking.Unavailablefrom).Format "2006-01-02"}}"
class="shadow-sm focus:ring-co-blue focus:border-co-blue block sm:text-sm border-gray-300 rounded-2xl">
</div>
<button
class=" justify-center text-co-blue px-4">
OK
</button>
</form>
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">Sera à nouveau disponible le</dt>
<dd class="mt-1 text-sm font-bold text-co-green sm:mt-0 sm:col-span-2">{{(timeFrom .ViewState.booking.Unavailableto).Format
"02/01/2006"}}</dd>
<dd class="mt-1 text-sm font-bold text-co-green sm:mt-0 sm:col-span-2 inline-flex"
x-data="{ updateOpen: false }">
<div class="w-full inline-flex" x-show="!updateOpen">
<div class="flex-1">{{(timeFrom .ViewState.booking.Unavailableto).Format "02/01/2006"}}</div>
<a href="#" class="text-co-blue hover:text-co-blue ml-5" @click="updateOpen = ! updateOpen">Modifier</a>
</div>
<form method="POST" class="inline-flex" x-show="updateOpen">
<div class="flex-1">
<input type="date" name="unavailableto" value="{{(timeFrom .ViewState.booking.Unavailableto).Format "2006-01-02"}}"
class="shadow-sm focus:ring-co-blue focus:border-co-blue block sm:text-sm border-gray-300 rounded-2xl">
</div>
<button
class=" justify-center text-co-blue px-4">
OK
</button>
</dd>
</div>
</dl>
</div>

View File

@ -74,7 +74,7 @@
{{range .ViewState.vehicle.Bookings}}
<li class="py-4 flex">
<div class="ml-3">
<a href="/vehicles/bookings/{{.ID}}" class="hover:bg-gray-200">
<a href="/app/vehicles-management/bookings/{{.ID}}" class="hover:bg-gray-200">
<p class="text-sm font-medium text-gray-900">Du {{(timeFrom .Startdate).Format "02/01/2006"}} au {{(timeFrom .Enddate).Format "02/01/2006"}}</p>
<p class="text-sm text-gray-500"></p>
</a>

View File

@ -798,14 +798,6 @@ html {
left: 0px;
}
.top-4 {
top: 1rem;
}
.left-4 {
left: 1rem;
}
.z-40 {
z-index: 40;
}
@ -830,22 +822,10 @@ html {
grid-column: span 3 / span 3;
}
.col-span-2 {
grid-column: span 2 / span 2;
}
.col-span-4 {
grid-column: span 4 / span 4;
}
.col-span-5 {
grid-column: span 5 / span 5;
}
.m-auto {
margin: auto;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
@ -861,6 +841,11 @@ html {
margin-right: -1rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.-my-1\.5 {
margin-top: -0.375rem;
margin-bottom: -0.375rem;
@ -871,9 +856,9 @@ html {
margin-bottom: -0.25rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.-mr-12 {
@ -952,18 +937,18 @@ html {
margin-bottom: -1px;
}
.-mb-8 {
margin-bottom: -2rem;
}
.-ml-px {
margin-left: -1px;
}
.-ml-4 {
margin-left: -1rem;
}
.-mt-4 {
margin-top: -1rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.-mt-2 {
margin-top: -0.5rem;
}
@ -980,12 +965,20 @@ html {
margin-right: -0.25rem;
}
.mb-4 {
margin-bottom: 1rem;
.mr-2 {
margin-right: 0.5rem;
}
.-mt-4 {
margin-top: -1rem;
.mr-0 {
margin-right: 0px;
}
.mt-16 {
margin-top: 4rem;
}
.mt-12 {
margin-top: 3rem;
}
.block {
@ -1012,10 +1005,6 @@ html {
display: table;
}
.flow-root {
display: flow-root;
}
.grid {
display: grid;
}
@ -1060,10 +1049,6 @@ html {
height: 3rem;
}
.h-96 {
height: 24rem;
}
.max-h-60 {
max-height: 15rem;
}
@ -1116,12 +1101,8 @@ html {
width: 3rem;
}
.w-0\.5 {
width: 0.125rem;
}
.w-0 {
width: 0px;
.w-11 {
width: 2.75rem;
}
.min-w-0 {
@ -1190,6 +1171,11 @@ html {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-x-5 {
--tw-translate-x: 1.25rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.scale-95 {
--tw-scale-x: .95;
--tw-scale-y: .95;
@ -1221,10 +1207,6 @@ html {
user-select: none;
}
.grid-flow-row {
grid-auto-flow: row;
}
.grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
@ -1233,6 +1215,10 @@ html {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@ -1241,10 +1227,6 @@ html {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.flex-row {
flex-direction: row;
}
@ -1344,24 +1326,18 @@ html {
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.-space-x-px > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(-1px * var(--tw-space-x-reverse));
margin-left: calc(-1px * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
@ -1374,12 +1350,6 @@ html {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1.5rem * var(--tw-space-x-reverse));
margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 1;
}
@ -1478,9 +1448,9 @@ html {
border-bottom-right-radius: 0.375rem;
}
.rounded-b-2xl {
.rounded-r-2xl {
border-top-right-radius: 1rem;
border-bottom-right-radius: 1rem;
border-bottom-left-radius: 1rem;
}
.rounded-l-2xl {
@ -1488,9 +1458,9 @@ html {
border-bottom-left-radius: 1rem;
}
.rounded-r-2xl {
border-top-right-radius: 1rem;
.rounded-b-2xl {
border-bottom-right-radius: 1rem;
border-bottom-left-radius: 1rem;
}
.border {
@ -1501,10 +1471,6 @@ html {
border-width: 2px;
}
.border-4 {
border-width: 4px;
}
.border-r {
border-right-width: 1px;
}
@ -1521,18 +1487,10 @@ html {
border-bottom-width: 2px;
}
.border-r-0 {
border-right-width: 0px;
}
.border-l-0 {
border-left-width: 0px;
}
.border-dashed {
border-style: dashed;
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@ -1602,31 +1560,6 @@ html {
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
@ -1637,11 +1570,6 @@ html {
background-color: rgb(238 242 255 / var(--tw-bg-opacity));
}
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.bg-indigo-600 {
--tw-bg-opacity: 1;
background-color: rgb(79 70 229 / var(--tw-bg-opacity));
@ -1652,6 +1580,11 @@ html {
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.bg-opacity-75 {
--tw-bg-opacity: 0.75;
}
@ -1660,8 +1593,8 @@ html {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
.p-12 {
padding: 3rem;
}
.p-2 {
@ -1676,14 +1609,6 @@ html {
padding: 0.25rem;
}
.p-8 {
padding: 2rem;
}
.p-12 {
padding: 3rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
@ -1749,26 +1674,16 @@ html {
padding-bottom: 0.75rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-0 {
padding-top: 0px;
padding-bottom: 0px;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.pt-5 {
padding-top: 1.25rem;
}
@ -1809,40 +1724,28 @@ html {
padding-left: 1rem;
}
.pb-8 {
padding-bottom: 2rem;
}
.pt-1\.5 {
padding-top: 0.375rem;
}
.pt-1 {
padding-top: 0.25rem;
}
.pr-4 {
padding-right: 1rem;
}
.pr-2 {
padding-right: 0.5rem;
.pr-12 {
padding-right: 3rem;
}
.pt-8 {
padding-top: 2rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pl-1\.5 {
padding-left: 0.375rem;
}
.pl-3\.5 {
padding-left: 0.875rem;
}
.pr-12 {
padding-right: 3rem;
.pb-1 {
padding-bottom: 0.25rem;
}
.text-left {
@ -1857,6 +1760,10 @@ html {
text-align: right;
}
.text-justify {
text-align: justify;
}
.align-middle {
vertical-align: middle;
}
@ -1916,10 +1823,6 @@ html {
font-weight: 800;
}
.font-black {
font-weight: 900;
}
.uppercase {
text-transform: uppercase;
}
@ -1989,16 +1892,16 @@ html {
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.text-co-green {
--tw-text-opacity: 1;
color: rgb(108 193 31 / var(--tw-text-opacity));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.placeholder-gray-500::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(107 114 128 / var(--tw-placeholder-opacity));
@ -2058,9 +1961,9 @@ html {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-8 {
.ring-0 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
@ -2101,6 +2004,14 @@ html {
transition-duration: 150ms;
}
.transition-colors {
transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-300 {
transition-duration: 300ms;
}
@ -2113,6 +2024,10 @@ html {
transition-duration: 75ms;
}
.duration-200 {
transition-duration: 200ms;
}
.ease-linear {
transition-timing-function: linear;
}
@ -2188,21 +2103,16 @@ html {
background-color: rgb(36 56 135 / var(--tw-bg-opacity));
}
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.hover\:bg-indigo-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(67 56 202 / var(--tw-bg-opacity));
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.hover\:bg-opacity-5:hover {
--tw-bg-opacity: 0.05;
}
@ -2222,9 +2132,9 @@ html {
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.hover\:text-indigo-900:hover {
.hover\:text-co-blue:hover {
--tw-text-opacity: 1;
color: rgb(49 46 129 / var(--tw-text-opacity));
color: rgb(36 56 135 / var(--tw-text-opacity));
}
.hover\:text-gray-900:hover {
@ -2232,23 +2142,13 @@ html {
color: rgb(17 24 39 / var(--tw-text-opacity));
}
.hover\:text-co-blue:hover {
--tw-text-opacity: 1;
color: rgb(36 56 135 / var(--tw-text-opacity));
}
.hover\:text-inherit:hover {
color: inherit;
}
.hover\:text-gray-500:hover {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.hover\:text-indigo-500:hover {
--tw-text-opacity: 1;
color: rgb(99 102 241 / var(--tw-text-opacity));
.hover\:text-inherit:hover {
color: inherit;
}
.focus\:border-transparent:focus {
@ -2327,16 +2227,16 @@ html {
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
}
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.focus\:ring-co-red:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(255 19 0 / var(--tw-ring-opacity));
}
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
@ -2367,6 +2267,10 @@ html {
grid-column: span 1 / span 1;
}
.sm\:col-span-6 {
grid-column: span 6 / span 6;
}
.sm\:mx-auto {
margin-left: auto;
margin-right: auto;
@ -2437,10 +2341,6 @@ html {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.sm\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}

View File

@ -51,7 +51,7 @@ func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHand
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "groups", "profile"},
Scopes: []string{oidc.ScopeOpenID, "groups", "first_name", "last_name", "display_name"},
}
var store = sessions.NewCookieStore([]byte(sessionsSecret))