feat(bookings): allow editing and soft-deleting bookings

Add edit modal to update guest details, ticket selection, and quantity
Implement soft delete functionality to archive bookings
This commit is contained in:
2026-05-08 15:57:32 +08:00
parent 1318e766d5
commit e05c238495
7 changed files with 754 additions and 14 deletions

View File

@@ -35,6 +35,7 @@ type DbBookingRow = {
customer_name: string
customer_phone: string
locale: AppLocale | string | null
deleted_at: Date | string | null
booking_mode_id: string | null
booking_mode: string
booking_mode_label: string | null
@@ -129,6 +130,7 @@ function bookingSelectColumns(sql: any) {
bookings.customer_name,
bookings.customer_phone,
bookings.locale,
bookings.deleted_at,
bookings.booking_mode_id,
coalesce(booking_modes.code, bookings.booking_mode) as booking_mode,
booking_modes.label as booking_mode_label,
@@ -556,6 +558,7 @@ export async function getBookingByConfirmationToken(confirmationToken: string):
from bookings
${bookingJoins(sql)}
where bookings.confirmation_token = ${confirmationToken}
and bookings.deleted_at is null
limit 1
`
@@ -571,6 +574,7 @@ export async function getBookingByReceiptToken(receiptToken: string): Promise<Pu
from bookings
${bookingJoins(sql)}
where bookings.receipt_token = ${receiptToken}
and bookings.deleted_at is null
limit 1
`
@@ -589,6 +593,7 @@ export async function listBookings(options?: {
from bookings
${bookingJoins(sql)}
where dinner_events.is_active = true
and bookings.deleted_at is null
and bookings.person_in_charge_id = ${options.personInChargeId}
order by bookings.created_at desc
`
@@ -597,6 +602,7 @@ export async function listBookings(options?: {
from bookings
${bookingJoins(sql)}
where dinner_events.is_active = true
and bookings.deleted_at is null
order by bookings.created_at desc
`
@@ -618,8 +624,9 @@ export async function updateBookingRemark(input: {
set
remark = ${input.remark},
updated_at = now()
where id = ${input.bookingId}
and person_in_charge_id = ${input.personInChargeId}
where id = ${input.bookingId}
and deleted_at is null
and person_in_charge_id = ${input.personInChargeId}
returning *
)
select ${bookingSelectColumns(sql)}
@@ -634,8 +641,9 @@ export async function updateBookingRemark(input: {
set
remark = ${input.remark},
updated_at = now()
where id = ${input.bookingId}
returning *
where id = ${input.bookingId}
and deleted_at is null
returning *
)
select ${bookingSelectColumns(sql)}
from updated_booking as bookings
@@ -662,8 +670,9 @@ export async function updateBookingPersonInCharge(input: {
set
person_in_charge_id = ${input.nextPersonInChargeId},
updated_at = now()
where id = ${input.bookingId}
and person_in_charge_id = ${input.currentPersonInChargeId}
where id = ${input.bookingId}
and deleted_at is null
and person_in_charge_id = ${input.currentPersonInChargeId}
returning *
)
select ${bookingSelectColumns(sql)}
@@ -678,8 +687,9 @@ export async function updateBookingPersonInCharge(input: {
set
person_in_charge_id = ${input.nextPersonInChargeId},
updated_at = now()
where id = ${input.bookingId}
returning *
where id = ${input.bookingId}
and deleted_at is null
returning *
)
select ${bookingSelectColumns(sql)}
from updated_booking as bookings
@@ -691,6 +701,178 @@ export async function updateBookingPersonInCharge(input: {
return rows[0] ? mapBooking(rows[0]) : null
}
export async function getBookingById(bookingId: string, options?: {
personInChargeId?: string
}): Promise<PublicBooking | null> {
await ensureDatabaseReady()
const sql = getSqlClient()
const rows = options?.personInChargeId
? await sql<DbBookingRow[]>`
select ${bookingSelectColumns(sql)}
from bookings
${bookingJoins(sql)}
where bookings.id = ${bookingId}
and bookings.deleted_at is null
and bookings.person_in_charge_id = ${options.personInChargeId}
limit 1
`
: await sql<DbBookingRow[]>`
select ${bookingSelectColumns(sql)}
from bookings
${bookingJoins(sql)}
where bookings.id = ${bookingId}
and bookings.deleted_at is null
limit 1
`
return rows[0] ? mapBooking(rows[0]) : null
}
async function syncBookingSeats(tx: ReturnType<typeof getSqlClient>, bookingId: string, currentSeatCount: number, nextSeatCount: number) {
if (nextSeatCount > currentSeatCount) {
for (let seatNumber = currentSeatCount + 1; seatNumber <= nextSeatCount; seatNumber += 1) {
await tx`
insert into booking_seats (
id,
booking_id,
seat_number,
seat_token
)
values (
${randomUUID()},
${bookingId},
${seatNumber},
${randomToken(24)}
)
`
}
return
}
if (nextSeatCount < currentSeatCount) {
await tx`
delete from booking_seats
where booking_id = ${bookingId}
and seat_number > ${nextSeatCount}
`
}
}
export async function updateBookingDetails(input: {
bookingId: string
customerName: string
customerPhone: string
bookingModeId: string
bookingMode: BookingMode
quantity: number
seatCount: number
ticketTypeId: string
ticketType: TicketType
unitPrice: number
totalPrice: number
remark: string | null
personInChargeId?: string
}) {
await ensureDatabaseReady()
const sql = getSqlClient()
return await sql.begin(async (tx) => {
const [currentBooking] = await tx<{ seat_count: number | string }[]>`
select seat_count
from bookings
where id = ${input.bookingId}
and deleted_at is null
limit 1
`
if (!currentBooking) {
return null
}
const currentSeatCount = parseInteger(currentBooking.seat_count)
const [row] = await tx<DbBookingRow[]>`
with updated_booking as (
update bookings
set
customer_name = ${input.customerName},
customer_phone = ${input.customerPhone},
booking_mode_id = ${input.bookingModeId},
booking_mode = ${input.bookingMode},
quantity = ${input.quantity},
seat_count = ${input.seatCount},
ticket_type_id = ${input.ticketTypeId},
ticket_type = ${input.ticketType},
unit_price = ${input.unitPrice},
total_price = ${input.totalPrice},
remark = ${input.remark},
updated_at = now()
where id = ${input.bookingId}
and deleted_at is null
${input.personInChargeId ? tx`and person_in_charge_id = ${input.personInChargeId}` : tx``}
returning *
)
select ${bookingSelectColumns(tx)}
from updated_booking as bookings
${bookingJoins(tx)}
limit 1
`
if (!row) {
return null
}
await syncBookingSeats(tx, input.bookingId, currentSeatCount, input.seatCount)
return mapBooking(row)
})
}
export async function softDeleteBooking(input: {
bookingId: string
personInChargeId?: string
}) {
await ensureDatabaseReady()
const sql = getSqlClient()
const rows = input.personInChargeId
? await sql<DbBookingRow[]>`
with updated_booking as (
update bookings
set
deleted_at = now(),
updated_at = now()
where id = ${input.bookingId}
and deleted_at is null
and person_in_charge_id = ${input.personInChargeId}
returning *
)
select ${bookingSelectColumns(sql)}
from updated_booking as bookings
${bookingJoins(sql)}
limit 1
`
: await sql<DbBookingRow[]>`
with updated_booking as (
update bookings
set
deleted_at = now(),
updated_at = now()
where id = ${input.bookingId}
and deleted_at is null
returning *
)
select ${bookingSelectColumns(sql)}
from updated_booking as bookings
${bookingJoins(sql)}
limit 1
`
return rows[0] ? mapBooking(rows[0]) : null
}
export async function listBookingSeats(bookingId: string): Promise<PublicBookingSeat[]> {
await ensureDatabaseReady()
const sql = getSqlClient()
@@ -707,6 +889,12 @@ export async function listBookingSeats(bookingId: string): Promise<PublicBooking
updated_at
from booking_seats
where booking_id = ${bookingId}
and exists (
select 1
from bookings
where bookings.id = booking_seats.booking_id
and bookings.deleted_at is null
)
order by seat_number asc
`
@@ -759,6 +947,7 @@ export async function getSeatReceiptBySeatToken(seatToken: string): Promise<{
bookings.customer_name,
bookings.customer_phone,
bookings.locale,
bookings.deleted_at,
bookings.booking_mode_id,
coalesce(booking_modes.code, bookings.booking_mode) as booking_mode,
booking_modes.label as booking_mode_label,
@@ -782,6 +971,7 @@ export async function getSeatReceiptBySeatToken(seatToken: string): Promise<{
inner join bookings on bookings.id = booking_seats.booking_id
${bookingJoins(sql)}
where booking_seats.seat_token = ${seatToken}
and bookings.deleted_at is null
limit 1
`
@@ -820,6 +1010,7 @@ export async function updateBookingSeatShareByReceiptToken(input: {
from bookings
where booking_seats.booking_id = bookings.id
and bookings.receipt_token = ${input.receiptToken}
and bookings.deleted_at is null
and booking_seats.id = ${input.seatId}
returning
booking_seats.id,
@@ -899,8 +1090,9 @@ export async function confirmBookingByConfirmationToken(confirmationToken: strin
status = 'confirmed',
confirmed_at = now(),
updated_at = now()
where confirmation_token = ${confirmationToken}
and status = 'pending'
where confirmation_token = ${confirmationToken}
and deleted_at is null
and status = 'pending'
returning *
)
select ${bookingSelectColumns(sql)}
@@ -926,8 +1118,9 @@ export async function cancelBookingConfirmationByConfirmationToken(confirmationT
status = 'pending',
confirmed_at = null,
updated_at = now()
where confirmation_token = ${confirmationToken}
and status = 'confirmed'
where confirmation_token = ${confirmationToken}
and deleted_at is null
and status = 'confirmed'
returning *
)
select ${bookingSelectColumns(sql)}