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:
2026-05-05 07:04:42 +08:00
parent 4e40bfd804
commit 13e85cfcd0
5 changed files with 179 additions and 3 deletions

View File

@@ -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>