Add Check-in / Ice / Banquet scan modes
- Ice mode: prepaid ice bags (Ice Total / Ice Redeemed columns) redeemed independently of ticket check-ins; grab all bags at once or some now. - Banquet mode: donor total (online + offline) looked up by the ticket's email via the Donors Master List, with a manual email override. New DonorService + POST /api/banquet. - Redeem generalized over a resource (tickets|ice); audit records ice actions. - App gains a mode selector; webhook maps ice_bags (defaults to ICE_BAGS_DEFAULT when only a boolean ice option is present). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b36d63a6a4
commit
d293e53ee6
15 changed files with 587 additions and 125 deletions
|
|
@ -24,6 +24,8 @@ export const COL = {
|
|||
redeemed: "Redeemed",
|
||||
submissionKey: "SubmissionKey",
|
||||
lastScanAt: "LastScanAt",
|
||||
iceTotal: "Ice Total", // prepaid ice bags
|
||||
iceRedeemed: "Ice Redeemed", // bags picked up
|
||||
} as const;
|
||||
|
||||
/** Age-bracket columns, in order. */
|
||||
|
|
@ -74,6 +76,12 @@ export function ageBreakdown(rec: NocoRecord): { bracket: string; count: number;
|
|||
})).filter((b) => b.count > 0);
|
||||
}
|
||||
|
||||
export interface ResourceCount {
|
||||
total: number;
|
||||
redeemed: number;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
export interface TicketView {
|
||||
code: string;
|
||||
name: string;
|
||||
|
|
@ -81,6 +89,7 @@ export interface TicketView {
|
|||
total: number;
|
||||
redeemed: number;
|
||||
remaining: number;
|
||||
ice: ResourceCount;
|
||||
extras: {
|
||||
carParking: boolean;
|
||||
rvParking: boolean;
|
||||
|
|
@ -91,9 +100,15 @@ export interface TicketView {
|
|||
ages: { bracket: string; count: number; free: boolean }[];
|
||||
}
|
||||
|
||||
export function computeIceTotal(rec: NocoRecord): number {
|
||||
return num(rec[COL.iceTotal]);
|
||||
}
|
||||
|
||||
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] ?? ""),
|
||||
|
|
@ -101,6 +116,11 @@ export function toView(rec: NocoRecord): TicketView {
|
|||
total,
|
||||
redeemed,
|
||||
remaining: Math.max(0, total - redeemed),
|
||||
ice: {
|
||||
total: iceTotal,
|
||||
redeemed: iceRedeemed,
|
||||
remaining: Math.max(0, iceTotal - iceRedeemed),
|
||||
},
|
||||
extras: {
|
||||
carParking: bool(rec[COL.carParking]),
|
||||
rvParking: bool(rec[COL.rvParking]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue