feat: lunch manifest query + CSV export

Adds buildManifest service shared between getManifest and the recap.
CSV escaper handles commas/quotes/newlines for safe spreadsheet import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-29 02:34:24 +02:00
parent d779959e54
commit a671bb853c
3 changed files with 268 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { router, adminProcedure, protectedProcedure } from '../trpc'
import { logAudit } from '../utils/audit'
import { buildManifest } from '../services/lunch-recap'
// ─── Shared zod schemas ──────────────────────────────────────────────────────
@@ -225,6 +226,48 @@ export const lunchRouter = router({
return { ok: true as const }
}),
// ─── Manifest + CSV export ───────────────────────────────────────────────
getManifest: adminProcedure
.input(z.object({ programId: z.string() }))
.query(({ ctx, input }) => buildManifest(ctx.prisma, input.programId)),
exportManifestCsv: adminProcedure
.input(z.object({ programId: z.string() }))
.query(async ({ ctx, input }) => {
const m = await buildManifest(ctx.prisma, input.programId)
const escape = (s: string | null | undefined) => {
const v = s ?? ''
return /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v
}
const lines = [
'Type,Team,Name,Email,Dish,Allergens,Allergen notes',
...m.members.map((row) =>
[
'Member',
escape(row.project?.name),
escape(row.name),
escape(row.email),
escape(row.dish?.name),
escape(row.allergens.join(';')),
escape(row.allergenOther),
].join(','),
),
...m.externals.map((row) =>
[
'External',
escape(row.project?.name),
escape(row.name),
escape(row.email),
escape(row.dish?.name),
escape(row.allergens.join(';')),
escape(row.allergenOther),
].join(','),
),
]
return lines.join('\n')
}),
// ─── Member reads ────────────────────────────────────────────────────────
/**