rentease/internal/server/handle_bookings.go

412 lines
12 KiB
Go

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 paymentViewModelFromBookingPayment(p booking.Payment) *view.PaymentViewModel {
stripeStatus := ""
if p.StripeStatus != nil {
stripeStatus = *p.StripeStatus
}
return &view.PaymentViewModel{
Amount: strconv.FormatFloat(p.Amount, 'f', 2, 64),
PaymentMethod: string(p.PaymentMethod),
PaymentUrl: fmt.Sprintf("%s/%d", constant.RoutePayment, p.ID),
StripeStatus: stripeStatus,
}
}
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 *paymentViewModelFromBookingPayment(p)
}),
},
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(paymentViewModelFromBookingPayment(*p)))
}
}
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, " <span>Canceled</span>")
return err
}))
}
}