Add a remark column to the bookings table for management-only notes. Include UI to view and edit remarks directly from the bookings list. Create API endpoint and database queries to support remark updates.
206 lines
4.8 KiB
TypeScript
206 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
|
|
remark: string | null
|
|
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
|
|
}
|
|
}
|