rentease/internal/view/booking_by_id.templ
Ruidy 91a9a74750
Some checks are pending
CI / checks (push) Waiting to run
feat(stripe): add payment link creation for bookings
Introduce backend and frontend support for generating Stripe payment
links
for outstanding booking balances. Adds a new POST endpoint to create
payment
links, updates booking view to include a Stripe button, and integrates
error handling and feedback for payment link creation. Refactors view
models and templates to support the new feature.
2025-11-01 17:22:13 +01:00

215 lines
6.2 KiB
Text

package view
import (
"fmt"
"github.com/rjNemo/rentease/internal/view/layout"
)
templ BookingById(booking *BookingViewModel) {
@layout.BaseLayout() {
<section class="flex justify-between items-center mb-6">
<hgroup>
<h1 class="text-3xl font-bold mb-1">{ booking.Name }</h1>
<p class="text-lg text-base-content/80">{ booking.Id }</p>
</hgroup>
<div class="flex space-x-2">
<a class="btn btn-outline btn-secondary" href={ booking.PdfUrl } target="_blank">Create PDF</a>
<a
href="https://web.whatsapp.com/"
target="_blank"
rel="noreferrer noopener"
class="btn btn-ghost btn-sm btn-square"
>
<img src="/static/icons/whatsapp.png" class="w-6 h-6"/>
</a>
if booking.Canceled {
<span class="badge badge-error">Canceled</span>
} else {
<button class="btn btn-outline" hx-patch={ booking.CancelUrl } hx-swap="outerHTML">
Cancel
</button>
}
</div>
</section>
<section>
@BookingForm(*booking)
</section>
<section class="card bg-base-100 shadow-md p-6 mb-8">
<hgroup class="flex justify-between items-center">
<h2
class="text-xl font-semibold mb-4 border-b pb-2 border-base-content/10"
>
Line Items
</h2>
<div class="flex items-center gap-2">
<button class="btn btn-secondary btn-sm " onclick="document.getElementById('payment_modal').showModal()">Add Payment</button>
<button
type="button"
class="btn btn-ghost btn-sm btn-square"
data-payment-link-url={ booking.StripePaymentLinkUrl }
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>Item</th>
<th>Quantity</th>
<th>Price (€)</th>
<th>Payment Method</th>
<th>Sub-total (€)</th>
<th class="text-right">Actions</th>
</tr>
</thead>
@ItemList(booking.Items)
<tfoot>
<tr>
<td colspan="4" class="text-right font-bold">Total:</td>
<td colspan="2" class="font-bold">{ booking.Total }</td>
</tr>
</tfoot>
</table>
</div>
</section>
<div class="border-t pt-4 border-base-content/10">
<h3 class="text-lg font-semibold mb-3">Add New Line Item</h3>
<form
hx-post={ fmt.Sprintf("%s/items", booking.Url) }
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">Item</span>
</label>
<select class="select select-bordered w-full" name="item" id="new-line-item">
for _, item := range booking.ItemList {
<option value={ item }>{ item }</option>
}
</select>
</div>
<div class="form-control w-full">
<label class="label" for="new-line-quantity">
<span class="label-text">Quantity</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">Price (€)</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">Add Line Item</button>
</div>
</form>
</div>
<script>
async function createStripePaymentLink(event) {
const button = event.currentTarget;
if (!button) {
return;
}
const endpoint = button.dataset.paymentLinkUrl;
if (!endpoint) {
return;
}
button.disabled = true;
button.classList.add("loading");
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Accept": "application/json",
},
});
if (!response.ok) {
const message = await extractStripePaymentLinkError(response);
alert(message);
return;
}
const data = await response.json();
if (data && data.url) {
window.open(data.url, "_blank", "noopener");
}
} catch (error) {
console.error("Failed to create Stripe payment link", error);
alert("Unable to create the Stripe payment link. Please try again.");
} finally {
button.disabled = false;
button.classList.remove("loading");
}
}
async function extractStripePaymentLinkError(response) {
try {
const payload = await response.json();
if (payload && payload.message) {
return payload.message;
}
} catch (_error) {
// ignore parsing errors
}
return response.statusText || "Unexpected error while creating payment link.";
}
</script>
@PaymentModal(booking.PaymentUrl)
}
}
templ PaymentModal(paymentUrl string) {
<dialog id="payment_modal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">Add Payment</h3>
<form
class="py-4 space-y-4"
hx-post={ paymentUrl }
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">Amount</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">Payment Method</span>
</label>
<select name="paymentMethod" class="select select-bordered w-full" required>
<option value="">Select payment method</option>
<option value="Cash">Cash</option>
<option value="Card">Card</option>
<option value="Cheque">Cheque</option>
<option value="Transfer">Bank Transfer</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Add Payment </button>
</form>
</div>
</dialog>
}