feat(booking): move event and ticket configuration to database

Replace hardcoded event details and ticket types with dynamic DB records
Add booking-config API endpoint to serve active event settings
This commit is contained in:
2026-05-04 10:09:08 +08:00
parent 06165f80db
commit 3f7025c8e4
13 changed files with 970 additions and 342 deletions

View File

@@ -1,56 +1,62 @@
export type BookingMode = 'table' | 'seat'
export type TicketType = 'vip' | 'supporter'
export type BookingMode = string
export type TicketType = string
export type BookingStatus = 'pending' | 'confirmed'
export const DINNER_EVENT_TITLE = 'DAP JOHOR 60th Anniversary Celebration'
export const DINNER_EVENT_DATE_LABEL = 'Saturday, 30 May 2026'
export const DINNER_EVENT_TIME_LABEL = '6:30 PM'
export const DINNER_EVENT_VENUE = "Yong Peng's Chee Ann Kor"
export interface DinnerEvent {
id: string
title: string
dateLabel: string
timeLabel: string
venue: string
}
export const TABLE_SEAT_COUNT = 10
export interface BookingModeOption {
id: string
value: BookingMode
label: string
quantityLabel: string
seatsPerUnit: number
sortOrder: number
}
export const BOOKING_MODE_OPTIONS = [
{
value: 'table',
label: `Table (${TABLE_SEAT_COUNT} seats)`
},
{
value: 'seat',
label: 'Seat'
}
] satisfies Array<{ value: BookingMode, label: string }>
export interface TicketCatalogItem {
id: string
value: TicketType
label: string
description: string
price: number
sortOrder: number
}
export const BOOKING_TICKET_CATALOG = [
{
value: 'vip',
label: 'VIP',
description: 'RM150 / seat',
price: 150
},
{
value: 'supporter',
label: 'Supporter',
description: 'RM60 / seat',
price: 60
}
] satisfies Array<{ value: TicketType, label: string, description: string, price: 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
}
@@ -58,15 +64,22 @@ export interface PublicBooking {
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
}
@@ -131,32 +144,28 @@ export interface ConfirmBookingResponse {
ticketReceiptWhatsApp: WhatsAppDeliveryResult
}
export function isBookingMode(value: string | null | undefined): value is BookingMode {
return value === 'table' || value === 'seat'
}
export function isTicketType(value: string | null | undefined): value is TicketType {
return value === 'vip' || value === 'supporter'
}
export function isBookingStatus(value: string | null | undefined): value is BookingStatus {
return value === 'pending' || value === 'confirmed'
}
export function getBookingModeLabel(value: BookingMode) {
return value === 'table' ? `Table (${TABLE_SEAT_COUNT} seats each)` : 'Per seat'
}
export function getBookingStatusLabel(value: BookingStatus | string, label?: string | null) {
if (label) {
return label
}
export function getBookingStatusLabel(value: BookingStatus) {
return value === 'confirmed' ? 'Confirmed' : 'Pending PIC confirmation'
}
export function getSeatCount(bookingMode: BookingMode, quantity: number) {
return bookingMode === 'table' ? quantity * TABLE_SEAT_COUNT : quantity
export function getSeatCount(bookingMode: Pick<BookingModeOption, 'seatsPerUnit'> | null | undefined, quantity: number) {
return quantity * (bookingMode?.seatsPerUnit ?? 1)
}
export function getTicketCatalogItem(ticketType: TicketType) {
return BOOKING_TICKET_CATALOG.find((ticket) => ticket.value === ticketType) ?? null
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) {