Add receipt tokens and booking_seats table to track individual tickets Create receipt and seat view pages with QR code generation
204 lines
5.6 KiB
TypeScript
204 lines
5.6 KiB
TypeScript
export type BookingMode = 'table' | 'pax'
|
|
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 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
|
|
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 {
|
|
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 getSeatLabel(seatNumber: number) {
|
|
return `Seat ${seatNumber}`
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|