Implement useLocale composable and shared translation dictionaries Translate public pages, booking flow, and receipt views Store booking locale to send localized WhatsApp notifications
223 lines
5.4 KiB
TypeScript
223 lines
5.4 KiB
TypeScript
import type { AppLocale } from './i18n'
|
|
|
|
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
|
|
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
|
|
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
|
|
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 function isBookingStatus(value: string | null | undefined): value is BookingStatus {
|
|
return value === 'pending' || value === 'confirmed'
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|