feat(bookings): allow transferring bookings to another PIC

Add UI modal and button to reassign bookings to different contacts
Create API endpoint and repository method to handle PIC transfers
This commit is contained in:
2026-05-08 14:36:01 +08:00
parent 13e85cfcd0
commit f77f4390b6
5 changed files with 277 additions and 2 deletions

View File

@@ -0,0 +1,36 @@
import type { TransferBookingPicResponse } from '~~/shared/booking'
import { requireAuth } from '../../../utils/auth'
import { updateBookingPersonInCharge } from '../../../utils/booking-repository'
import { parseBookingPicTransferInput } from '../../../utils/bookings'
import { getRequiredRouteParam, httpError } from '../../../utils/http'
import { getPublicContactById } from '../../../utils/user-repository'
export default defineEventHandler(async (event): Promise<TransferBookingPicResponse> => {
const auth = await requireAuth(event)
const bookingId = getRequiredRouteParam(event, 'id', 'Booking ID')
const body = await readBody<{
personInChargeId?: string | null
}>(event)
const input = parseBookingPicTransferInput(body)
const nextPersonInCharge = await getPublicContactById(input.personInChargeId)
if (!nextPersonInCharge) {
httpError(404, 'Selected person in charge is not available')
}
const booking = await updateBookingPersonInCharge({
bookingId,
nextPersonInChargeId: nextPersonInCharge.id,
currentPersonInChargeId: auth.user.role === 'super_admin' ? undefined : auth.user.id
})
if (!booking) {
httpError(404, 'Booking not found')
}
return {
booking
}
})

View File

@@ -637,6 +637,50 @@ export async function updateBookingRemark(input: {
return rows[0] ? mapBooking(rows[0]) : null
}
export async function updateBookingPersonInCharge(input: {
bookingId: string
currentPersonInChargeId?: string
nextPersonInChargeId: string
}): Promise<PublicBooking | null> {
await ensureDatabaseReady()
const sql = getSqlClient()
const rows = input.currentPersonInChargeId
? await sql<DbBookingRow[]>`
with updated_booking as (
update bookings
set
person_in_charge_id = ${input.nextPersonInChargeId},
updated_at = now()
where id = ${input.bookingId}
and person_in_charge_id = ${input.currentPersonInChargeId}
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
person_in_charge_id = ${input.nextPersonInChargeId},
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()

View File

@@ -51,6 +51,20 @@ export function parseBookingRemarkInput(body: {
}
}
export function parseBookingPicTransferInput(body: {
personInChargeId?: string | null
}) {
const personInChargeId = typeof body.personInChargeId === 'string'
? body.personInChargeId.trim()
: ''
assertBadRequest(personInChargeId, 'Person in charge is required')
return {
personInChargeId
}
}
export function buildBookingMessage(booking: PublicBooking, confirmationUrl: string) {
return [
`I'd like to book tickets for the ${booking.event.title}.`,