feat(bookings): allow cancelling booking confirmations
Add API endpoint to revert confirmed bookings to pending status Add unconfirm buttons to the bookings list and confirmation page Update inventory summary when a confirmation is cancelled
This commit is contained in:
@@ -302,6 +302,17 @@
|
||||
icon="i-lucide-receipt"
|
||||
size="sm"
|
||||
/>
|
||||
<UButton
|
||||
v-if="row.original.status === 'confirmed'"
|
||||
label="Unconfirm"
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-x-circle"
|
||||
size="sm"
|
||||
:loading="cancellingBookingId === row.original.id"
|
||||
:disabled="cancellingBookingId !== null && cancellingBookingId !== row.original.id"
|
||||
@click="cancelBookingConfirmation(row.original)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
@@ -365,7 +376,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { BookingCapacitySettings, BookingInventorySummary, PublicBooking } from '~~/shared/booking'
|
||||
import type { BookingCapacitySettings, BookingInventorySummary, CancelBookingConfirmationResponse, PublicBooking } from '~~/shared/booking'
|
||||
|
||||
import {
|
||||
formatBookingCurrency,
|
||||
@@ -387,6 +398,7 @@ const bookings = ref<PublicBooking[]>([])
|
||||
const loadingBookings = ref(false)
|
||||
const savingCapacity = ref(false)
|
||||
const savingRemark = ref(false)
|
||||
const cancellingBookingId = ref<string | null>(null)
|
||||
const remarkModalOpen = ref(false)
|
||||
const editingBooking = ref<PublicBooking | null>(null)
|
||||
const searchQuery = ref('')
|
||||
@@ -528,6 +540,12 @@ function replaceBooking(updatedBooking: PublicBooking) {
|
||||
bookings.value.splice(index, 1, updatedBooking)
|
||||
}
|
||||
|
||||
function applyCancelledConfirmationToSummary(booking: PublicBooking) {
|
||||
summary.soldSeats = Math.max(summary.soldSeats - booking.seatCount, 0)
|
||||
summary.pendingSeats += booking.seatCount
|
||||
summary.leftSeats = summary.totalSeats === null ? null : Math.max(summary.totalSeats - summary.soldSeats, 0)
|
||||
}
|
||||
|
||||
function openRemarkEditor(booking: PublicBooking) {
|
||||
editingBooking.value = booking
|
||||
remarkForm.remark = booking.remark || ''
|
||||
@@ -650,4 +668,46 @@ async function saveRemark() {
|
||||
savingRemark.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelBookingConfirmation(booking: PublicBooking) {
|
||||
if (booking.status !== 'confirmed' || cancellingBookingId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (import.meta.client && !window.confirm(`Cancel confirmation for ${booking.customerName}? The booking will return to pending and the seats will be released.`)) {
|
||||
return
|
||||
}
|
||||
|
||||
cancellingBookingId.value = booking.id
|
||||
|
||||
try {
|
||||
const response = await apiClient<CancelBookingConfirmationResponse>(`/api/public/bookings/${booking.confirmationToken}/cancel`, {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
replaceBooking(response.booking)
|
||||
|
||||
if (!response.alreadyPending) {
|
||||
applyCancelledConfirmationToSummary(booking)
|
||||
}
|
||||
|
||||
toast.add({
|
||||
title: response.alreadyPending ? 'Booking already pending' : 'Confirmation cancelled',
|
||||
description: response.alreadyPending
|
||||
? `${booking.customerName} was already pending confirmation.`
|
||||
: `${booking.customerName} has been returned to pending status.`,
|
||||
color: response.alreadyPending ? 'warning' : 'success',
|
||||
icon: 'i-lucide-x-circle'
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast.add({
|
||||
title: 'Cancellation failed',
|
||||
description: getErrorMessage(error, 'Unable to cancel the booking confirmation.'),
|
||||
color: 'error',
|
||||
icon: 'i-lucide-circle-alert'
|
||||
})
|
||||
} finally {
|
||||
cancellingBookingId.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ConfirmBookingResponse, PublicBooking } from '~~/shared/booking'
|
||||
import type { CancelBookingConfirmationResponse, ConfirmBookingResponse, PublicBooking } from '~~/shared/booking'
|
||||
|
||||
import {
|
||||
formatBookingCurrency,
|
||||
@@ -15,6 +15,7 @@ const apiClient = useApiClient()
|
||||
|
||||
const token = String(route.params.token || '')
|
||||
const confirming = ref(false)
|
||||
const cancelling = ref(false)
|
||||
|
||||
let initialBooking: PublicBooking
|
||||
|
||||
@@ -114,6 +115,47 @@ async function confirmBooking() {
|
||||
confirming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelBookingConfirmation() {
|
||||
if (booking.value.status !== 'confirmed') {
|
||||
return
|
||||
}
|
||||
|
||||
if (import.meta.client && !window.confirm('Cancel this confirmation? The booking will return to pending and the seats will be released.')) {
|
||||
return
|
||||
}
|
||||
|
||||
cancelling.value = true
|
||||
|
||||
try {
|
||||
const response = await apiClient<CancelBookingConfirmationResponse>(
|
||||
`/api/public/bookings/${token}/cancel`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
|
||||
booking.value = response.booking
|
||||
|
||||
toast.add({
|
||||
title: response.alreadyPending ? 'Booking already pending' : 'Confirmation cancelled',
|
||||
description: response.alreadyPending
|
||||
? 'This booking was already pending confirmation.'
|
||||
: 'The booking has been returned to pending status.',
|
||||
color: response.alreadyPending ? 'warning' : 'success',
|
||||
icon: 'i-lucide-x-circle'
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: 'Cancellation failed',
|
||||
description: getErrorMessage(error, 'Please try again in a moment.'),
|
||||
color: 'error',
|
||||
icon: 'i-lucide-circle-alert'
|
||||
})
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -198,13 +240,24 @@ async function confirmBooking() {
|
||||
/>
|
||||
|
||||
<UButton
|
||||
v-if="booking.status === 'pending'"
|
||||
label="Confirm This Booking"
|
||||
icon="i-lucide-check-check"
|
||||
class="justify-center"
|
||||
:disabled="booking.status === 'confirmed'"
|
||||
:loading="confirming"
|
||||
@click="confirmBooking"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
v-else
|
||||
label="Cancel Confirmation"
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-x-circle"
|
||||
class="justify-center"
|
||||
:loading="cancelling"
|
||||
@click="cancelBookingConfirmation"
|
||||
/>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user