- Booking Mode
+ Total Seats
- {{ getBookingModeLabel(receipt.booking.bookingMode) }}
+ {{ receipt.booking.seatCount }} seats
diff --git a/server/api/bookings/capacity.patch.ts b/server/api/bookings/capacity.patch.ts
index 78314fe..495328c 100644
--- a/server/api/bookings/capacity.patch.ts
+++ b/server/api/bookings/capacity.patch.ts
@@ -7,15 +7,15 @@ export default defineEventHandler(async (event) => {
await requireRole(event, 'super_admin')
const body = await readBody<{
- totalTables?: number | string | null
+ totalSeats?: number | string | null
}>(event)
const input = parseBookingCapacityInput(body)
const summary = await getBookingInventorySummary()
assertBadRequest(
- input.totalTables === null || (input.totalTables * 10) >= summary.soldCapacitySeats,
- `Total tables cannot be lower than the currently sold capacity of ${summary.soldTables} tables and ${summary.soldSeats} seats`
+ input.totalSeats === null || input.totalSeats >= summary.soldSeats,
+ `Total seats cannot be lower than the currently sold count of ${summary.soldSeats} seats`
)
const settings = await updateBookingCapacitySettings(input)
diff --git a/server/api/public/bookings.post.ts b/server/api/public/bookings.post.ts
index d4a80c8..fa59a62 100644
--- a/server/api/public/bookings.post.ts
+++ b/server/api/public/bookings.post.ts
@@ -12,7 +12,7 @@ export default defineEventHandler(async (event): Promise
const body = await readBody<{
customerName?: string
customerPhone?: string
- bookingMode?: BookingMode
+ bookingMode?: BookingMode | string | null
quantity?: number
ticketType?: TicketType
personInChargeId?: string
diff --git a/server/api/public/bookings/[token]/confirm.post.ts b/server/api/public/bookings/[token]/confirm.post.ts
index 84ec526..02a1467 100644
--- a/server/api/public/bookings/[token]/confirm.post.ts
+++ b/server/api/public/bookings/[token]/confirm.post.ts
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
const summary = await getBookingInventorySummary()
- if (summary.leftCapacitySeats !== null && existingBooking.seatCount > summary.leftCapacitySeats) {
+ if (summary.leftSeats !== null && existingBooking.seatCount > summary.leftSeats) {
httpError(409, 'Not enough capacity left to confirm this booking')
}
diff --git a/server/utils/booking-repository.ts b/server/utils/booking-repository.ts
index 7f31ea2..b756627 100644
--- a/server/utils/booking-repository.ts
+++ b/server/utils/booking-repository.ts
@@ -11,7 +11,7 @@ import type {
TicketType
} from '~~/shared/booking'
-import { calculateBookingInventorySummary, isBookingStatus } from '~~/shared/booking'
+import { calculateBookingInventorySummary, isBookingMode, isBookingStatus } from '~~/shared/booking'
import { randomToken, toIsoString } from './base64url'
import { ensureDatabaseReady } from './db-init'
@@ -23,7 +23,7 @@ type DbBookingRow = {
receipt_token: string
customer_name: string
customer_phone: string
- booking_mode: BookingMode
+ booking_mode: string
quantity: number | string
seat_count: number | string
ticket_type: TicketType
@@ -54,7 +54,7 @@ type DbBookingSeatWithBookingRow = DbBookingSeatRow & {
receipt_token: string
customer_name: string
customer_phone: string
- booking_mode: BookingMode
+ booking_mode: string
quantity: number | string
seat_count: number | string
ticket_type: TicketType
@@ -67,6 +67,7 @@ type DbBookingSeatWithBookingRow = DbBookingSeatRow & {
type DbBookingSettingsRow = {
total_tables: number | string | null
+ total_seats: number | string | null
updated_at: Date | string
}
@@ -74,16 +75,22 @@ function parseInteger(value: number | string) {
return typeof value === 'number' ? value : Number.parseInt(value, 10)
}
+function normalizeBookingMode(value: string): BookingMode {
+ return isBookingMode(value) ? value : 'seat'
+}
+
function mapBooking(row: DbBookingRow): PublicBooking {
+ const seatCount = parseInteger(row.seat_count)
+
return {
id: row.id,
confirmationToken: row.confirmation_token,
receiptToken: row.receipt_token,
customerName: row.customer_name,
customerPhone: row.customer_phone,
- bookingMode: row.booking_mode,
+ bookingMode: normalizeBookingMode(row.booking_mode),
quantity: parseInteger(row.quantity),
- seatCount: parseInteger(row.seat_count),
+ seatCount,
ticketType: row.ticket_type,
unitPrice: parseInteger(row.unit_price),
totalPrice: parseInteger(row.total_price),
@@ -97,14 +104,16 @@ function mapBooking(row: DbBookingRow): PublicBooking {
}
function mapReceiptBooking(row: DbBookingRow | DbBookingSeatWithBookingRow): ReceiptBooking {
+ const seatCount = parseInteger(row.seat_count)
+
return {
id: row.id,
receiptToken: row.receipt_token,
customerName: row.customer_name,
customerPhone: row.customer_phone,
- bookingMode: row.booking_mode,
+ bookingMode: normalizeBookingMode(row.booking_mode),
quantity: parseInteger(row.quantity),
- seatCount: parseInteger(row.seat_count),
+ seatCount,
ticketType: row.ticket_type,
unitPrice: parseInteger(row.unit_price),
totalPrice: parseInteger(row.total_price),
@@ -130,13 +139,17 @@ function mapBookingSeat(row: DbBookingSeatRow): PublicBookingSeat {
function mapBookingCapacitySettings(row: DbBookingSettingsRow | undefined): BookingCapacitySettings {
if (!row) {
return {
- totalTables: null,
+ totalSeats: null,
updatedAt: null
}
}
+ const totalSeats = row.total_seats === null
+ ? (row.total_tables === null ? null : parseInteger(row.total_tables) * 10)
+ : parseInteger(row.total_seats)
+
return {
- totalTables: row.total_tables === null ? null : parseInteger(row.total_tables),
+ totalSeats,
updatedAt: toIsoString(row.updated_at)
}
}
@@ -521,6 +534,7 @@ export async function getBookingCapacitySettings(): Promise`
select
total_tables,
+ total_seats,
updated_at
from booking_settings
where id = 'default'
@@ -531,7 +545,7 @@ export async function getBookingCapacitySettings(): Promise {
await ensureDatabaseReady()
const sql = getSqlClient()
@@ -539,11 +553,12 @@ export async function updateBookingCapacitySettings(input: {
const [row] = await sql`
update booking_settings
set
- total_tables = ${input.totalTables},
+ total_seats = ${input.totalSeats},
updated_at = now()
where id = 'default'
returning
total_tables,
+ total_seats,
updated_at
`
diff --git a/server/utils/bookings.ts b/server/utils/bookings.ts
index f831663..49cd1fb 100644
--- a/server/utils/bookings.ts
+++ b/server/utils/bookings.ts
@@ -2,7 +2,6 @@ import type { BookingCapacitySettings, BookingMode, PublicBooking, TicketType }
import {
formatBookingCurrency,
- getBookingModeLabel,
getTicketCatalogItem,
isBookingMode,
isTicketType
@@ -14,14 +13,14 @@ import { assertBadRequest } from './http'
export function parseCreateBookingInput(body: {
customerName?: string
customerPhone?: string
- bookingMode?: BookingMode
+ bookingMode?: BookingMode | string | null
quantity?: number
ticketType?: TicketType
personInChargeId?: string
}) {
const customerName = normalizeFullName(body.customerName || '')
const customerPhone = normalizePhoneNumber(body.customerPhone || '')
- const bookingMode = body.bookingMode
+ const bookingMode = typeof body.bookingMode === 'string' ? body.bookingMode.trim().toLowerCase() : body.bookingMode
const ticketType = body.ticketType
const quantity = Number(body.quantity)
const personInChargeId = (body.personInChargeId || '').trim()
@@ -52,10 +51,8 @@ export function buildBookingMessage(booking: PublicBooking, confirmationUrl: str
'',
`Name: ${booking.customerName}`,
`Phone Number: ${booking.customerPhone}`,
- `Booking Mode: ${getBookingModeLabel(booking.bookingMode)}`,
- `Quantity: ${booking.quantity}`,
+ `Seats: ${booking.seatCount}`,
`Ticket Category: ${ticketLabel}`,
- `Seats Covered: ${booking.seatCount}`,
`Total Price: ${formatBookingCurrency(booking.totalPrice)}`,
'',
'PIC confirmation link:',
@@ -87,14 +84,14 @@ export function parseSeatShareInput(body: {
}
export function parseBookingCapacityInput(body: {
- totalTables?: number | string | null
-}): Pick {
- const totalTables = parseOptionalInteger(body.totalTables)
+ totalSeats?: number | string | null
+}): Pick {
+ const totalSeats = parseOptionalInteger(body.totalSeats)
- assertBadRequest(totalTables === null || totalTables >= 0, 'Total tables must be 0 or greater')
+ assertBadRequest(totalSeats === null || totalSeats >= 0, 'Total seats must be 0 or greater')
return {
- totalTables
+ totalSeats
}
}
diff --git a/server/utils/db-init.ts b/server/utils/db-init.ts
index dcabbf3..208e658 100644
--- a/server/utils/db-init.ts
+++ b/server/utils/db-init.ts
@@ -69,7 +69,7 @@ async function initializeDatabase() {
receipt_token text not null unique,
customer_name text not null,
customer_phone text not null,
- booking_mode text not null check (booking_mode in ('table', 'pax')),
+ booking_mode text not null check (booking_mode in ('table', 'seat')),
quantity integer not null check (quantity >= 1),
seat_count integer not null check (seat_count >= 1),
ticket_type text not null check (ticket_type in ('vip', 'supporter')),
@@ -124,6 +124,11 @@ async function initializeDatabase() {
)
`
+ await sql`
+ alter table booking_settings
+ add column if not exists total_seats integer
+ `
+
await sql`
insert into booking_settings (id)
values ('default')
@@ -146,6 +151,45 @@ async function initializeDatabase() {
`
}
+ await sql`
+ alter table bookings
+ drop constraint if exists bookings_booking_mode_check
+ `
+
+ await sql`
+ alter table bookings
+ add constraint bookings_booking_mode_check
+ check (booking_mode in ('table', 'pax', 'seat'))
+ `
+
+ await sql`
+ update bookings
+ set
+ booking_mode = 'seat',
+ updated_at = now()
+ where booking_mode = 'pax'
+ `
+
+ await sql`
+ alter table bookings
+ drop constraint if exists bookings_booking_mode_check
+ `
+
+ await sql`
+ alter table bookings
+ add constraint bookings_booking_mode_check
+ check (booking_mode in ('table', 'seat'))
+ `
+
+ await sql`
+ update booking_settings
+ set
+ total_seats = total_tables * 10,
+ updated_at = now()
+ where total_seats is null
+ and total_tables is not null
+ `
+
const existingBookings = await sql<{ id: string, seat_count: number | string }[]>`
select
id,
diff --git a/shared/booking.ts b/shared/booking.ts
index 5e7de1b..ca68849 100644
--- a/shared/booking.ts
+++ b/shared/booking.ts
@@ -1,4 +1,4 @@
-export type BookingMode = 'table' | 'pax'
+export type BookingMode = 'table' | 'seat'
export type TicketType = 'vip' | 'supporter'
export type BookingStatus = 'pending' | 'confirmed'
@@ -7,14 +7,16 @@ 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 TABLE_SEAT_COUNT = 10
+
export const BOOKING_MODE_OPTIONS = [
{
value: 'table',
- label: 'Table (10 pax)'
+ label: `Table (${TABLE_SEAT_COUNT} seats)`
},
{
- value: 'pax',
- label: 'Person'
+ value: 'seat',
+ label: 'Seat'
}
] satisfies Array<{ value: BookingMode, label: string }>
@@ -22,13 +24,13 @@ export const BOOKING_TICKET_CATALOG = [
{
value: 'vip',
label: 'VIP',
- description: 'RM150 / pax',
+ description: 'RM150 / seat',
price: 150
},
{
value: 'supporter',
label: 'Supporter',
- description: 'RM60 / pax',
+ description: 'RM60 / seat',
price: 60
}
] satisfies Array<{ value: TicketType, label: string, description: string, price: number }>
@@ -97,22 +99,15 @@ export interface PublicSeatReceipt {
}
export interface BookingCapacitySettings {
- totalTables: number | null
+ totalSeats: number | null
updatedAt: string | null
}
export interface BookingInventorySummary {
- totalTables: number | null
- totalCapacitySeats: number | null
- soldTables: number
- pendingTables: number
+ totalSeats: number | null
soldSeats: number
pendingSeats: number
- soldCapacitySeats: number
- pendingCapacitySeats: number
- leftTables: number | null
leftSeats: number | null
- leftCapacitySeats: number | null
}
export interface CreateBookingResponse {
@@ -122,7 +117,7 @@ export interface CreateBookingResponse {
}
export function isBookingMode(value: string | null | undefined): value is BookingMode {
- return value === 'table' || value === 'pax'
+ return value === 'table' || value === 'seat'
}
export function isTicketType(value: string | null | undefined): value is TicketType {
@@ -134,7 +129,7 @@ export function isBookingStatus(value: string | null | undefined): value is Book
}
export function getBookingModeLabel(value: BookingMode) {
- return value === 'table' ? 'Table (10 pax each)' : 'Per person'
+ return value === 'table' ? `Table (${TABLE_SEAT_COUNT} seats each)` : 'Per seat'
}
export function getBookingStatusLabel(value: BookingStatus) {
@@ -142,7 +137,7 @@ export function getBookingStatusLabel(value: BookingStatus) {
}
export function getSeatCount(bookingMode: BookingMode, quantity: number) {
- return bookingMode === 'table' ? quantity * 10 : quantity
+ return bookingMode === 'table' ? quantity * TABLE_SEAT_COUNT : quantity
}
export function getTicketCatalogItem(ticketType: TicketType) {
@@ -163,41 +158,23 @@ export function getSeatLabel(seatNumber: number) {
}
export function calculateBookingInventorySummary(
- bookings: Pick[],
+ bookings: Pick[],
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')
+ .filter((booking) => booking.status === 'confirmed')
.reduce((total, booking) => total + booking.seatCount, 0)
const pendingSeats = bookings
- .filter((booking) => booking.status === 'pending' && booking.bookingMode === 'pax')
+ .filter((booking) => booking.status === 'pending')
.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)
+ const leftSeats = settings.totalSeats === null ? null : Math.max(settings.totalSeats - soldSeats, 0)
return {
- totalTables: settings.totalTables,
- totalCapacitySeats,
- soldTables,
- pendingTables,
+ totalSeats: settings.totalSeats,
soldSeats,
pendingSeats,
- soldCapacitySeats,
- pendingCapacitySeats,
- leftTables: leftCapacitySeats === null ? null : Math.floor(leftCapacitySeats / 10),
- leftSeats: leftCapacitySeats === null ? null : leftCapacitySeats % 10,
- leftCapacitySeats
+ leftSeats
}
}