Add per-event finance, event team permissions and public door sales state.
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b51c1360e2
commit
e203fb6c74
@@ -0,0 +1,40 @@
|
||||
// The one place PYG amounts are formatted for admin screens (cards, tables,
|
||||
// charts, modal previews). PYG has no decimals. Separators follow the UI
|
||||
// language: English 2,470,000 PYG, Spanish 2.470.000 PYG. Grouping is done by
|
||||
// hand so it does not depend on the browser's locale data. Components use the
|
||||
// useMoney() hook (lib/useMoney.ts) to bind these to the active language.
|
||||
|
||||
export type MoneyLocale = 'en' | 'es';
|
||||
|
||||
const SEP: Record<MoneyLocale, { group: string; decimal: string }> = {
|
||||
en: { group: ',', decimal: '.' },
|
||||
es: { group: '.', decimal: ',' },
|
||||
};
|
||||
|
||||
const sepFor = (locale: string) => SEP[locale === 'en' ? 'en' : 'es'];
|
||||
|
||||
export function formatNumber(n: number, locale: string = 'es'): string {
|
||||
const sign = n < 0 ? '-' : '';
|
||||
return sign + Math.abs(Math.round(n)).toString().replace(/\B(?=(\d{3})+(?!\d))/g, sepFor(locale).group);
|
||||
}
|
||||
|
||||
export function formatPyg(n: number, locale: string = 'es'): string {
|
||||
return `${formatNumber(n, locale)} PYG`;
|
||||
}
|
||||
|
||||
/** Axis labels: 2.470.000 -> 2,5M (es) / 2.5M (en); 850.000 -> 850k. */
|
||||
export function formatPygShort(n: number, locale: string = 'es'): string {
|
||||
const abs = Math.abs(n);
|
||||
const sign = n < 0 ? '-' : '';
|
||||
if (abs >= 1_000_000) {
|
||||
return `${sign}${(abs / 1_000_000).toFixed(abs >= 10_000_000 ? 0 : 1).replace('.', sepFor(locale).decimal)}M`;
|
||||
}
|
||||
if (abs >= 1_000) return `${sign}${Math.round(abs / 1_000)}k`;
|
||||
return `${sign}${abs}`;
|
||||
}
|
||||
|
||||
/** Basis points as a percent string: 290 -> "2.9%" (en) / "2,9%" (es). */
|
||||
export function formatPercent(bp: number, locale: string = 'es'): string {
|
||||
const v = Math.round(bp) / 100;
|
||||
return `${String(v).replace('.', sepFor(locale).decimal)}%`;
|
||||
}
|
||||
Reference in New Issue
Block a user