Files
dticket.tootaio.com/shared/booking.ts
xiaomai faa998c7e1 refactor(bookings): simplify capacity tracking to use seats
Replace 'pax' booking mode with 'seat'
Consolidate inventory summary to track only seat counts
Update database schema and UI for seat-centric capacity
2026-04-13 08:49:54 +08:00

181 lines
4.5 KiB
TypeScript

export type BookingMode = 'table' | 'seat'
export type TicketType = 'vip' | 'supporter'
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 const TABLE_SEAT_COUNT = 10
export const BOOKING_MODE_OPTIONS = [
{
value: 'table',
label: `Table (${TABLE_SEAT_COUNT} seats)`
},
{
value: 'seat',
label: 'Seat'
}
] satisfies Array<{ value: BookingMode, label: string }>
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 PublicBooking {
id: string
confirmationToken: string
receiptToken: string
customerName: string
customerPhone: string
bookingMode: BookingMode
quantity: number
seatCount: number
ticketType: TicketType
unitPrice: number
totalPrice: number
personInChargeId: string
personInChargeName: string
personInChargePhoneNumber: string
status: BookingStatus
createdAt: string
confirmedAt: string | null
}
export interface ReceiptBooking {
id: string
receiptToken: string
customerName: string
customerPhone: string
bookingMode: BookingMode
quantity: number
seatCount: number
ticketType: TicketType
unitPrice: number
totalPrice: number
status: BookingStatus
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 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) {
return value === 'confirmed' ? 'Confirmed' : 'Pending PIC confirmation'
}
export function getSeatCount(bookingMode: BookingMode, quantity: number) {
return bookingMode === 'table' ? quantity * TABLE_SEAT_COUNT : quantity
}
export function getTicketCatalogItem(ticketType: TicketType) {
return BOOKING_TICKET_CATALOG.find((ticket) => ticket.value === ticketType) ?? null
}
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
}
}