Add database tables and repository for managing bookings Create API endpoints for booking submission and capacity management Update landing page to persist bookings before WhatsApp redirection
210 lines
6.2 KiB
Vue
210 lines
6.2 KiB
Vue
<script lang="ts" setup>
|
|
import type { PublicBooking } from '~~/shared/booking'
|
|
|
|
import {
|
|
formatBookingCurrency,
|
|
getBookingModeLabel,
|
|
getBookingStatusLabel,
|
|
getTicketCatalogItem
|
|
} from '~~/shared/booking'
|
|
|
|
import { getErrorMessage } from '../../utils/errors'
|
|
import { formatDateTime } from '../../utils/formatters'
|
|
|
|
const route = useRoute()
|
|
const toast = useToast()
|
|
const apiClient = useApiClient()
|
|
|
|
const token = String(route.params.token || '')
|
|
const confirming = ref(false)
|
|
|
|
let initialBooking: PublicBooking
|
|
|
|
try {
|
|
const response = await apiClient<{ booking: PublicBooking }>(`/api/public/bookings/${token}`)
|
|
initialBooking = response.booking
|
|
} catch (error: any) {
|
|
throw createError({
|
|
statusCode: error?.statusCode || error?.data?.statusCode || 404,
|
|
statusMessage: error?.data?.statusMessage || error?.message || 'Booking not found'
|
|
})
|
|
}
|
|
|
|
const booking = ref(initialBooking)
|
|
|
|
const statusColor = computed(() => booking.value.status === 'confirmed' ? 'success' : 'warning')
|
|
const ticketLabel = computed(() => getTicketCatalogItem(booking.value.ticketType)?.label || booking.value.ticketType.toUpperCase())
|
|
const totalFormatted = computed(() => formatBookingCurrency(booking.value.totalPrice))
|
|
const detailRows = computed(() => {
|
|
const rows = [
|
|
{
|
|
label: 'Guest / Organizer',
|
|
value: booking.value.customerName
|
|
},
|
|
{
|
|
label: 'Contact Number',
|
|
value: booking.value.customerPhone
|
|
},
|
|
{
|
|
label: 'Person In Charge',
|
|
value: booking.value.personInChargeName
|
|
},
|
|
{
|
|
label: 'PIC Phone',
|
|
value: booking.value.personInChargePhoneNumber
|
|
},
|
|
{
|
|
label: 'Booking Mode',
|
|
value: getBookingModeLabel(booking.value.bookingMode)
|
|
},
|
|
{
|
|
label: 'Ticket Category',
|
|
value: ticketLabel.value
|
|
},
|
|
{
|
|
label: 'Quantity',
|
|
value: String(booking.value.quantity)
|
|
},
|
|
{
|
|
label: 'Seats Covered',
|
|
value: String(booking.value.seatCount)
|
|
},
|
|
{
|
|
label: 'Submitted',
|
|
value: formatDateTime(booking.value.createdAt)
|
|
}
|
|
]
|
|
|
|
if (booking.value.confirmedAt) {
|
|
rows.push({
|
|
label: 'Confirmed At',
|
|
value: formatDateTime(booking.value.confirmedAt)
|
|
})
|
|
}
|
|
|
|
return rows
|
|
})
|
|
|
|
async function confirmBooking() {
|
|
if (booking.value.status === 'confirmed') {
|
|
return
|
|
}
|
|
|
|
confirming.value = true
|
|
|
|
try {
|
|
const response = await apiClient<{ booking: PublicBooking, alreadyConfirmed: boolean }>(
|
|
`/api/public/bookings/${token}/confirm`,
|
|
{
|
|
method: 'POST'
|
|
}
|
|
)
|
|
|
|
booking.value = response.booking
|
|
|
|
toast.add({
|
|
title: response.alreadyConfirmed ? 'Booking already confirmed' : 'Booking confirmed',
|
|
description: response.alreadyConfirmed
|
|
? 'This booking had already been confirmed earlier.'
|
|
: 'The booking details have been confirmed successfully.',
|
|
color: 'success',
|
|
icon: 'i-lucide-check-circle-2'
|
|
})
|
|
} catch (error) {
|
|
toast.add({
|
|
title: 'Confirmation failed',
|
|
description: getErrorMessage(error, 'Please try again in a moment.'),
|
|
color: 'error',
|
|
icon: 'i-lucide-circle-alert'
|
|
})
|
|
} finally {
|
|
confirming.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UContainer class="py-8">
|
|
<div class="mx-auto max-w-3xl space-y-5">
|
|
<div class="space-y-2 text-center">
|
|
<UBadge label="PIC Confirmation" color="primary" variant="soft" class="rounded-full" />
|
|
<h1 class="text-2xl font-bold tracking-tight text-highlighted sm:text-3xl">
|
|
Review Booking Details
|
|
</h1>
|
|
<p class="text-sm text-muted">
|
|
Confirm the booking after verifying the details below.
|
|
</p>
|
|
</div>
|
|
|
|
<UCard
|
|
class="border border-default bg-default shadow-sm"
|
|
:ui="{ body: 'space-y-4 p-4 sm:p-5' }"
|
|
>
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="space-y-1">
|
|
<p class="text-xs font-medium uppercase tracking-wide text-muted">
|
|
Booking status
|
|
</p>
|
|
<UBadge :label="getBookingStatusLabel(booking.status)" :color="statusColor" variant="soft" />
|
|
</div>
|
|
|
|
<div class="text-sm text-muted">
|
|
Submitted {{ formatDateTime(booking.createdAt) }}
|
|
</div>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-if="booking.status === 'confirmed'"
|
|
title="Booking already confirmed"
|
|
:description="`Confirmed on ${formatDateTime(booking.confirmedAt)}.`"
|
|
color="success"
|
|
icon="i-lucide-badge-check"
|
|
/>
|
|
|
|
<div class="overflow-hidden rounded-xl border border-default">
|
|
<div
|
|
v-for="row in detailRows"
|
|
:key="row.label"
|
|
class="grid grid-cols-[8.5rem_minmax(0,1fr)] gap-3 border-b border-default px-4 py-3 text-sm last:border-b-0 sm:grid-cols-[11rem_minmax(0,1fr)]"
|
|
>
|
|
<div class="text-xs font-medium uppercase tracking-wide text-muted sm:text-sm sm:normal-case sm:tracking-normal">
|
|
{{ row.label }}
|
|
</div>
|
|
<div class="min-w-0 font-medium text-highlighted break-words">
|
|
{{ row.value }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-[8.5rem_minmax(0,1fr)] gap-3 bg-primary/5 px-4 py-3 sm:grid-cols-[11rem_minmax(0,1fr)]">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-primary sm:text-sm sm:normal-case sm:tracking-normal">
|
|
Total Price
|
|
</div>
|
|
<div class="min-w-0 text-lg font-bold text-highlighted sm:text-xl">
|
|
{{ totalFormatted }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
|
|
<UButton
|
|
to="/"
|
|
label="Back To Booking Form"
|
|
color="neutral"
|
|
variant="ghost"
|
|
class="justify-center"
|
|
/>
|
|
|
|
<UButton
|
|
label="Confirm This Booking"
|
|
icon="i-lucide-check-check"
|
|
class="justify-center"
|
|
:disabled="booking.status === 'confirmed'"
|
|
:loading="confirming"
|
|
@click="confirmBooking"
|
|
/>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</UContainer>
|
|
</template>
|