Files
dticket.tootaio.com/shared/booking.ts
xiaomai 13e85cfcd0 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
2026-05-05 07:04:42 +08:00

211 lines
4.9 KiB
TypeScript

export type BookingMode = string
export type TicketType = string
export type BookingStatus = 'pending' | 'confirmed'
export interface DinnerEvent {
id: string
title: string
dateLabel: string
timeLabel: string
venue: string
}
export interface BookingModeOption {
id: string
value: BookingMode
label: string
quantityLabel: string
seatsPerUnit: number
sortOrder: number
}
export interface TicketCatalogItem {
id: string
value: TicketType
label: string
description: string
price: number
sortOrder: number
}
export interface PublicBookingConfig {
event: DinnerEvent
bookingModes: BookingModeOption[]
ticketCatalog: TicketCatalogItem[]
}
export interface PublicBooking {
id: string
confirmationToken: string
receiptToken: string
event: DinnerEvent
customerName: string
customerPhone: string
bookingModeId: string | null
bookingMode: BookingMode
bookingModeLabel: string
quantity: number
seatCount: number
ticketTypeId: string | null
ticketType: TicketType
ticketLabel: string
ticketDescription: string | null
unitPrice: number
totalPrice: number
personInChargeId: string
personInChargeName: string
personInChargePhoneNumber: string
remark: string | null
status: BookingStatus
statusLabel: string
createdAt: string
confirmedAt: string | null
}
export interface ReceiptBooking {
id: string
receiptToken: string
event: DinnerEvent
customerName: string
customerPhone: string
bookingModeId: string | null
bookingMode: BookingMode
bookingModeLabel: string
quantity: number
seatCount: number
ticketTypeId: string | null
ticketType: TicketType
ticketLabel: string
ticketDescription: string | null
unitPrice: number
totalPrice: number
status: BookingStatus
statusLabel: string
createdAt: string
confirmedAt: string | null
}
export interface PublicBookingSeat {
id: string
seatNumber: number
seatToken: string
recipientName: string | null
recipientPhone: string | null
sharedAt: string | null
createdAt: string
updatedAt: string
}
export interface PublicBookingSeatWithUrl extends PublicBookingSeat {
seatUrl: string
}
export interface PublicBookingReceipt {
booking: ReceiptBooking
receiptUrl: string
seats: PublicBookingSeatWithUrl[]
}
export interface PublicSeatReceipt {
booking: ReceiptBooking
seat: PublicBookingSeatWithUrl
receiptUrl: string
}
export interface BookingCapacitySettings {
totalSeats: number | null
updatedAt: string | null
}
export interface BookingInventorySummary {
totalSeats: number | null
soldSeats: number
pendingSeats: number
leftSeats: number | null
}
export interface CreateBookingResponse {
booking: PublicBooking
confirmationUrl: string
whatsappUrl: string
}
export interface WhatsAppDeliveryResult {
sent: boolean
skipped: boolean
recipientPhone: string
apiRecipientPhone: string
messageId?: string
error?: string
}
export interface ConfirmBookingResponse {
booking: PublicBooking
alreadyConfirmed: boolean
ticketReceiptWhatsApp: WhatsAppDeliveryResult
}
export interface CancelBookingConfirmationResponse {
booking: PublicBooking
alreadyPending: boolean
}
export function isBookingStatus(value: string | null | undefined): value is BookingStatus {
return value === 'pending' || value === 'confirmed'
}
export function getBookingStatusLabel(value: BookingStatus | string, label?: string | null) {
if (label) {
return label
}
return value === 'confirmed' ? 'Confirmed' : 'Pending PIC confirmation'
}
export function getSeatCount(bookingMode: Pick<BookingModeOption, 'seatsPerUnit'> | null | undefined, quantity: number) {
return quantity * (bookingMode?.seatsPerUnit ?? 1)
}
export function getTicketLabel(ticket: Pick<TicketCatalogItem, 'label'> | null | undefined, ticketType: TicketType) {
return ticket?.label || ticketType.toUpperCase()
}
export function getBookingTicketLabel(booking: Pick<PublicBooking | ReceiptBooking, 'ticketLabel' | 'ticketType'>) {
return booking.ticketLabel || booking.ticketType.toUpperCase()
}
export function formatBookingCurrency(value: number) {
return new Intl.NumberFormat('en-MY', {
style: 'currency',
currency: 'MYR',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value)
}
export function getSeatLabel(seatNumber: number) {
return `Seat ${seatNumber}`
}
export function calculateBookingInventorySummary(
bookings: Pick<PublicBooking, 'seatCount' | 'status'>[],
settings: BookingCapacitySettings
): BookingInventorySummary {
const soldSeats = bookings
.filter((booking) => booking.status === 'confirmed')
.reduce((total, booking) => total + booking.seatCount, 0)
const pendingSeats = bookings
.filter((booking) => booking.status === 'pending')
.reduce((total, booking) => total + booking.seatCount, 0)
const leftSeats = settings.totalSeats === null ? null : Math.max(settings.totalSeats - soldSeats, 0)
return {
totalSeats: settings.totalSeats,
soldSeats,
pendingSeats,
leftSeats
}
}