167 lines
4.9 KiB
TypeScript
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,
|
|
}
|
|
},
|
|
})
|