/** * Mapping for the "2026 Campground Tickets" NocoDB table, matching the 2026 * FluentForms "Tickets 2026" schema. Change titles here if the columns differ. */ export const COL = { id: "Id", name: "Title", // purchaser full name adultNames: "Adult Names", // newline-separated list of adult attendee names email: "Email Address", address: "Address", isDonor: "Is Donor", donorTier: "Donor Tier", // member / donor / "" vouchers: "Vouchers", // Attendee counts by group: adults: "Adults", youth: "Youth 13-16", kids12: "Kids 10-12", kids9: "Kids 5-9", kids4: "Kids 0-4", // free — NOT counted toward the scannable total carParking: "Car Parking", rvParking: "RV Parking", utv: "UTV", iceAccess: "Ice Access", paymentMethod: "Payment Method", ticketType: "Ticket Type", // "" for regular; Guest/Worker/Performer/Volunteer/Speaker for portal comps createdBy: "Created By", // gate-staff name who issued a comp ticket (portal) // Columns this system manages: code: "Ticket Code", redeemed: "Redeemed", submissionKey: "SubmissionKey", lastScanAt: "LastScanAt", iceTotal: "Ice Total", iceRedeemed: "Ice Redeemed", } as const; export type NocoRecord = Record & { Id: number }; function num(v: unknown): number { const n = Number(v); return Number.isFinite(n) ? n : 0; } function bool(v: unknown): boolean { if (typeof v === "boolean") return v; if (typeof v === "number") return v !== 0; if (typeof v === "string") return /^(1|true|yes|y|on)$/i.test(v.trim()); return false; } /** * Total scannable (paid) tickets = adults + youth 13-16. Children 12 and under * (kids 10-12 / 5-9 / 0-4) are admitted free and not counted; charging starts * at age 13. */ export function computeTotal(rec: NocoRecord): number { return num(rec[COL.adults]) + num(rec[COL.youth]); } /** Free children (age 12 and under). */ export function freeKidsCount(rec: NocoRecord): number { return num(rec[COL.kids12]) + num(rec[COL.kids9]) + num(rec[COL.kids4]); } export function computeIceTotal(rec: NocoRecord): number { return num(rec[COL.iceTotal]); } /** Per-group breakdown for display. */ export function ageBreakdown(rec: NocoRecord): { bracket: string; count: number; free: boolean }[] { return [ { bracket: "Adults", count: num(rec[COL.adults]), free: false }, { bracket: "Youth 13-16", count: num(rec[COL.youth]), free: false }, { bracket: "Kids 10-12", count: num(rec[COL.kids12]), free: true }, { bracket: "Kids 5-9", count: num(rec[COL.kids9]), free: true }, { bracket: "Kids 0-4", count: num(rec[COL.kids4]), free: true }, ].filter((b) => b.count > 0); } /** Adult names stored as a newline-separated list. */ export function parseAdultNames(rec: NocoRecord): string[] { const raw = rec[COL.adultNames]; if (Array.isArray(raw)) return raw.map((x) => String(x)).filter(Boolean); if (typeof raw === "string") { return raw .split(/\r?\n/) .map((s) => s.trim()) .filter(Boolean); } return []; } export interface ResourceCount { total: number; redeemed: number; remaining: number; } export interface TicketView { code: string; name: string; email: string; ticketType: string; // "" for regular; Guest/Worker/... for special tickets createdBy: string; // who issued a comp ticket total: number; redeemed: number; remaining: number; ice: ResourceCount; adultNames: string[]; extras: { carParking: boolean; rvParking: boolean; utv: boolean; iceAccess: boolean; isDonor: boolean; donorTier: string; vouchers: number; freeKids: number; // children 12 & under (free admission) }; ages: { bracket: string; count: number; free: boolean }[]; } export function toView(rec: NocoRecord): TicketView { const total = computeTotal(rec); const redeemed = num(rec[COL.redeemed]); const iceTotal = computeIceTotal(rec); const iceRedeemed = num(rec[COL.iceRedeemed]); return { code: String(rec[COL.code] ?? ""), name: String(rec[COL.name] ?? ""), email: String(rec[COL.email] ?? ""), ticketType: String(rec[COL.ticketType] ?? ""), createdBy: String(rec[COL.createdBy] ?? ""), total, redeemed, remaining: Math.max(0, total - redeemed), ice: { total: iceTotal, redeemed: iceRedeemed, remaining: Math.max(0, iceTotal - iceRedeemed), }, adultNames: parseAdultNames(rec), extras: { carParking: bool(rec[COL.carParking]), rvParking: bool(rec[COL.rvParking]), utv: bool(rec[COL.utv]), iceAccess: bool(rec[COL.iceAccess]), isDonor: bool(rec[COL.isDonor]), donorTier: String(rec[COL.donorTier] ?? ""), vouchers: num(rec[COL.vouchers]), freeKids: freeKidsCount(rec), }, ages: ageBreakdown(rec), }; } export { num as toNumber, bool as toBool };