Event finance - Finance tab on the event page: P&L summary with a revenue-to-result waterfall, costs and other income in one ledger, and the partner split with payouts. Lifecycle stepper (Selling, Adding costs, Ready to close, Finalized) with what is left to do; closing the books goes through a checklist dialog that freezes the numbers. - Expense modal shows only the fields each calculation type needs, a live preview with the event's real counts, and a category suggested from the description. Date inputs follow the UI language. - Global Finance page: profit per event with outliers clipped and labelled, and a "Ready to close" list of past events whose books are still open. - Calculation service (integer PYG, basis points) with pinned regression scenarios; PYG formatting centralised in lib/money with locale-aware separators. Event team permissions - Per-event members with role presets and requireEventPermission; the header stat "Confirmed" is relabelled "Not checked in yet", which is what it counts. Public sales state - One sales state (online, door, sold out, ended, external, cancelled) for the event page, listings and JSON-LD, with the door price and tenders shown only while people can still pay at the door. Frontend unit tests run with vitest (npm test in frontend/). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
21 lines
1.1 KiB
TypeScript
21 lines
1.1 KiB
TypeScript
// Client-side preview of an expense amount, mirroring
|
|
// backend/src/lib/finance/calculate.ts expenseAmount(). Only used to show
|
|
// "this will be X PYG right now" while typing; the server stays authoritative.
|
|
|
|
import type { CalcType } from '@/lib/api';
|
|
|
|
export interface PreviewContext { ticketsSold: number; checkedIn: number; sales: number }
|
|
|
|
export function previewExpense(
|
|
e: { calcType: CalcType; quantity: number; unitAmount: number; percentBp: number; minimumAmount: number },
|
|
ctx: PreviewContext,
|
|
): { amount: number; count: number } {
|
|
switch (e.calcType) {
|
|
case 'per_ticket_sold': return { amount: e.unitAmount * ctx.ticketsSold, count: ctx.ticketsSold };
|
|
case 'per_checked_in': return { amount: e.unitAmount * ctx.checkedIn, count: ctx.checkedIn };
|
|
case 'percent_of_revenue': return { amount: Math.round((Math.max(0, ctx.sales) * e.percentBp) / 10000), count: 0 };
|
|
case 'minimum_spend': return { amount: Math.max(e.minimumAmount, e.unitAmount * ctx.checkedIn), count: ctx.checkedIn };
|
|
default: return { amount: e.unitAmount * e.quantity, count: e.quantity };
|
|
}
|
|
}
|