Files
seminarhof-e2e-mqo21zci/packages/functions/src/functions/get-admin-bookings.function.ts
2026-06-21 19:23:11 +02:00

167 lines
4.9 KiB
TypeScript

import { z } from 'zod'
import { pikkuSessionlessFunc } from '#pikku'
import { BookingZ } from '#pikku/db/zod.gen.js'
export const GetAdminBookingsInput = z.object({
venueSlug: z.string().default('drawehn'),
years: z.array(z.number().int()).optional(),
status: z.array(z.string()).optional(),
search: z.string().optional(),
clientId: z.string().optional(),
halfHouseOnly: z.boolean().optional(),
})
const Row = BookingZ
.pick({
bookingId: true,
eventName: true,
status: true,
halfHouse: true,
expectedPersons: true,
})
.extend({
startDate: z.date().nullable(),
endDate: z.date().nullable(),
clientName: z.string().nullable(),
isStammgruppe: z.boolean(),
participantCount: z.number(),
hasDeposit: z.boolean(),
hasFinal: z.boolean(),
depositPaid: z.boolean(),
finalPaid: z.boolean(),
})
export const GetAdminBookingsOutput = z.object({
bookings: z.array(Row),
totalCount: z.number(),
yearOptions: z.array(z.number()),
})
export const getAdminBookings = pikkuSessionlessFunc({
expose: true,
description: 'Admin booking list with filters — replaces Buchungsübersicht spreadsheet.',
input: GetAdminBookingsInput,
output: GetAdminBookingsOutput,
func: async ({ kysely }, input) => {
const venue = await kysely
.selectFrom('venue')
.where('slug', '=', input.venueSlug)
.select('venueId')
.executeTakeFirstOrThrow()
let q = kysely
.selectFrom('booking')
.innerJoin('client', 'client.clientId', 'booking.clientId')
.where('booking.venueId', '=', venue.venueId)
if (input.years?.length) {
const years = input.years.map(String)
q = q.where(
(eb) => eb.fn<string>('substr', ['booking.startDate', eb.lit(1), eb.lit(4)]),
'in',
years,
)
}
if (input.status?.length) {
q = q.where('booking.status', 'in', input.status)
}
if (input.clientId) {
q = q.where('booking.clientId', '=', input.clientId)
}
if (input.halfHouseOnly) {
q = q.where('booking.halfHouse', '=', 1)
}
if (input.search?.trim()) {
const term = `%${input.search.trim()}%`
q = q.where((eb) =>
eb.or([
eb('booking.eventName', 'like', term),
eb('client.name', 'like', term),
]),
)
}
const rows = await q
.orderBy('booking.startDate', 'desc')
.select([
'booking.bookingId',
'booking.eventName',
'booking.startDate',
'booking.endDate',
'booking.status',
'booking.halfHouse',
'booking.expectedPersons',
'client.name as clientName',
'client.isStammgruppe',
])
.execute()
const bookingIds = rows.map((r) => r.bookingId)
const participantCounts = bookingIds.length
? await kysely
.selectFrom('participant')
.where('bookingId', 'in', bookingIds)
.select(({ fn }) => ['bookingId', fn.countAll<number>().as('count')])
.groupBy('bookingId')
.execute()
: []
const countByBooking = new Map(participantCounts.map((r) => [r.bookingId, Number(r.count)]))
const invoices = bookingIds.length
? await kysely
.selectFrom('invoice')
.where('bookingId', 'in', bookingIds)
.select(['bookingId', 'kind', 'status'])
.execute()
: []
const invoicesByBooking = new Map<string, typeof invoices>()
for (const inv of invoices) {
const list = invoicesByBooking.get(inv.bookingId) ?? []
list.push(inv)
invoicesByBooking.set(inv.bookingId, list)
}
const bookings = rows.map((r) => {
const invs = invoicesByBooking.get(r.bookingId) ?? []
const deposit = invs.find((i) => i.kind === 'deposit')
const final = invs.find((i) => i.kind === 'final')
return {
bookingId: r.bookingId,
eventName: r.eventName,
startDate: r.startDate ? new Date(r.startDate) : null,
endDate: r.endDate ? new Date(r.endDate) : null,
status: r.status,
halfHouse: r.halfHouse,
expectedPersons: r.expectedPersons,
clientName: r.clientName,
isStammgruppe: !!r.isStammgruppe,
participantCount: countByBooking.get(r.bookingId) ?? 0,
hasDeposit: !!deposit,
hasFinal: !!final,
depositPaid: deposit?.status === 'paid',
finalPaid: final?.status === 'paid',
}
})
// Year facet — derived from full venue, not filtered set, so the
// chips reflect real data even after filtering.
const yearRows = await kysely
.selectFrom('booking')
.where('venueId', '=', venue.venueId)
.select((eb) => eb.fn<string>('substr', ['booking.startDate', eb.lit(1), eb.lit(4)]).as('year'))
.distinct()
.execute()
const yearOptions = yearRows
.map((r) => Number(r.year))
.filter((y) => Number.isFinite(y))
.sort((a, b) => b - a)
return {
bookings,
totalCount: bookings.length,
yearOptions,
}
},
})