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.
862 lines
23 KiB
TypeScript
862 lines
23 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
|
|
import type {
|
|
BookingModeOption,
|
|
BookingCapacitySettings,
|
|
BookingInventorySummary,
|
|
DinnerEvent,
|
|
BookingMode,
|
|
BookingStatus,
|
|
PublicBookingConfig,
|
|
PublicBooking,
|
|
PublicBookingSeat,
|
|
ReceiptBooking,
|
|
TicketCatalogItem,
|
|
TicketType
|
|
} from '~~/shared/booking'
|
|
|
|
import { calculateBookingInventorySummary, getBookingStatusLabel, isBookingStatus } from '~~/shared/booking'
|
|
|
|
import { randomToken, toIsoString } from './base64url'
|
|
import { ensureDatabaseReady } from './db-init'
|
|
import { getSqlClient } from './postgres'
|
|
|
|
type DbBookingRow = {
|
|
id: string
|
|
confirmation_token: string
|
|
receipt_token: string
|
|
event_id: string
|
|
event_title: string
|
|
event_date_label: string
|
|
event_time_label: string
|
|
event_venue: string
|
|
customer_name: string
|
|
customer_phone: string
|
|
booking_mode_id: string | null
|
|
booking_mode: string
|
|
booking_mode_label: string | null
|
|
booking_mode_seats_per_unit: number | string | null
|
|
quantity: number | string
|
|
seat_count: number | string
|
|
ticket_type_id: string | null
|
|
ticket_type: string
|
|
ticket_label: string | null
|
|
ticket_description: string | null
|
|
unit_price: number | string
|
|
total_price: number | string
|
|
person_in_charge_id: string
|
|
person_in_charge_name: string | null
|
|
person_in_charge_phone_number: string | null
|
|
remark?: string | null
|
|
status: BookingStatus | string
|
|
status_label: string | null
|
|
created_at: Date | string
|
|
confirmed_at: Date | string | null
|
|
}
|
|
|
|
type DbBookingSeatRow = {
|
|
id: string
|
|
seat_number: number | string
|
|
seat_token: string
|
|
recipient_name: string | null
|
|
recipient_phone: string | null
|
|
shared_at: Date | string | null
|
|
created_at: Date | string
|
|
updated_at: Date | string
|
|
}
|
|
|
|
type DbBookingSeatWithBookingRow = DbBookingSeatRow & Omit<DbBookingRow, 'id' | 'created_at'> & {
|
|
booking_id: string
|
|
booking_created_at: Date | string
|
|
}
|
|
|
|
type DbBookingSettingsRow = {
|
|
event_id: string
|
|
total_tables: number | string | null
|
|
total_seats: number | string | null
|
|
updated_at: Date | string
|
|
}
|
|
|
|
type DbDinnerEventRow = {
|
|
id: string
|
|
title: string
|
|
date_label: string
|
|
time_label: string
|
|
venue: string
|
|
}
|
|
|
|
type DbBookingModeOptionRow = {
|
|
id: string
|
|
event_id: string
|
|
code: string
|
|
label: string
|
|
quantity_label: string
|
|
seats_per_unit: number | string
|
|
sort_order: number | string
|
|
}
|
|
|
|
type DbTicketCatalogItemRow = {
|
|
id: string
|
|
event_id: string
|
|
code: string
|
|
label: string
|
|
description: string
|
|
price: number | string
|
|
sort_order: number | string
|
|
}
|
|
|
|
export interface BookingModeOptionRecord extends BookingModeOption {
|
|
eventId: string
|
|
}
|
|
|
|
export interface TicketCatalogItemRecord extends TicketCatalogItem {
|
|
eventId: string
|
|
}
|
|
|
|
function bookingSelectColumns(sql: any) {
|
|
return sql`
|
|
bookings.id,
|
|
bookings.confirmation_token,
|
|
bookings.receipt_token,
|
|
dinner_events.id as event_id,
|
|
dinner_events.title as event_title,
|
|
dinner_events.date_label as event_date_label,
|
|
dinner_events.time_label as event_time_label,
|
|
dinner_events.venue as event_venue,
|
|
bookings.customer_name,
|
|
bookings.customer_phone,
|
|
bookings.booking_mode_id,
|
|
coalesce(booking_modes.code, bookings.booking_mode) as booking_mode,
|
|
booking_modes.label as booking_mode_label,
|
|
booking_modes.seats_per_unit as booking_mode_seats_per_unit,
|
|
bookings.quantity,
|
|
bookings.seat_count,
|
|
bookings.ticket_type_id,
|
|
coalesce(ticket_types.code, bookings.ticket_type) as ticket_type,
|
|
ticket_types.label as ticket_label,
|
|
ticket_types.description as ticket_description,
|
|
bookings.unit_price,
|
|
bookings.total_price,
|
|
bookings.person_in_charge_id,
|
|
coalesce(users.full_name, bookings.person_in_charge_name) as person_in_charge_name,
|
|
coalesce(users.phone_number, bookings.person_in_charge_phone_number) as person_in_charge_phone_number,
|
|
bookings.remark,
|
|
bookings.status,
|
|
booking_statuses.label as status_label,
|
|
bookings.created_at,
|
|
bookings.confirmed_at
|
|
`
|
|
}
|
|
|
|
function bookingJoins(sql: any) {
|
|
return sql`
|
|
inner join dinner_events on dinner_events.id = bookings.event_id
|
|
left join booking_modes on booking_modes.id = bookings.booking_mode_id
|
|
left join ticket_types on ticket_types.id = bookings.ticket_type_id
|
|
left join users on users.id = bookings.person_in_charge_id
|
|
left join booking_statuses on booking_statuses.code = bookings.status
|
|
`
|
|
}
|
|
|
|
function parseInteger(value: number | string) {
|
|
return typeof value === 'number' ? value : Number.parseInt(value, 10)
|
|
}
|
|
|
|
function mapDinnerEvent(row: DbDinnerEventRow): DinnerEvent {
|
|
return {
|
|
id: row.id,
|
|
title: row.title,
|
|
dateLabel: row.date_label,
|
|
timeLabel: row.time_label,
|
|
venue: row.venue
|
|
}
|
|
}
|
|
|
|
function mapDinnerEventFromBooking(row: DbBookingRow | DbBookingSeatWithBookingRow): DinnerEvent {
|
|
return {
|
|
id: row.event_id,
|
|
title: row.event_title,
|
|
dateLabel: row.event_date_label,
|
|
timeLabel: row.event_time_label,
|
|
venue: row.event_venue
|
|
}
|
|
}
|
|
|
|
function mapBookingModeOption(row: DbBookingModeOptionRow): BookingModeOptionRecord {
|
|
return {
|
|
id: row.id,
|
|
eventId: row.event_id,
|
|
value: row.code,
|
|
label: row.label,
|
|
quantityLabel: row.quantity_label,
|
|
seatsPerUnit: parseInteger(row.seats_per_unit),
|
|
sortOrder: parseInteger(row.sort_order)
|
|
}
|
|
}
|
|
|
|
function mapTicketCatalogItem(row: DbTicketCatalogItemRow): TicketCatalogItemRecord {
|
|
return {
|
|
id: row.id,
|
|
eventId: row.event_id,
|
|
value: row.code,
|
|
label: row.label,
|
|
description: row.description,
|
|
price: parseInteger(row.price),
|
|
sortOrder: parseInteger(row.sort_order)
|
|
}
|
|
}
|
|
|
|
function mapBooking(row: DbBookingRow): PublicBooking {
|
|
const seatCount = parseInteger(row.seat_count)
|
|
const status = isBookingStatus(row.status) ? row.status : 'pending'
|
|
const ticketType = row.ticket_type
|
|
const bookingMode = row.booking_mode
|
|
|
|
return {
|
|
id: row.id,
|
|
confirmationToken: row.confirmation_token,
|
|
receiptToken: row.receipt_token,
|
|
event: mapDinnerEventFromBooking(row),
|
|
customerName: row.customer_name,
|
|
customerPhone: row.customer_phone,
|
|
bookingModeId: row.booking_mode_id,
|
|
bookingMode,
|
|
bookingModeLabel: row.booking_mode_label || bookingMode,
|
|
quantity: parseInteger(row.quantity),
|
|
seatCount,
|
|
ticketTypeId: row.ticket_type_id,
|
|
ticketType,
|
|
ticketLabel: row.ticket_label || ticketType.toUpperCase(),
|
|
ticketDescription: row.ticket_description,
|
|
unitPrice: parseInteger(row.unit_price),
|
|
totalPrice: parseInteger(row.total_price),
|
|
personInChargeId: row.person_in_charge_id,
|
|
personInChargeName: row.person_in_charge_name || '',
|
|
personInChargePhoneNumber: row.person_in_charge_phone_number || '',
|
|
remark: row.remark || null,
|
|
status,
|
|
statusLabel: row.status_label || getBookingStatusLabel(status),
|
|
createdAt: toIsoString(row.created_at) ?? new Date().toISOString(),
|
|
confirmedAt: toIsoString(row.confirmed_at)
|
|
}
|
|
}
|
|
|
|
function mapReceiptBooking(row: DbBookingRow | DbBookingSeatWithBookingRow): ReceiptBooking {
|
|
const seatCount = parseInteger(row.seat_count)
|
|
const status = isBookingStatus(row.status) ? row.status : 'pending'
|
|
const ticketType = row.ticket_type
|
|
const bookingMode = row.booking_mode
|
|
|
|
return {
|
|
id: row.id,
|
|
receiptToken: row.receipt_token,
|
|
event: mapDinnerEventFromBooking(row),
|
|
customerName: row.customer_name,
|
|
customerPhone: row.customer_phone,
|
|
bookingModeId: row.booking_mode_id,
|
|
bookingMode,
|
|
bookingModeLabel: row.booking_mode_label || bookingMode,
|
|
quantity: parseInteger(row.quantity),
|
|
seatCount,
|
|
ticketTypeId: row.ticket_type_id,
|
|
ticketType,
|
|
ticketLabel: row.ticket_label || ticketType.toUpperCase(),
|
|
ticketDescription: row.ticket_description,
|
|
unitPrice: parseInteger(row.unit_price),
|
|
totalPrice: parseInteger(row.total_price),
|
|
status,
|
|
statusLabel: row.status_label || getBookingStatusLabel(status),
|
|
createdAt: toIsoString('booking_created_at' in row ? row.booking_created_at : row.created_at) ?? new Date().toISOString(),
|
|
confirmedAt: toIsoString(row.confirmed_at)
|
|
}
|
|
}
|
|
|
|
function mapPublicBookingToReceiptBooking(booking: PublicBooking): ReceiptBooking {
|
|
return {
|
|
id: booking.id,
|
|
receiptToken: booking.receiptToken,
|
|
event: booking.event,
|
|
customerName: booking.customerName,
|
|
customerPhone: booking.customerPhone,
|
|
bookingModeId: booking.bookingModeId,
|
|
bookingMode: booking.bookingMode,
|
|
bookingModeLabel: booking.bookingModeLabel,
|
|
quantity: booking.quantity,
|
|
seatCount: booking.seatCount,
|
|
ticketTypeId: booking.ticketTypeId,
|
|
ticketType: booking.ticketType,
|
|
ticketLabel: booking.ticketLabel,
|
|
ticketDescription: booking.ticketDescription,
|
|
unitPrice: booking.unitPrice,
|
|
totalPrice: booking.totalPrice,
|
|
status: booking.status,
|
|
statusLabel: booking.statusLabel,
|
|
createdAt: booking.createdAt,
|
|
confirmedAt: booking.confirmedAt
|
|
}
|
|
}
|
|
|
|
function mapBookingSeat(row: DbBookingSeatRow): PublicBookingSeat {
|
|
return {
|
|
id: row.id,
|
|
seatNumber: parseInteger(row.seat_number),
|
|
seatToken: row.seat_token,
|
|
recipientName: row.recipient_name,
|
|
recipientPhone: row.recipient_phone,
|
|
sharedAt: toIsoString(row.shared_at),
|
|
createdAt: toIsoString(row.created_at) ?? new Date().toISOString(),
|
|
updatedAt: toIsoString(row.updated_at) ?? new Date().toISOString()
|
|
}
|
|
}
|
|
|
|
function mapBookingCapacitySettings(row: DbBookingSettingsRow | undefined): BookingCapacitySettings {
|
|
if (!row) {
|
|
return {
|
|
totalSeats: null,
|
|
updatedAt: null
|
|
}
|
|
}
|
|
|
|
const totalSeats = row.total_seats === null ? null : parseInteger(row.total_seats)
|
|
|
|
return {
|
|
totalSeats,
|
|
updatedAt: toIsoString(row.updated_at)
|
|
}
|
|
}
|
|
|
|
export async function getPublicBookingConfig(): Promise<PublicBookingConfig> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [event] = await sql<DbDinnerEventRow[]>`
|
|
select
|
|
id,
|
|
title,
|
|
date_label,
|
|
time_label,
|
|
venue
|
|
from dinner_events
|
|
where is_active = true
|
|
order by sort_order asc, created_at asc
|
|
limit 1
|
|
`
|
|
|
|
if (!event) {
|
|
throw new Error('No active dinner event is configured.')
|
|
}
|
|
|
|
const [bookingModes, ticketCatalog] = await Promise.all([
|
|
sql<DbBookingModeOptionRow[]>`
|
|
select
|
|
id,
|
|
event_id,
|
|
code,
|
|
label,
|
|
quantity_label,
|
|
seats_per_unit,
|
|
sort_order
|
|
from booking_modes
|
|
where event_id = ${event.id}
|
|
and is_active = true
|
|
order by sort_order asc, label asc
|
|
`,
|
|
sql<DbTicketCatalogItemRow[]>`
|
|
select
|
|
id,
|
|
event_id,
|
|
code,
|
|
label,
|
|
description,
|
|
price,
|
|
sort_order
|
|
from ticket_types
|
|
where event_id = ${event.id}
|
|
and is_active = true
|
|
order by sort_order asc, label asc
|
|
`
|
|
])
|
|
|
|
return {
|
|
event: mapDinnerEvent(event),
|
|
bookingModes: bookingModes.map(mapBookingModeOption),
|
|
ticketCatalog: ticketCatalog.map(mapTicketCatalogItem)
|
|
}
|
|
}
|
|
|
|
export async function getActiveBookingModeOptionByCode(code: string): Promise<BookingModeOptionRecord | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingModeOptionRow[]>`
|
|
select
|
|
booking_modes.id,
|
|
booking_modes.event_id,
|
|
booking_modes.code,
|
|
booking_modes.label,
|
|
booking_modes.quantity_label,
|
|
booking_modes.seats_per_unit,
|
|
booking_modes.sort_order
|
|
from booking_modes
|
|
inner join dinner_events on dinner_events.id = booking_modes.event_id
|
|
where dinner_events.is_active = true
|
|
and booking_modes.is_active = true
|
|
and booking_modes.code = ${code}
|
|
order by booking_modes.sort_order asc
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapBookingModeOption(row) : null
|
|
}
|
|
|
|
export async function getActiveTicketCatalogItemByCode(code: string): Promise<TicketCatalogItemRecord | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbTicketCatalogItemRow[]>`
|
|
select
|
|
ticket_types.id,
|
|
ticket_types.event_id,
|
|
ticket_types.code,
|
|
ticket_types.label,
|
|
ticket_types.description,
|
|
ticket_types.price,
|
|
ticket_types.sort_order
|
|
from ticket_types
|
|
inner join dinner_events on dinner_events.id = ticket_types.event_id
|
|
where dinner_events.is_active = true
|
|
and ticket_types.is_active = true
|
|
and ticket_types.code = ${code}
|
|
order by ticket_types.sort_order asc
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapTicketCatalogItem(row) : null
|
|
}
|
|
|
|
async function insertBookingSeats(
|
|
tx: ReturnType<typeof getSqlClient>,
|
|
bookingId: string,
|
|
seatCount: number
|
|
) {
|
|
for (let seatNumber = 1; seatNumber <= seatCount; seatNumber += 1) {
|
|
await tx`
|
|
insert into booking_seats (
|
|
id,
|
|
booking_id,
|
|
seat_number,
|
|
seat_token
|
|
)
|
|
values (
|
|
${randomUUID()},
|
|
${bookingId},
|
|
${seatNumber},
|
|
${randomToken(24)}
|
|
)
|
|
`
|
|
}
|
|
}
|
|
|
|
export async function createBooking(input: {
|
|
eventId: string
|
|
customerName: string
|
|
customerPhone: string
|
|
bookingModeId: string
|
|
bookingMode: BookingMode
|
|
quantity: number
|
|
seatCount: number
|
|
ticketTypeId: string
|
|
ticketType: TicketType
|
|
unitPrice: number
|
|
totalPrice: number
|
|
personInChargeId: string
|
|
}) {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
const bookingId = randomUUID()
|
|
const confirmationToken = randomToken(24)
|
|
const receiptToken = randomToken(24)
|
|
|
|
const row = await sql.begin(async (tx) => {
|
|
const [createdBooking] = await tx<DbBookingRow[]>`
|
|
with inserted_booking as (
|
|
insert into bookings (
|
|
id,
|
|
confirmation_token,
|
|
receipt_token,
|
|
event_id,
|
|
customer_name,
|
|
customer_phone,
|
|
booking_mode_id,
|
|
booking_mode,
|
|
quantity,
|
|
seat_count,
|
|
ticket_type_id,
|
|
ticket_type,
|
|
unit_price,
|
|
total_price,
|
|
person_in_charge_id,
|
|
remark,
|
|
status
|
|
)
|
|
values (
|
|
${bookingId},
|
|
${confirmationToken},
|
|
${receiptToken},
|
|
${input.eventId},
|
|
${input.customerName},
|
|
${input.customerPhone},
|
|
${input.bookingModeId},
|
|
${input.bookingMode},
|
|
${input.quantity},
|
|
${input.seatCount},
|
|
${input.ticketTypeId},
|
|
${input.ticketType},
|
|
${input.unitPrice},
|
|
${input.totalPrice},
|
|
${input.personInChargeId},
|
|
null,
|
|
'pending'
|
|
)
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(tx)}
|
|
from inserted_booking as bookings
|
|
${bookingJoins(tx)}
|
|
`
|
|
|
|
await insertBookingSeats(tx, bookingId, input.seatCount)
|
|
|
|
return createdBooking
|
|
})
|
|
|
|
return {
|
|
booking: mapBooking(row),
|
|
confirmationToken,
|
|
receiptToken
|
|
}
|
|
}
|
|
|
|
export async function getBookingByConfirmationToken(confirmationToken: string): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where bookings.confirmation_token = ${confirmationToken}
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapBooking(row) : null
|
|
}
|
|
|
|
export async function getBookingByReceiptToken(receiptToken: string): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where bookings.receipt_token = ${receiptToken}
|
|
limit 1
|
|
`
|
|
|
|
return row ? mapBooking(row) : null
|
|
}
|
|
|
|
export async function listBookings(options?: {
|
|
personInChargeId?: string
|
|
}): Promise<PublicBooking[]> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = options?.personInChargeId
|
|
? await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
and bookings.person_in_charge_id = ${options.personInChargeId}
|
|
order by bookings.created_at desc
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
select ${bookingSelectColumns(sql)}
|
|
from bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
order by bookings.created_at desc
|
|
`
|
|
|
|
return rows.map(mapBooking)
|
|
}
|
|
|
|
export async function updateBookingRemark(input: {
|
|
bookingId: string
|
|
personInChargeId?: string
|
|
remark: string | null
|
|
}): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = input.personInChargeId
|
|
? await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
remark = ${input.remark},
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
and person_in_charge_id = ${input.personInChargeId}
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
limit 1
|
|
`
|
|
: await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
remark = ${input.remark},
|
|
updated_at = now()
|
|
where id = ${input.bookingId}
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
where dinner_events.is_active = true
|
|
limit 1
|
|
`
|
|
|
|
return rows[0] ? mapBooking(rows[0]) : null
|
|
}
|
|
|
|
export async function listBookingSeats(bookingId: string): Promise<PublicBookingSeat[]> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const rows = await sql<DbBookingSeatRow[]>`
|
|
select
|
|
id,
|
|
seat_number,
|
|
seat_token,
|
|
recipient_name,
|
|
recipient_phone,
|
|
shared_at,
|
|
created_at,
|
|
updated_at
|
|
from booking_seats
|
|
where booking_id = ${bookingId}
|
|
order by seat_number asc
|
|
`
|
|
|
|
return rows.map(mapBookingSeat)
|
|
}
|
|
|
|
export async function getBookingReceiptByReceiptToken(receiptToken: string): Promise<{
|
|
booking: ReceiptBooking
|
|
seats: PublicBookingSeat[]
|
|
} | null> {
|
|
const booking = await getBookingByReceiptToken(receiptToken)
|
|
|
|
if (!booking) {
|
|
return null
|
|
}
|
|
|
|
const seats = await listBookingSeats(booking.id)
|
|
|
|
return {
|
|
booking: mapPublicBookingToReceiptBooking(booking),
|
|
seats
|
|
}
|
|
}
|
|
|
|
export async function getSeatReceiptBySeatToken(seatToken: string): Promise<{
|
|
booking: ReceiptBooking
|
|
seat: PublicBookingSeat
|
|
} | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSeatWithBookingRow[]>`
|
|
select
|
|
booking_seats.id,
|
|
booking_seats.seat_number,
|
|
booking_seats.seat_token,
|
|
booking_seats.recipient_name,
|
|
booking_seats.recipient_phone,
|
|
booking_seats.shared_at,
|
|
booking_seats.created_at,
|
|
booking_seats.updated_at,
|
|
bookings.id as booking_id,
|
|
bookings.confirmation_token,
|
|
bookings.receipt_token,
|
|
dinner_events.id as event_id,
|
|
dinner_events.title as event_title,
|
|
dinner_events.date_label as event_date_label,
|
|
dinner_events.time_label as event_time_label,
|
|
dinner_events.venue as event_venue,
|
|
bookings.customer_name,
|
|
bookings.customer_phone,
|
|
bookings.booking_mode_id,
|
|
coalesce(booking_modes.code, bookings.booking_mode) as booking_mode,
|
|
booking_modes.label as booking_mode_label,
|
|
booking_modes.seats_per_unit as booking_mode_seats_per_unit,
|
|
bookings.quantity,
|
|
bookings.seat_count,
|
|
bookings.ticket_type_id,
|
|
coalesce(ticket_types.code, bookings.ticket_type) as ticket_type,
|
|
ticket_types.label as ticket_label,
|
|
ticket_types.description as ticket_description,
|
|
bookings.unit_price,
|
|
bookings.total_price,
|
|
bookings.person_in_charge_id,
|
|
coalesce(users.full_name, bookings.person_in_charge_name) as person_in_charge_name,
|
|
coalesce(users.phone_number, bookings.person_in_charge_phone_number) as person_in_charge_phone_number,
|
|
bookings.status,
|
|
booking_statuses.label as status_label,
|
|
bookings.created_at as booking_created_at,
|
|
bookings.confirmed_at
|
|
from booking_seats
|
|
inner join bookings on bookings.id = booking_seats.booking_id
|
|
${bookingJoins(sql)}
|
|
where booking_seats.seat_token = ${seatToken}
|
|
limit 1
|
|
`
|
|
|
|
if (!row) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
booking: mapReceiptBooking({
|
|
...row,
|
|
id: row.booking_id
|
|
}),
|
|
seat: mapBookingSeat(row)
|
|
}
|
|
}
|
|
|
|
export async function updateBookingSeatShareByReceiptToken(input: {
|
|
receiptToken: string
|
|
seatId: string
|
|
shared: boolean
|
|
recipientName: string | null
|
|
recipientPhone: string | null
|
|
}): Promise<PublicBookingSeat | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
const nextSeatToken = input.shared ? null : randomToken(24)
|
|
|
|
const [row] = await sql<DbBookingSeatRow[]>`
|
|
update booking_seats
|
|
set
|
|
recipient_name = ${input.shared ? input.recipientName : null},
|
|
recipient_phone = ${input.shared ? input.recipientPhone : null},
|
|
shared_at = ${input.shared ? new Date() : null},
|
|
seat_token = coalesce(${nextSeatToken}, seat_token),
|
|
updated_at = now()
|
|
from bookings
|
|
where booking_seats.booking_id = bookings.id
|
|
and bookings.receipt_token = ${input.receiptToken}
|
|
and booking_seats.id = ${input.seatId}
|
|
returning
|
|
booking_seats.id,
|
|
booking_seats.seat_number,
|
|
booking_seats.seat_token,
|
|
booking_seats.recipient_name,
|
|
booking_seats.recipient_phone,
|
|
booking_seats.shared_at,
|
|
booking_seats.created_at,
|
|
booking_seats.updated_at
|
|
`
|
|
|
|
return row ? mapBookingSeat(row) : null
|
|
}
|
|
|
|
export async function getBookingCapacitySettings(): Promise<BookingCapacitySettings> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSettingsRow[]>`
|
|
select
|
|
booking_settings.event_id,
|
|
total_tables,
|
|
total_seats,
|
|
booking_settings.updated_at
|
|
from booking_settings
|
|
inner join dinner_events on dinner_events.id = booking_settings.event_id
|
|
where dinner_events.is_active = true
|
|
order by dinner_events.sort_order asc
|
|
limit 1
|
|
`
|
|
|
|
return mapBookingCapacitySettings(row)
|
|
}
|
|
|
|
export async function updateBookingCapacitySettings(input: {
|
|
totalSeats: number | null
|
|
}): Promise<BookingCapacitySettings> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingSettingsRow[]>`
|
|
update booking_settings
|
|
set
|
|
total_seats = ${input.totalSeats},
|
|
updated_at = now()
|
|
from dinner_events
|
|
where booking_settings.event_id = dinner_events.id
|
|
and dinner_events.is_active = true
|
|
returning
|
|
booking_settings.event_id,
|
|
total_tables,
|
|
total_seats,
|
|
booking_settings.updated_at
|
|
`
|
|
|
|
return mapBookingCapacitySettings(row)
|
|
}
|
|
|
|
export async function getBookingInventorySummary(): Promise<BookingInventorySummary> {
|
|
const [bookings, settings] = await Promise.all([
|
|
listBookings(),
|
|
getBookingCapacitySettings()
|
|
])
|
|
|
|
return calculateBookingInventorySummary(bookings, settings)
|
|
}
|
|
|
|
export async function confirmBookingByConfirmationToken(confirmationToken: string): Promise<PublicBooking | null> {
|
|
await ensureDatabaseReady()
|
|
const sql = getSqlClient()
|
|
|
|
const [row] = await sql<DbBookingRow[]>`
|
|
with updated_booking as (
|
|
update bookings
|
|
set
|
|
status = 'confirmed',
|
|
confirmed_at = now(),
|
|
updated_at = now()
|
|
where confirmation_token = ${confirmationToken}
|
|
and status = 'pending'
|
|
returning *
|
|
)
|
|
select ${bookingSelectColumns(sql)}
|
|
from updated_booking as bookings
|
|
${bookingJoins(sql)}
|
|
`
|
|
|
|
if (row) {
|
|
return mapBooking(row)
|
|
}
|
|
|
|
return await getBookingByConfirmationToken(confirmationToken)
|
|
}
|