Add payment method selection (Cash/Bank) to booking details Support uploading, downloading, and deleting transaction documents Update database schema and API endpoints to handle file storage
246 lines
5.9 KiB
TypeScript
246 lines
5.9 KiB
TypeScript
import type { AppLocale } from './i18n'
|
|
|
|
export type BookingMode = string
|
|
export type TicketType = string
|
|
export type BookingStatus = 'pending' | 'confirmed'
|
|
export type PaymentMethod = 'cash' | 'bank'
|
|
|
|
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
|
|
locale: AppLocale
|
|
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
|
|
paymentMethod: PaymentMethod
|
|
transactionDocument: BookingTransactionDocument | null
|
|
remark: string | null
|
|
status: BookingStatus
|
|
statusLabel: string
|
|
createdAt: string
|
|
confirmedAt: string | null
|
|
}
|
|
|
|
export interface BookingTransactionDocument {
|
|
originalName: string
|
|
mimeType: string
|
|
size: number
|
|
uploadedAt: string
|
|
url: string
|
|
}
|
|
|
|
export interface ReceiptBooking {
|
|
id: string
|
|
receiptToken: string
|
|
event: DinnerEvent
|
|
customerName: string
|
|
customerPhone: string
|
|
locale: AppLocale
|
|
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 interface CancelBookingConfirmationResponse {
|
|
booking: PublicBooking
|
|
alreadyPending: boolean
|
|
}
|
|
|
|
export interface TransferBookingPicResponse {
|
|
booking: PublicBooking
|
|
}
|
|
|
|
export interface UpdateBookingDetailsResponse {
|
|
booking: PublicBooking
|
|
}
|
|
|
|
export interface DeleteBookingResponse {
|
|
booking: PublicBooking
|
|
}
|
|
|
|
export function isBookingStatus(value: string | null | undefined): value is BookingStatus {
|
|
return value === 'pending' || value === 'confirmed'
|
|
}
|
|
|
|
export function isPaymentMethod(value: string | null | undefined): value is PaymentMethod {
|
|
return value === 'cash' || value === 'bank'
|
|
}
|
|
|
|
export function getBookingStatusLabel(value: BookingStatus | string, label?: string | null, locale: AppLocale = 'en') {
|
|
if (label && locale !== 'zh') {
|
|
return label
|
|
}
|
|
|
|
if (locale === 'zh') {
|
|
return value === 'confirmed' ? '已确认' : '等待负责人确认'
|
|
}
|
|
|
|
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, locale: AppLocale = 'en') {
|
|
return new Intl.NumberFormat(locale === 'zh' ? 'zh-MY' : 'en-MY', {
|
|
style: 'currency',
|
|
currency: 'MYR',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(value)
|
|
}
|
|
|
|
export function getSeatLabel(seatNumber: number, locale: AppLocale = 'en') {
|
|
return locale === 'zh' ? `座位 ${seatNumber}` : `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
|
|
}
|
|
}
|