mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-06 02:36:49 +00:00
222 lines
7.5 KiB
Text
222 lines
7.5 KiB
Text
package view
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/rjNemo/rentease/internal/i18n"
|
|
"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">{ i18n.Localize(ctx, "booking.view.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">{ i18n.Localize(ctx, "booking.view.canceled") }</span>
|
|
} else {
|
|
<button class="btn btn-outline" hx-patch={ booking.CancelUrl } hx-swap="outerHTML">
|
|
{ i18n.Localize(ctx, "booking.view.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"
|
|
>
|
|
{ i18n.Localize(ctx, "booking.view.line_items") }
|
|
</h2>
|
|
<div class="flex items-center gap-2">
|
|
<button class="btn btn-secondary btn-sm " onclick="document.getElementById('payment_modal').showModal()">{ i18n.Localize(ctx, "booking.view.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>{ i18n.Localize(ctx, "booking.view.item") }</th>
|
|
<th>{ i18n.Localize(ctx, "booking.view.quantity") }</th>
|
|
<th>{ i18n.Localize(ctx, "booking.view.price") } (€)</th>
|
|
<th>{ i18n.Localize(ctx, "booking.view.payment_method") }</th>
|
|
<th>{ i18n.Localize(ctx, "booking.view.subtotal") } (€)</th>
|
|
<th class="text-right">{ i18n.Localize(ctx, "booking.view.actions") }</th>
|
|
</tr>
|
|
</thead>
|
|
@ItemList(booking.Items)
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="4" class="text-right font-bold">{ i18n.Localize(ctx, "booking.view.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">{ i18n.Localize(ctx, "booking.view.add_line_item_title") }</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">{ i18n.Localize(ctx, "booking.view.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">{ i18n.Localize(ctx, "booking.view.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">{ i18n.Localize(ctx, "booking.view.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">{ i18n.Localize(ctx, "booking.view.add_line_item") }</button>
|
|
</div>
|
|
</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) {
|
|
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(paymentLinkFailedMessage);
|
|
} 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 paymentLinkUnexpectedMessage;
|
|
}
|
|
return paymentLinkUnavailableMessage;
|
|
}
|
|
</script>
|
|
@PaymentModal(booking.PaymentUrl)
|
|
}
|
|
}
|
|
|
|
templ PaymentModal(paymentUrl string) {
|
|
<dialog id="payment_modal" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="text-lg font-bold">{ i18n.Localize(ctx, "payment.modal.title") }</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">{ i18n.Localize(ctx, "payment.modal.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">{ i18n.Localize(ctx, "payment.modal.method") }</span>
|
|
</label>
|
|
<select name="paymentMethod" class="select select-bordered w-full" required>
|
|
<option value="">{ i18n.Localize(ctx, "payment.modal.select_method") }</option>
|
|
<option value="Cash">{ i18n.Localize(ctx, "payment.method.cash") }</option>
|
|
<option value="Card">{ i18n.Localize(ctx, "payment.method.card") }</option>
|
|
<option value="Cheque">{ i18n.Localize(ctx, "payment.method.cheque") }</option>
|
|
<option value="Transfer">{ i18n.Localize(ctx, "payment.method.transfer") }</option>
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">{ i18n.Localize(ctx, "payment.modal.submit") }</button>
|
|
</form>
|
|
</div>
|
|
</dialog>
|
|
}
|