Major changes: - Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI - Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions - Sitzungsprotokolle module: meeting protocols, agenda items, task tracking - Verbandsverwaltung module: federation management, member clubs, contacts, fees - Per-account module activation via Modules page toggle - Site Builder: live CMS data in Puck blocks (courses, events, membership registration) - Public registration APIs: course signup, event registration, membership application - Document generation: PDF member cards, Excel reports, HTML labels - Landing page: real Com.BISS content (no filler text) - UX audit fixes: AccountNotFound component, shared status badges, confirm dialog, pagination, duplicate heading removal, emoji→badge replacement, a11y fixes - QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
/**
|
|
* Client-side utility functions for member display.
|
|
*/
|
|
|
|
export function computeAge(dateOfBirth: string | null | undefined): number | null {
|
|
if (!dateOfBirth) return null;
|
|
const birth = new Date(dateOfBirth);
|
|
const today = new Date();
|
|
let age = today.getFullYear() - birth.getFullYear();
|
|
const m = today.getMonth() - birth.getMonth();
|
|
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
|
|
return age;
|
|
}
|
|
|
|
export function computeMembershipYears(entryDate: string | null | undefined): number {
|
|
if (!entryDate) return 0;
|
|
const entry = new Date(entryDate);
|
|
const today = new Date();
|
|
let years = today.getFullYear() - entry.getFullYear();
|
|
const m = today.getMonth() - entry.getMonth();
|
|
if (m < 0 || (m === 0 && today.getDate() < entry.getDate())) years--;
|
|
return Math.max(0, years);
|
|
}
|
|
|
|
export function formatSalutation(salutation: string | null | undefined, firstName: string, lastName: string): string {
|
|
if (salutation) return `${salutation} ${firstName} ${lastName}`;
|
|
return `${firstName} ${lastName}`;
|
|
}
|
|
|
|
export function formatAddress(member: Record<string, unknown>): string {
|
|
const parts: string[] = [];
|
|
if (member.street) {
|
|
let line = String(member.street);
|
|
if (member.house_number) line += ` ${member.house_number}`;
|
|
parts.push(line);
|
|
}
|
|
if (member.street2) parts.push(String(member.street2));
|
|
if (member.postal_code || member.city) {
|
|
parts.push(`${member.postal_code ?? ''} ${member.city ?? ''}`.trim());
|
|
}
|
|
return parts.join(', ');
|
|
}
|
|
|
|
export function formatIban(iban: string | null | undefined): string {
|
|
if (!iban) return '—';
|
|
const cleaned = iban.replace(/\s/g, '');
|
|
return cleaned.replace(/(.{4})/g, '$1 ').trim();
|
|
}
|
|
|
|
export function getMemberStatusColor(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
|
switch (status) {
|
|
case 'active': return 'default';
|
|
case 'inactive': return 'secondary';
|
|
case 'pending': return 'outline';
|
|
case 'resigned':
|
|
case 'excluded':
|
|
case 'deceased': return 'destructive';
|
|
default: return 'secondary';
|
|
}
|
|
}
|
|
|
|
export const STATUS_LABELS: Record<string, string> = {
|
|
active: 'Aktiv',
|
|
inactive: 'Inaktiv',
|
|
pending: 'Ausstehend',
|
|
resigned: 'Ausgetreten',
|
|
excluded: 'Ausgeschlossen',
|
|
deceased: 'Verstorben',
|
|
};
|
|
|
|
export const APPLICATION_STATUS_VARIANT: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
|
submitted: 'outline',
|
|
review: 'secondary',
|
|
approved: 'default',
|
|
rejected: 'destructive',
|
|
};
|
|
|
|
export const APPLICATION_STATUS_LABEL: Record<string, string> = {
|
|
submitted: 'Eingereicht',
|
|
review: 'In Prüfung',
|
|
approved: 'Genehmigt',
|
|
rejected: 'Abgelehnt',
|
|
};
|