Replace hardcoded event details and ticket types with dynamic DB records Add booking-config API endpoint to serve active event settings
205 lines
4.8 KiB
TypeScript
205 lines
4.8 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
|
|
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 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
|
|
}
|
|
}
|