Files
dticket.tootaio.com/shared/booking.ts
xiaomai 8541c4a2d1 feat(bookings): implement booking system and confirmation flow
Add database tables and repository for managing bookings
Create API endpoints for booking submission and capacity management
Update landing page to persist bookings before WhatsApp redirection
2026-04-12 21:43:30 +08:00

151 lines
4.4 KiB
TypeScript

export type BookingMode = 'table' | 'pax'
export type TicketType = 'vip' | 'supporter'
export type BookingStatus = 'pending' | 'confirmed'
export const BOOKING_MODE_OPTIONS = [
{
value: 'table',
label: 'Table (10 pax)'
},
{
value: 'pax',
label: 'Person'
}
] satisfies Array<{ value: BookingMode, label: string }>
export const BOOKING_TICKET_CATALOG = [
{
value: 'vip',
label: 'VIP',
description: 'RM150 / pax',
price: 150
},
{
value: 'supporter',
label: 'Supporter',
description: 'RM60 / pax',
price: 60
}
] satisfies Array<{ value: TicketType, label: string, description: string, price: number }>
export interface PublicBooking {
id: string
confirmationToken: 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 BookingCapacitySettings {
totalTables: number | null
updatedAt: string | null
}
export interface BookingInventorySummary {
totalTables: number | null
totalCapacitySeats: number | null
soldTables: number
pendingTables: number
soldSeats: number
pendingSeats: number
soldCapacitySeats: number
pendingCapacitySeats: number
leftTables: number | null
leftSeats: number | null
leftCapacitySeats: 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 === 'pax'
}
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 (10 pax each)' : 'Per person'
}
export function getBookingStatusLabel(value: BookingStatus) {
return value === 'confirmed' ? 'Confirmed' : 'Pending PIC confirmation'
}
export function getSeatCount(bookingMode: BookingMode, quantity: number) {
return bookingMode === 'table' ? quantity * 10 : 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 calculateBookingInventorySummary(
bookings: Pick<PublicBooking, 'bookingMode' | 'quantity' | 'seatCount' | 'status'>[],
settings: BookingCapacitySettings
): BookingInventorySummary {
const soldTables = bookings
.filter((booking) => booking.status === 'confirmed' && booking.bookingMode === 'table')
.reduce((total, booking) => total + booking.quantity, 0)
const pendingTables = bookings
.filter((booking) => booking.status === 'pending' && booking.bookingMode === 'table')
.reduce((total, booking) => total + booking.quantity, 0)
const soldSeats = bookings
.filter((booking) => booking.status === 'confirmed' && booking.bookingMode === 'pax')
.reduce((total, booking) => total + booking.seatCount, 0)
const pendingSeats = bookings
.filter((booking) => booking.status === 'pending' && booking.bookingMode === 'pax')
.reduce((total, booking) => total + booking.seatCount, 0)
const totalCapacitySeats = settings.totalTables === null ? null : settings.totalTables * 10
const soldCapacitySeats = (soldTables * 10) + soldSeats
const pendingCapacitySeats = (pendingTables * 10) + pendingSeats
const leftCapacitySeats = totalCapacitySeats === null ? null : Math.max(totalCapacitySeats - soldCapacitySeats, 0)
return {
totalTables: settings.totalTables,
totalCapacitySeats,
soldTables,
pendingTables,
soldSeats,
pendingSeats,
soldCapacitySeats,
pendingCapacitySeats,
leftTables: leftCapacitySeats === null ? null : Math.floor(leftCapacitySeats / 10),
leftSeats: leftCapacitySeats === null ? null : leftCapacitySeats % 10,
leftCapacitySeats
}
}