import { z } from 'zod' import { FlightDetailStatus } from '@prisma/client' import { router, adminProcedure } from '../trpc' import { logAudit } from '../utils/audit' export const logisticsRouter = router({ /** Read the hotel for a program (1:1). Null if not yet set. */ getHotel: adminProcedure .input(z.object({ programId: z.string() })) .query(async ({ ctx, input }) => { return ctx.prisma.hotel.findUnique({ where: { programId: input.programId } }) }), /** Create or update the program's hotel. Empty link strings are stored as null. */ upsertHotel: adminProcedure .input( z.object({ programId: z.string(), name: z.string().min(1).max(200), address: z.string().max(500).optional(), link: z .string() .url() .or(z.literal('')) .optional(), notes: z.string().max(2000).optional(), }), ) .mutation(async ({ ctx, input }) => { const link = input.link && input.link.trim().length > 0 ? input.link : null const hotel = await ctx.prisma.hotel.upsert({ where: { programId: input.programId }, create: { programId: input.programId, name: input.name, address: input.address ?? null, link, notes: input.notes ?? null, }, update: { name: input.name, address: input.address ?? null, link, notes: input.notes ?? null, }, }) await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'HOTEL_UPSERT', entityType: 'Hotel', entityId: hotel.id, detailsJson: { programId: input.programId, name: input.name }, }) return hotel }), /** * List all attending members for CONFIRMED finalists in a program, with * their (optional) flight details. One row per attendee — even those * without a FlightDetail row yet, so the UI can render empty editors. */ listFlightDetails: adminProcedure .input(z.object({ programId: z.string() })) .query(async ({ ctx, input }) => { return ctx.prisma.attendingMember.findMany({ where: { confirmation: { status: 'CONFIRMED', project: { programId: input.programId }, }, }, select: { id: true, needsVisa: true, user: { select: { id: true, name: true, email: true, country: true } }, confirmation: { select: { project: { select: { id: true, title: true, country: true, competitionCategory: true, }, }, }, }, flightDetail: true, }, orderBy: [{ user: { name: 'asc' } }], }) }), /** Create or update a flight detail row for an attending member. */ upsertFlightDetail: adminProcedure .input( z.object({ attendingMemberId: z.string(), arrivalAt: z.date().nullable().optional(), arrivalFlightNumber: z.string().max(20).nullable().optional(), arrivalAirport: z.string().max(10).nullable().optional(), departureAt: z.date().nullable().optional(), departureFlightNumber: z.string().max(20).nullable().optional(), departureAirport: z.string().max(10).nullable().optional(), adminNotes: z.string().max(1000).nullable().optional(), }), ) .mutation(async ({ ctx, input }) => { const { attendingMemberId, ...rest } = input // Strip out undefineds so an upsert update doesn't blow away unset fields. const data: Record = {} for (const [k, v] of Object.entries(rest)) { if (v !== undefined) data[k] = v } const detail = await ctx.prisma.flightDetail.upsert({ where: { attendingMemberId }, create: { attendingMemberId, ...(data as object) }, update: data, }) await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'FLIGHT_DETAIL_UPSERT', entityType: 'FlightDetail', entityId: detail.id, detailsJson: { attendingMemberId }, }) return detail }), /** Toggle PENDING ↔ CONFIRMED on a flight detail. */ setFlightStatus: adminProcedure .input( z.object({ flightDetailId: z.string(), status: z.nativeEnum(FlightDetailStatus), }), ) .mutation(async ({ ctx, input }) => { const detail = await ctx.prisma.flightDetail.update({ where: { id: input.flightDetailId }, data: { status: input.status }, }) await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'FLIGHT_STATUS_SET', entityType: 'FlightDetail', entityId: detail.id, detailsJson: { status: input.status }, }) return detail }), })