package server import ( "context" "fmt" "io" "log/slog" "net/http" "strconv" "time" "github.com/a-h/templ" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" u "github.com/rjNemo/underscore" "github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/constant" "github.com/rjNemo/rentease/internal/service/booking" "github.com/rjNemo/rentease/internal/view" myTime "github.com/rjNemo/rentease/pkg/time" ) func handleBookingListPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc { return func(c echo.Context) error { search := c.FormValue("search") var bookings []*booking.Line if search != "" { bookings = bs.Search(search) } else { bookings = bs.All() } slog.Info("serving bookings", slog.Int("bookings_length", len(bookings))) bvm := u.Map(bookings, func(b *booking.Line) *view.ListBookingsViewModel { return &view.ListBookingsViewModel{ Id: b.InvoiceNumber(hc), Url: templ.SafeURL(fmt.Sprintf("%s/%d", constant.RouteBooking, b.Id)), From: b.From.Format(time.DateOnly), To: b.To.Format(time.DateOnly), Platform: b.Platform, Name: b.CustomerName, Total: strconv.FormatFloat(b.Total, 'f', 2, 64), Canceled: b.Canceled, } }) if hxRequest(c) && !hxBoosted(c) { return renderTempl(c, http.StatusOK, view.BookingLines(bvm)) } else { return renderTempl(c, http.StatusOK, view.ListBookings(bvm)) } } } func handleBookingList(bs *booking.Service) echo.HandlerFunc { return func(c echo.Context) error { search := c.FormValue("search") var bookings []*booking.Line if search != "" { bookings = bs.Search(search) } else { bookings = bs.All() } return c.JSON(http.StatusOK, bookings) } } func handleBookingCreatePage(hc *config.Host) echo.HandlerFunc { return func(c echo.Context) error { return renderTempl(c, http.StatusOK, view.NewBooking(u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }))) } } func handleBookingCreate(bs *booking.Service) echo.HandlerFunc { return func(c echo.Context) error { type NewBooking struct { From time.Time `json:"from"` To time.Time `json:"to"` ExternalId *string `form:"external_id"` Name string `form:"name"` PhoneNumber string `form:"phone_number"` Email string `form:"email"` Platform string `form:"platform"` CustomerNumber int `form:"customer_number"` PlatformFees float64 `form:"platform_fees"` } nb := new(NewBooking) err := c.Bind(nb) if err != nil { log.Warn(err) return err } ts, _ := myTime.ParseFromForm(c.FormValue("from")) nb.From = ts ts, _ = myTime.ParseFromForm(c.FormValue("to")) nb.To = ts if *nb.ExternalId == "" { nb.ExternalId = nil } b := bs.Create(nb.From, nb.To, nb.Name, nb.PhoneNumber, nb.Email, nb.Platform, nb.CustomerNumber, nb.PlatformFees, nb.ExternalId) return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%d", constant.RouteBooking, b.Id)) } } func handleBookingPage(bs *booking.Service, hc *config.Host) echo.HandlerFunc { return func(c echo.Context) error { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil { return err } b := bs.One(id) var eid string if b.ExternalId == nil { eid = "" } else { eid = *b.ExternalId } bvm := &view.BookingViewModel{ Id: b.InvoiceNumber(hc), Name: b.Name, PhoneNumber: b.PhoneNumber, CustomerNumber: strconv.Itoa(b.CustomerNumber), Email: b.Email, From: b.From.Format(time.DateOnly), To: b.To.Format(time.DateOnly), Platform: string(b.Platform), ExternalId: eid, Canceled: b.Canceled, PlatformFees: strconv.FormatFloat(b.PlatformFees, 'f', 2, 64), Url: templ.EscapeString(fmt.Sprintf("%s/%d", constant.RouteBooking, b.Id)), PdfUrl: templ.SafeURL(fmt.Sprintf("%s/pdf/%d", constant.RouteBooking, b.Id)), CancelUrl: fmt.Sprintf("%s/%d/cancel", constant.RouteBooking, b.Id), PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, b.Id), Items: view.ItemListViewModel{ Items: u.Map(b.Items, func(i booking.Item) view.ItemViewModel { return view.ItemViewModel{ Id: strconv.Itoa(i.Id), Item: i.Item, Quantity: strconv.Itoa(i.Quantity), Price: strconv.FormatFloat(i.Price, 'f', 2, 64), SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), PaymentStatus: i.PaymentStatus, ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id), } }), Payments: u.Map(b.Payments, func(p booking.Payment) view.PaymentViewModel { return view.PaymentViewModel{ Amount: strconv.FormatFloat(p.Amount, 'f', 2, 64), PaymentMethod: string(p.PaymentMethod), PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, p.ID), } }), }, Total: strconv.FormatFloat(u.Reduce(b.Items, func(i booking.Item, sum float64) float64 { return sum + i.Price*float64(i.Quantity) }, 0.0), 'f', 2, 64), Platforms: u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }), ItemList: u.OrderBy(func(items map[string]config.HostItem) (out []string) { // TODO: return the full item to prefill the form for _, item := range items { out = append(out, item.Name) } return out }(hc.Items), func(l, r string) bool { return l > r }, ), PaymentMethods: hc.PaymentMethods, } return renderTempl(c, http.StatusOK, view.BookingById(bvm)) } } func handleBookingUpdate(bs *booking.Service, hc *config.Host) echo.HandlerFunc { return func(c echo.Context) error { type UpdateBooking struct { From time.Time `json:"from"` To time.Time `json:"to"` ExternalId *string `form:"external_id"` Name string `form:"name"` PhoneNumber string `form:"phone_number"` Email string `form:"email"` Platform string `form:"platform"` Id int `param:"id"` CustomerNumber int `form:"customer_number"` PlatformFees float64 `form:"platform_fees"` } nb := new(UpdateBooking) err := c.Bind(nb) if err != nil { log.Warn(err) return err } ts, _ := myTime.ParseFromForm(c.FormValue("from")) nb.From = ts ts, _ = myTime.ParseFromForm(c.FormValue("to")) nb.To = ts if *nb.ExternalId == "" { nb.ExternalId = nil } b := bs.Update(nb.Id, nb.From, nb.To, nb.Name, nb.PhoneNumber, nb.Email, nb.Platform, nb.CustomerNumber, nb.PlatformFees, nb.ExternalId) form := view.BookingForm(view.BookingViewModel{ Id: b.InvoiceNumber(hc), Name: b.Name, PhoneNumber: b.PhoneNumber, CustomerNumber: strconv.Itoa(b.CustomerNumber), Email: b.Email, From: b.From.Format(time.DateOnly), To: b.To.Format(time.DateOnly), Canceled: b.Canceled, Platform: string(b.Platform), ExternalId: *b.ExternalId, Platforms: u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }), PlatformFees: strconv.FormatFloat(b.PlatformFees, 'f', 2, 64), PaymentMethods: hc.PaymentMethods, Url: templ.EscapeString(fmt.Sprintf("%s/%d", constant.RouteBooking, b.Id)), PdfUrl: templ.SafeURL(fmt.Sprintf("%s/pdf/%d", constant.RouteBooking, b.Id)), }) return renderTempl(c, http.StatusOK, form) } } func handleLineItemForm(bs *booking.Service) echo.HandlerFunc { return func(c echo.Context) error { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil { return err } i := bs.OneItem(id) form := view.LineItemForm(&view.ItemViewModel{ Id: strconv.Itoa(i.Id), Item: i.Item, Quantity: strconv.Itoa(i.Quantity), Price: strconv.FormatFloat(i.Price, 'f', 2, 64), PaymentStatus: i.PaymentStatus, SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id), }) return renderTempl(c, http.StatusOK, form) } } func handleCreateItem(bs *booking.Service, hc *config.Host) echo.HandlerFunc { type NewItem struct { Item string `form:"item"` PaymentMethod string `form:"method"` Quantity int `form:"quantity"` Price float64 `form:"price"` } return func(c echo.Context) error { bookingIdStr := c.Param("id") bid, err := strconv.Atoi(bookingIdStr) if err != nil { return err } b := bs.One(bid) ni := new(NewItem) if err := c.Bind(ni); err != nil { log.Warn(err) return err } itm, ok := hc.Items[ni.Item] if !ok { return fmt.Errorf("invalid item name %q", ni.Item) } newItems := bs.CreateItem(b.Id, itm, ni.Quantity, ni.Price, ni.PaymentMethod, b.CustomerNumber, string(b.Platform)) // TODO: fix the calendar integration // if err = cs.Create( // itm.CalendarId, // b.Name, // fmt.Sprintf("Reservation: %s\n %d voyageur(s)\n", b.Name, b.CustomerNumber), // b.From, b.To, // ); err != nil { // log.Warnf("could not create event: %s", err) // captureError(c, err) // } for _, i := range newItems { _ = renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{ Id: strconv.Itoa(i.Id), Item: i.Item, Quantity: strconv.Itoa(i.Quantity), Price: strconv.FormatFloat(i.Price, 'f', 2, 64), PaymentStatus: i.PaymentStatus, SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id), })) } return nil } } func handleItemPay(bs *booking.Service) echo.HandlerFunc { return func(c echo.Context) error { itemIdStr := c.Param("id") itemId, err := strconv.Atoi(itemIdStr) if err != nil { return err } i := bs.PayItem(itemId) return renderTempl(c, http.StatusOK, view.LineItem(&view.ItemViewModel{ Id: itemIdStr, Item: i.Item, Quantity: strconv.Itoa(i.Quantity), Price: strconv.FormatFloat(i.Price, 'f', 2, 64), PaymentStatus: i.PaymentStatus, SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id), })) } } func handleItemUpdate(bs *booking.Service) echo.HandlerFunc { return func(c echo.Context) error { type updateItem struct { Item string `form:"item"` PaymentMethod string `form:"paymentMethod"` PaymentStatus string `form:"paymentStatus"` Id int `param:"id"` Quantity int `form:"quantity"` Price float64 `form:"price"` } ui := new(updateItem) if err := c.Bind(ui); err != nil { log.Warn(err) return err } i := bs.UpdateItem(ui.Id, ui.Item, ui.Quantity, ui.Price, ui.PaymentMethod, ui.PaymentStatus) return renderTempl(c, http.StatusCreated, view.LineItem(&view.ItemViewModel{ Id: strconv.Itoa(ui.Id), Item: i.Item, Quantity: strconv.Itoa(i.Quantity), Price: strconv.FormatFloat(i.Price, 'f', 2, 64), PaymentStatus: i.PaymentStatus, SubTotal: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64), ItemUrl: fmt.Sprintf("%s/%d", constant.RouteItem, i.Id), })) } } func handlePaymentUpdate(bs *booking.Service) echo.HandlerFunc { type updatePayment struct { Id int `param:"id"` Amount float64 `form:"amount"` PaymentMethod string `form:"paymentMethod"` } return func(c echo.Context) error { up := new(updatePayment) if err := c.Bind(up); err != nil { return fmt.Errorf("could not parse update payment request body: %w", err) } p := bs.UpdatePayment(up.Id, up.Amount, up.PaymentMethod) return renderTempl(c, http.StatusOK, view.PaymentLine(&view.PaymentViewModel{ Amount: strconv.FormatFloat(p.Amount, 'f', 2, 64), PaymentMethod: string(p.PaymentMethod), PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, p.ID), })) } } func handleBookingCancel(bs *booking.Service) echo.HandlerFunc { return func(c echo.Context) error { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil { return err } bs.Cancel(id) return renderTempl(c, http.StatusOK, templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { _, err := io.WriteString(w, " Canceled") return err })) } }