Compare commits

...

3 commits

Author SHA1 Message Date
bb71291278
fix(booking): restore stripe payment link button handler
Some checks failed
CI / checks (push) Has been cancelled
2026-02-18 19:00:22 +01:00
e138d493ab
fix(api): return bookingURL only from sync endpoint 2026-02-18 18:39:43 +01:00
df1cd66cf1
chore: wire APP_OPENAI_MODEL and upgrade Go/deps 2026-02-18 18:32:01 +01:00
16 changed files with 295 additions and 256 deletions

View file

@ -1,11 +1,12 @@
DATABASE_URL=
DEBUG=
LOG_LEVEL=
ORIGINS=
PORT=
SENTRY_DSN=
ADMIN=
ADMIN_SECRET=
API_KEY=
SECRET_KEY=
SESSION_SECRET=
APP_DATABASE_URL=
APP_DEBUG=
APP_LOG_LEVEL=
APP_ORIGINS=
APP_PORT=
APP_SENTRY_DSN=
APP_ADMIN=
APP_ADMIN_SECRET=
APP_API_KEY=
APP_SECRET_KEY=
APP_SESSION_SECRET=
APP_OPENAI_MODEL=gpt-5-nano

View file

@ -1,52 +1,43 @@
# Repository Guidelines
## Project Structure & Module Organization
- `main.go`: Application entrypoint (HTTP server on `:8000`).
- `internal/`: Private app code (e.g., `server/`, `service/`, `repository/`, `driver/`,
`config/`, `view/`). Templ views live in `internal/view` and generate `*_templ.go`.
- `pkg/`: Reusable packages shared across app layers.
- `cmd/`: Optional binaries/entrypoints.
- `assets/`: Static assets and images.
- `docs/`: Project documentation.
- `scripts/`, `tmp/`: Dev tooling and build artifacts (Air uses `tmp/`).
- `main.go`: application entrypoint (HTTP server).
- `internal/`: core app code by layer:
- `server/` (HTTP handlers/routes), `service/` (business logic), `repository/` (data access), `driver/` (external integrations), `config/` (env config), `view/` (templ views/viewmodels).
- `internal/view/*.templ` are source templates; generated files are `internal/view/*_templ.go`.
- `cmd/cron/`: cron entrypoint.
- `assets/`: static assets. `docs/`: documentation/images. `scripts/` and `tmp/`: tooling/artifacts.
- Tests live next to code as `*_test.go`.
## Build, Test, and Development Commands
- `make dev`: Start dev container with live reload (Air) on `http://localhost:8000`.
- `make run`: Build and run the Docker image with `PORT` and `DATABASE_URL`.
- `make test`: Run `go test ./...` inside the running dev container.
- `make format`: Run `templ generate`, `templ fmt`, and `go fmt`.
- `make lint`: Run `golangci-lint`.
- Local alternative: `air -c .air.toml`, `go test ./...`, `go run .`.
- `make dev`: run local dev stack with Docker Compose and hot reload.
- `make run`: build and run production image locally.
- `make test`: run `go test ./...`.
- `make format`: run `templ generate`, `templ fmt`, and `go fmt`.
- `make lint`: run `golangci-lint run ./...`.
- `make stop`: stop dev containers.
- Local (without Docker): `go run .`, `go test ./...`.
## Coding Style & Naming Conventions
- Go formatting: Use `go fmt` (tabs, standard import ordering). CI helpers:
`make format`.
- Linting: `golangci-lint run ./...` via `make lint` (fix issues before PR).
- Packages: lowercase, no underscores; files: lowercase with underscores; exported
identifiers: `PascalCase`; unexported: `camelCase`.
- Templ: keep `.templ` in `internal/view`; commit sources, not generated `*_templ.go`.
- Go style is standard `go fmt` (tabs, canonical imports).
- Package names: lowercase, no underscores.
- File names: lowercase with underscores when needed.
- Exported identifiers: `PascalCase`; unexported: `camelCase`.
- Keep handler/controller code thin; place business rules in `internal/service`.
## Testing Guidelines
- Framework: standard `testing` with table-driven tests.
- Files: `*_test.go`; functions: `TestXxx`, benchmarks: `BenchmarkXxx`.
- Run: `make test` (in container) or `go test ./...` locally.
- Aim for meaningful coverage on new/changed code; include error paths.
- Use Go `testing` with table-driven tests where practical.
- Name files `*_test.go`; tests `TestXxx`; benchmarks `BenchmarkXxx`.
- Cover success and failure paths for changed logic.
- Run `make test` (or `go test ./...`) before opening a PR.
## Commit & Pull Request Guidelines
- Messages: Prefer Conventional Commits (e.g., `feat(parser): ...`,
`fix(config): ...`). Short, imperative first line; scope optional.
- PRs: Provide clear description, link issues (e.g., `#45`), include screenshots
for UI, and note breaking changes/migrations.
- Keep diffs focused; run `make format` and `make lint` before opening.
- Prefer Conventional Commits: `feat(scope): ...`, `fix(scope): ...`, `chore: ...`.
- Keep commits focused and atomic.
- PRs should include: clear summary, linked issue (e.g., `#51`), and screenshots for UI changes.
- Call out config/env changes and any migration or deployment impact.
## Security & Configuration Tips
- Environment: Use `.env`/`prod.env`; never commit secrets.
Example: `DATABASE_URL="host=... user=... database=..."`.
- Ports: default `8000`; configure via `PORT`.
- Dependencies: use `go mod tidy` and `make up-deps` when updating.
- Config is environment-driven with `APP_` prefix (see `internal/config/config.go`).
- Example parser model override: `APP_OPENAI_MODEL=gpt-5-nano`.
- Never commit secrets; keep them in local `.env` / deployment secret manager.

View file

@ -1,4 +1,4 @@
FROM golang:1.25-alpine AS builder
FROM golang:1.26-alpine AS builder
RUN apk update && apk add --no-cache \
build-base \

View file

@ -1,5 +1,5 @@
# ----------- Builder Stage -----------
FROM golang:1.25-alpine AS builder
FROM golang:1.26-alpine AS builder
WORKDIR /app
RUN apk add --no-cache build-base
@ -22,7 +22,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
go build -ldflags="-s -w" -o rentease main.go
# ----------- Dev Stage -----------
FROM golang:1.25-alpine AS dev
FROM golang:1.26-alpine AS dev
WORKDIR /app
# Install runtime dependencies

View file

@ -83,6 +83,7 @@ APP_ORIGINS=http://localhost:8000
APP_PORT=8000
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_OPENAI_MODEL=gpt-5-nano
APP_STRIPE_SECRET_KEY=
APP_STRIPE_WEBHOOK_SECRET=
APP_SENTRY_DSN=

View file

@ -15,6 +15,7 @@ services:
APP_LOG_LEVEL: debug
APP_PORT: 8000
APP_ORIGINS: http://localhost:8000
APP_OPENAI_MODEL: gpt-5-nano
APP_DATABASE_URL: postgres://rentease:rentease@db:5432/rentease?sslmode=disable
APP_ADMIN: admin@example.com
APP_ADMIN_SECRET: supersecret

10
go.mod
View file

@ -1,11 +1,11 @@
module github.com/rjNemo/rentease
go 1.25.4
go 1.26
require (
github.com/a-h/templ v0.3.977
github.com/getsentry/sentry-go v0.40.0
github.com/go-chi/chi/v5 v5.2.3
github.com/getsentry/sentry-go v0.42.0
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
github.com/gorilla/sessions v1.4.0
github.com/joho/godotenv v1.5.1
@ -36,6 +36,6 @@ require (
github.com/openai/openai-go v1.12.0
github.com/sethvargo/go-envconfig v1.3.0
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.32.0
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0
)

16
go.sum
View file

@ -5,10 +5,10 @@ github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7RLEt8=
github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@ -75,10 +75,10 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -25,6 +25,8 @@ type Config struct {
Port int `env:"PORT, default=4200"`
// SentryDsn is the DSN for Sentry error reporting
SentryDsn string `env:"SENTRY_DSN"`
// OpenAIModel is the OpenAI model used by the booking parser
OpenAIModel string `env:"OPENAI_MODEL, default=gpt-5-nano"`
// Auth
// Admin is the email used to access the admin panel
Admin string `env:"ADMIN, required"`

View file

@ -16,11 +16,13 @@ import (
type BookingAgentParser struct {
systemPrompt string
llmClient openai.Client
model string
}
func NewBookingAgentParser() *BookingAgentParser {
func NewBookingAgentParser(model string) *BookingAgentParser {
return &BookingAgentParser{
llmClient: openai.NewClient(),
model: model,
systemPrompt: ` Extract the following fields from the booking content and return a JSON object with this exact structure (all fields required, use null or empty string if not available):
{
@ -71,7 +73,7 @@ func (p *BookingAgentParser) Parse(rawContent string) (*booking.Booking, error)
resp, err := p.llmClient.Responses.New(ctx, responses.ResponseNewParams{
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String(prompt)},
Model: openai.ChatModelChatgpt4oLatest,
Model: p.model,
})
if err != nil {
return nil, fmt.Errorf("error sending request to OpenAI: %w", err)

View file

@ -47,7 +47,7 @@ func TestBookingAgentParser_Parse(t *testing.T) {
Les conversations avec vos clients apparaîtront ici.
Booking.com reçoit tous les messages écrits ici et les traite selon sa Charte de confidentialité et informations sur les cookies Conditions `
parser := NewBookingAgentParser()
parser := NewBookingAgentParser("gpt-5-nano")
booking, err := parser.Parse(input)
if err != nil {
t.Fatalf("Parse failed: %v", err)

View file

@ -165,7 +165,7 @@ func buildPaymentLinkCreateParams(params CreatePaymentLinkParams) (*stripe.Payme
ProductData: &stripe.PaymentLinkCreateLineItemPriceDataProductDataParams{
Name: stripe.String(strings.TrimSpace(params.Description)),
},
UnitAmount: stripe.Int64(amountCents),
UnitAmount: new(amountCents),
},
Quantity: stripe.Int64(1),
},

View file

@ -50,7 +50,7 @@ func handleSync(bs *booking.Service) http.HandlerFunc {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(fmt.Sprintf("👍 %s", bookingURL)); err != nil {
if err := json.NewEncoder(w).Encode(map[string]string{"bookingURL": bookingURL}); err != nil {
slog.Error("failed to write response", slog.Any("error", err))
}
}

View file

@ -49,6 +49,9 @@ templ BookingById(booking *BookingViewModel) {
type="button"
class="btn btn-ghost btn-sm btn-square"
data-payment-link-url={ booking.StripePaymentLinkUrl }
data-payment-link-failed={ i18n.Localize(ctx, "booking.payment_link_failed") }
data-payment-link-unavailable={ i18n.Localize(ctx, "booking.payment_link_unavailable") }
data-payment-link-unexpected={ i18n.Localize(ctx, "booking.payment_link_unexpected") }
aria-label="Create Stripe payment link"
onclick="createStripePaymentLink(event)"
>
@ -129,10 +132,6 @@ templ BookingById(booking *BookingViewModel) {
</form>
</div>
<script>
const paymentLinkFailedMessage = "{ i18n.Localize(ctx, "booking.payment_link_failed") }";
const paymentLinkUnavailableMessage = "{ i18n.Localize(ctx, "booking.payment_link_unavailable") }";
const paymentLinkUnexpectedMessage = "{ i18n.Localize(ctx, "booking.payment_link_unexpected") }";
async function createStripePaymentLink(event) {
const button = event.currentTarget;
if (!button) {
@ -142,6 +141,9 @@ templ BookingById(booking *BookingViewModel) {
if (!endpoint) {
return;
}
const failedMessage = button.dataset.paymentLinkFailed || "Unable to create the Stripe payment link. Please try again.";
const unavailableMessage = button.dataset.paymentLinkUnavailable || "Unable to create the Stripe payment link.";
const unexpectedMessage = button.dataset.paymentLinkUnexpected || "Unexpected error while creating payment link.";
button.disabled = true;
button.classList.add("loading");
try {
@ -152,7 +154,7 @@ templ BookingById(booking *BookingViewModel) {
},
});
if (!response.ok) {
const message = await extractStripePaymentLinkError(response);
const message = await extractStripePaymentLinkError(response, unavailableMessage, unexpectedMessage);
alert(message);
return;
}
@ -162,14 +164,14 @@ templ BookingById(booking *BookingViewModel) {
}
} catch (error) {
console.error("Failed to create Stripe payment link", error);
alert(paymentLinkFailedMessage);
alert(failedMessage);
} finally {
button.disabled = false;
button.classList.remove("loading");
}
}
async function extractStripePaymentLinkError(response) {
async function extractStripePaymentLinkError(response, unavailableMessage, unexpectedMessage) {
try {
const payload = await response.json();
if (payload && payload.message) {
@ -177,9 +179,9 @@ templ BookingById(booking *BookingViewModel) {
}
} catch (_error) {
// ignore parsing errors
return paymentLinkUnexpectedMessage;
return unexpectedMessage;
}
return paymentLinkUnavailableMessage;
return unavailableMessage;
}
</script>
@PaymentModal(booking.PaymentUrl)

View file

@ -201,53 +201,53 @@ func BookingById(booking *BookingViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" aria-label=\"Create Stripe payment link\" onclick=\"createStripePaymentLink(event)\"><img src=\"/static/icons/stripe.png\" class=\"w-6 h-6\"></button></div></hgroup><div class=\"overflow-x-auto mb-6\"><table class=\"table w-full\"><thead><tr><th>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" data-payment-link-failed=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.item"))
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.payment_link_failed"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 63, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 52, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</th><th>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" data-payment-link-unavailable=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.quantity"))
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.payment_link_unavailable"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 64, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 53, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</th><th>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" data-payment-link-unexpected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.price"))
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.payment_link_unexpected"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 65, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 54, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " (€)</th><th>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" aria-label=\"Create Stripe payment link\" onclick=\"createStripePaymentLink(event)\"><img src=\"/static/icons/stripe.png\" class=\"w-6 h-6\"></button></div></hgroup><div class=\"overflow-x-auto mb-6\"><table class=\"table w-full\"><thead><tr><th>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.payment_method"))
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.item"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 66, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 66, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@ -258,7 +258,7 @@ func BookingById(booking *BookingViewModel) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.subtotal"))
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.quantity"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 67, Col: 56}
}
@ -266,20 +266,59 @@ func BookingById(booking *BookingViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " (€)</th><th class=\"text-right\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</th><th>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.actions"))
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.price"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 68, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 68, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</th></tr></thead>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " (€)</th><th>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.payment_method"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 69, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</th><th>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.subtotal"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 70, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " (€)</th><th class=\"text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.actions"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 71, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</th></tr></thead>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -287,147 +326,147 @@ func BookingById(booking *BookingViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<tfoot><tr><td colspan=\"4\" class=\"text-right font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.total"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 74, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ":</td><td colspan=\"2\" class=\"font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Total)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 75, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td></tr></tfoot></table></div></section><div class=\"border-t pt-4 border-base-content/10\"><h3 class=\"text-lg font-semibold mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.add_line_item_title"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 82, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</h3><form hx-post=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tfoot><tr><td colspan=\"4\" class=\"text-right font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/items", booking.Url))
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.total"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 84, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 77, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" hx-target=\"#line-items\" hx-swap=\"afterend\" hx-on::after-request=\"if(event.detail.successful) this.reset()\" class=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 items-end\"><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-item\"><span class=\"label-text\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, ":</td><td colspan=\"2\" class=\"font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.item"))
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(booking.Total)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 92, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 78, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</span></label> <select class=\"select select-bordered w-full\" name=\"item\" id=\"new-line-item\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td></tr></tfoot></table></div></section><div class=\"border-t pt-4 border-base-content/10\"><h3 class=\"text-lg font-semibold mb-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, item := range booking.ItemList {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(item)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 96, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(item)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 96, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.add_line_item_title"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 85, Col: 98}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</select></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-quantity\"><span class=\"label-text\">")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</h3><form hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/items", booking.Url))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 87, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" hx-target=\"#line-items\" hx-swap=\"afterend\" hx-on::after-request=\"if(event.detail.successful) this.reset()\" class=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 items-end\"><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-item\"><span class=\"label-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.quantity"))
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.item"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 102, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 95, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</span></label> <input type=\"number\" name=\"quantity\" id=\"new-line-quantity\" required class=\"input input-bordered w-full\"></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-price\"><span class=\"label-text\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</span></label> <select class=\"select select-bordered w-full\" name=\"item\" id=\"new-line-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.price"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 114, Col: 73}
for _, item := range booking.ItemList {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(item)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 99, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(item)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 99, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</select></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-quantity\"><span class=\"label-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " (€)</span></label> <input type=\"number\" name=\"price\" inputmode=\"decimal\" step=\"0.01\" id=\"new-line-price\" required class=\"input input-bordered w-full\"></div><div class=\"flex justify-end md:justify-start\"><button type=\"submit\" class=\"btn btn-secondary\">")
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.quantity"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 105, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.add_line_item"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 127, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></label> <input type=\"number\" name=\"quantity\" id=\"new-line-quantity\" required class=\"input input-bordered w-full\"></div><div class=\"form-control w-full\"><label class=\"label\" for=\"new-line-price\"><span class=\"label-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</button></div></form></div><script>\n\t\t\tconst paymentLinkFailedMessage = \"{ i18n.Localize(ctx, \"booking.payment_link_failed\") }\";\n\t\t\tconst paymentLinkUnavailableMessage = \"{ i18n.Localize(ctx, \"booking.payment_link_unavailable\") }\";\n\t\t\tconst paymentLinkUnexpectedMessage = \"{ i18n.Localize(ctx, \"booking.payment_link_unexpected\") }\";\n\n\t\t\tasync function createStripePaymentLink(event) {\n\t\t\t\tconst button = event.currentTarget;\n\t\t\t\tif (!button) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst endpoint = button.dataset.paymentLinkUrl;\n\t\t\t\tif (!endpoint) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tbutton.disabled = true;\n\t\t\t\tbutton.classList.add(\"loading\");\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await fetch(endpoint, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\"Accept\": \"application/json\",\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\tconst message = await extractStripePaymentLinkError(response);\n\t\t\t\t\t\talert(message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst data = await response.json();\n\t\t\t\t\tif (data && data.url) {\n\t\t\t\t\t\twindow.open(data.url, \"_blank\", \"noopener\");\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Failed to create Stripe payment link\", error);\n\t\t\t\t\talert(paymentLinkFailedMessage);\n\t\t\t\t} finally {\n\t\t\t\t\tbutton.disabled = false;\n\t\t\t\t\tbutton.classList.remove(\"loading\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tasync function extractStripePaymentLinkError(response) {\n\t\t\t\ttry {\n\t\t\t\t\tconst payload = await response.json();\n\t\t\t\t\tif (payload && payload.message) {\n\t\t\t\t\t\treturn payload.message;\n\t\t\t\t\t}\n\t\t\t\t} catch (_error) {\n\t\t\t\t\t// ignore parsing errors\n\t\t\t\t\treturn paymentLinkUnexpectedMessage;\n\t\t\t\t}\n\t\t\t\treturn paymentLinkUnavailableMessage;\n\t\t\t}\n\t\t</script> ")
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.price"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 117, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " (€)</span></label> <input type=\"number\" name=\"price\" inputmode=\"decimal\" step=\"0.01\" id=\"new-line-price\" required class=\"input input-bordered w-full\"></div><div class=\"flex justify-end md:justify-start\"><button type=\"submit\" class=\"btn btn-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "booking.view.add_line_item"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 130, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</button></div></form></div><script>\n\t\t\tasync function createStripePaymentLink(event) {\n\t\t\t\tconst button = event.currentTarget;\n\t\t\t\tif (!button) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst endpoint = button.dataset.paymentLinkUrl;\n\t\t\t\tif (!endpoint) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst failedMessage = button.dataset.paymentLinkFailed || \"Unable to create the Stripe payment link. Please try again.\";\n\t\t\t\tconst unavailableMessage = button.dataset.paymentLinkUnavailable || \"Unable to create the Stripe payment link.\";\n\t\t\t\tconst unexpectedMessage = button.dataset.paymentLinkUnexpected || \"Unexpected error while creating payment link.\";\n\t\t\t\tbutton.disabled = true;\n\t\t\t\tbutton.classList.add(\"loading\");\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await fetch(endpoint, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\"Accept\": \"application/json\",\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\tconst message = await extractStripePaymentLinkError(response, unavailableMessage, unexpectedMessage);\n\t\t\t\t\t\talert(message);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst data = await response.json();\n\t\t\t\t\tif (data && data.url) {\n\t\t\t\t\t\twindow.open(data.url, \"_blank\", \"noopener\");\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Failed to create Stripe payment link\", error);\n\t\t\t\t\talert(failedMessage);\n\t\t\t\t} finally {\n\t\t\t\t\tbutton.disabled = false;\n\t\t\t\t\tbutton.classList.remove(\"loading\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tasync function extractStripePaymentLinkError(response, unavailableMessage, unexpectedMessage) {\n\t\t\t\ttry {\n\t\t\t\t\tconst payload = await response.json();\n\t\t\t\t\tif (payload && payload.message) {\n\t\t\t\t\t\treturn payload.message;\n\t\t\t\t\t}\n\t\t\t\t} catch (_error) {\n\t\t\t\t\t// ignore parsing errors\n\t\t\t\t\treturn unexpectedMessage;\n\t\t\t\t}\n\t\t\t\treturn unavailableMessage;\n\t\t\t}\n\t\t</script> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -461,142 +500,142 @@ func PaymentModal(paymentUrl string) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var29 := templ.GetChildren(ctx)
if templ_7745c5c3_Var29 == nil {
templ_7745c5c3_Var29 = templ.NopComponent
templ_7745c5c3_Var32 := templ.GetChildren(ctx)
if templ_7745c5c3_Var32 == nil {
templ_7745c5c3_Var32 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<dialog id=\"payment_modal\" class=\"modal\"><div class=\"modal-box\"><h3 class=\"text-lg font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.title"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 192, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</h3><form class=\"py-4 space-y-4\" hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(paymentUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 195, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" hx-target=\"#payment-lines\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) payment_modal.close()\"><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.amount"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 202, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span></label> <input type=\"number\" step=\"0.01\" name=\"amount\" class=\"input input-bordered w-full\" required autofocus></div><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<dialog id=\"payment_modal\" class=\"modal\"><div class=\"modal-box\"><h3 class=\"text-lg font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.method"))
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.title"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 208, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 194, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span></label> <select name=\"paymentMethod\" class=\"select select-bordered w-full\" required><option value=\"\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</h3><form class=\"py-4 space-y-4\" hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.select_method"))
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(paymentUrl)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 211, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 197, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</option> <option value=\"Cash\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" hx-target=\"#payment-lines\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) payment_modal.close()\"><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.cash"))
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.amount"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 212, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 204, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</option> <option value=\"Card\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</span></label> <input type=\"number\" step=\"0.01\" name=\"amount\" class=\"input input-bordered w-full\" required autofocus></div><div class=\"form-control w-full\"><label class=\"label\"><span class=\"label-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.card"))
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.method"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 213, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 210, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</option> <option value=\"Cheque\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</span></label> <select name=\"paymentMethod\" class=\"select select-bordered w-full\" required><option value=\"\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.cheque"))
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.select_method"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 214, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 213, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</option> <option value=\"Transfer\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</option> <option value=\"Cash\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.transfer"))
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.cash"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 215, Col: 78}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 214, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</option></select></div><button type=\"submit\" class=\"btn btn-primary\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</option> <option value=\"Card\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.submit"))
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.card"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 218, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 215, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</button></form></div></dialog>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</option> <option value=\"Cheque\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.cheque"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 216, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</option> <option value=\"Transfer\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.method.transfer"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 217, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</option></select></div><button type=\"submit\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(i18n.Localize(ctx, "payment.modal.submit"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/booking_by_id.templ`, Line: 220, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</button></form></div></dialog>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -71,7 +71,7 @@ func run(ctx context.Context, appConfig *config.Config, appLogger *slog.Logger)
return fmt.Errorf("error starting pdf client %w", err)
}
parsingClient := parser.NewBookingAgentParser()
parsingClient := parser.NewBookingAgentParser(appConfig.OpenAIModel)
var stripeClient payment.StripeClient
if appConfig.StripeSecretKey != "" {