From fd8c2cc32a7ebfca8b728d6e098da56a2e8bc876 Mon Sep 17 00:00:00 2001 From: "T. Zehetbauer" <125989630+4thTomost@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:15:35 +0200 Subject: [PATCH] feat: add cross-organization member search and template cloning functionality --- .../[account]/verband/clubs/[clubId]/page.tsx | 11 +- .../home/[account]/verband/events/page.tsx | 65 + .../home/[account]/verband/members/page.tsx | 92 + .../home/[account]/verband/reporting/page.tsx | 44 + .../home/[account]/verband/templates/page.tsx | 40 + apps/web/i18n/messages/de/verband.json | 306 ++ apps/web/i18n/messages/en/verband.json | 306 ++ apps/web/i18n/request.ts | 1 + apps/web/lib/database.types.ts | 2561 ++++++++++++++++- apps/web/supabase/config.toml | 7 +- .../20260411900001_fischerei_enum_values.sql | 13 + .../migrations/20260412000001_fischerei.sql | 5 +- .../20260413000001_sitzungsprotokolle.sql | 4 +- .../20260413000002_verbandsverwaltung.sql | 4 +- .../20260414000001_account_hierarchy.sql | 53 +- ...20260414000002_hierarchy_functions_rls.sql | 4 +- ...20260414000003_cross_org_member_search.sql | 109 + .../20260414000004_member_transfer.sql | 198 ++ ..._events_consolidated_billing_reporting.sql | 270 ++ .../20260414000006_shared_templates.sql | 282 ++ docker-compose.yml | 70 +- .../src/components/club-contacts-manager.tsx | 10 +- .../src/components/clubs-data-table.tsx | 18 +- .../src/components/create-club-form.tsx | 9 +- .../components/cross-org-member-search.tsx | 595 ++++ .../src/components/hierarchy-events.tsx | 313 ++ .../src/components/hierarchy-report.tsx | 268 ++ .../src/components/hierarchy-tree.tsx | 17 +- .../src/components/index.ts | 4 + .../src/components/shared-templates.tsx | 263 ++ .../src/components/verband-tab-navigation.tsx | 19 +- .../src/schema/verband.schema.ts | 32 + .../src/server/actions/hierarchy-actions.ts | 80 + .../verbandsverwaltung/src/server/api.ts | 483 +++- packages/supabase/src/database.types.ts | 2561 ++++++++++++++++- .../supabase/src/get-supabase-client-keys.ts | 2 +- 36 files changed, 9025 insertions(+), 94 deletions(-) create mode 100644 apps/web/app/[locale]/home/[account]/verband/events/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/verband/members/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/verband/reporting/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/verband/templates/page.tsx create mode 100644 apps/web/i18n/messages/de/verband.json create mode 100644 apps/web/i18n/messages/en/verband.json create mode 100644 apps/web/supabase/migrations/20260411900001_fischerei_enum_values.sql create mode 100644 apps/web/supabase/migrations/20260414000003_cross_org_member_search.sql create mode 100644 apps/web/supabase/migrations/20260414000004_member_transfer.sql create mode 100644 apps/web/supabase/migrations/20260414000005_shared_events_consolidated_billing_reporting.sql create mode 100644 apps/web/supabase/migrations/20260414000006_shared_templates.sql create mode 100644 packages/features/verbandsverwaltung/src/components/cross-org-member-search.tsx create mode 100644 packages/features/verbandsverwaltung/src/components/hierarchy-events.tsx create mode 100644 packages/features/verbandsverwaltung/src/components/hierarchy-report.tsx create mode 100644 packages/features/verbandsverwaltung/src/components/shared-templates.tsx diff --git a/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx b/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx index f6a0d6057..d83c19832 100644 --- a/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx +++ b/apps/web/app/[locale]/home/[account]/verband/clubs/[clubId]/page.tsx @@ -27,7 +27,16 @@ export default async function ClubDetailPage({ params }: Props) { if (!acct) return ; const api = createVerbandApi(client); - const detail = await api.getClubDetail(clubId); + + let detail: Awaited>; + + try { + detail = await api.getClubDetail(clubId); + } catch { + return ; + } + + if (!detail?.club) return ; return ( diff --git a/apps/web/app/[locale]/home/[account]/verband/events/page.tsx b/apps/web/app/[locale]/home/[account]/verband/events/page.tsx new file mode 100644 index 000000000..ac9603255 --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/verband/events/page.tsx @@ -0,0 +1,65 @@ +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { createVerbandApi } from '@kit/verbandsverwaltung/api'; +import { + VerbandTabNavigation, + HierarchyEvents, +} from '@kit/verbandsverwaltung/components'; + +import { AccountNotFound } from '~/components/account-not-found'; +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface PageProps { + params: Promise<{ account: string }>; + searchParams: Promise<{ + status?: string; + sharedOnly?: string; + fromDate?: string; + page?: string; + }>; +} + +export default async function HierarchyEventsPage({ + params, + searchParams, +}: PageProps) { + const { account } = await params; + const sp = await searchParams; + const client = getSupabaseServerClient(); + + const { data: acct } = await client + .from('accounts') + .select('id') + .eq('slug', account) + .single(); + + if (!acct) return ; + + const api = createVerbandApi(client); + const page = Math.max(1, Number(sp.page) || 1); + const pageSize = 25; + + const result = await api.listHierarchyEvents(acct.id, { + status: sp.status, + sharedOnly: sp.sharedOnly === 'true', + fromDate: sp.fromDate, + page, + pageSize, + }); + + return ( + + + + + ); +} diff --git a/apps/web/app/[locale]/home/[account]/verband/members/page.tsx b/apps/web/app/[locale]/home/[account]/verband/members/page.tsx new file mode 100644 index 000000000..231e2ebbe --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/verband/members/page.tsx @@ -0,0 +1,92 @@ +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { createVerbandApi } from '@kit/verbandsverwaltung/api'; +import { + VerbandTabNavigation, + CrossOrgMemberSearch, +} from '@kit/verbandsverwaltung/components'; + +import { AccountNotFound } from '~/components/account-not-found'; +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface PageProps { + params: Promise<{ account: string }>; + searchParams: Promise<{ + q?: string; + status?: string; + accountId?: string; + page?: string; + }>; +} + +export default async function CrossOrgMembersPage({ + params, + searchParams, +}: PageProps) { + const { account } = await params; + const sp = await searchParams; + const client = getSupabaseServerClient(); + + const { data: acct } = await client + .from('accounts') + .select('id') + .eq('slug', account) + .single(); + + if (!acct) return ; + + const api = createVerbandApi(client); + const page = Math.max(1, Number(sp.page) || 1); + const pageSize = 25; + + const [result, hierarchy] = await Promise.all([ + api.searchMembersAcrossHierarchy(acct.id, { + search: sp.q, + status: sp.status, + accountId: sp.accountId, + page, + pageSize, + }), + api.getHierarchyTree(acct.id), + ]); + + // Build flat list of child accounts for the filter dropdown + const childAccounts: Array<{ id: string; name: string }> = []; + + function collectChildren(node: { + id: string; + name: string; + children?: unknown[]; + }) { + if (node.id !== acct!.id) { + childAccounts.push({ id: node.id, name: node.name }); + } + + if (Array.isArray(node.children)) { + for (const child of node.children) { + collectChildren( + child as { id: string; name: string; children?: unknown[] }, + ); + } + } + } + + collectChildren(hierarchy); + + return ( + + + + + ); +} diff --git a/apps/web/app/[locale]/home/[account]/verband/reporting/page.tsx b/apps/web/app/[locale]/home/[account]/verband/reporting/page.tsx new file mode 100644 index 000000000..31320e600 --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/verband/reporting/page.tsx @@ -0,0 +1,44 @@ +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { createVerbandApi } from '@kit/verbandsverwaltung/api'; +import { + VerbandTabNavigation, + HierarchyReport, +} from '@kit/verbandsverwaltung/components'; + +import { AccountNotFound } from '~/components/account-not-found'; +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface PageProps { + params: Promise<{ account: string }>; +} + +export default async function ReportingPage({ params }: PageProps) { + const { account } = await params; + const client = getSupabaseServerClient(); + + const { data: acct } = await client + .from('accounts') + .select('id') + .eq('slug', account) + .single(); + + if (!acct) return ; + + const api = createVerbandApi(client); + + const [summary, report] = await Promise.all([ + api.getHierarchySummary(acct.id), + api.getHierarchyReport(acct.id), + ]); + + return ( + + + + + ); +} diff --git a/apps/web/app/[locale]/home/[account]/verband/templates/page.tsx b/apps/web/app/[locale]/home/[account]/verband/templates/page.tsx new file mode 100644 index 000000000..e2e3b0bd6 --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/verband/templates/page.tsx @@ -0,0 +1,40 @@ +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { createVerbandApi } from '@kit/verbandsverwaltung/api'; +import { + VerbandTabNavigation, + SharedTemplates, +} from '@kit/verbandsverwaltung/components'; + +import { AccountNotFound } from '~/components/account-not-found'; +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface PageProps { + params: Promise<{ account: string }>; +} + +export default async function TemplatesPage({ params }: PageProps) { + const { account } = await params; + const client = getSupabaseServerClient(); + + const { data: acct } = await client + .from('accounts') + .select('id') + .eq('slug', account) + .single(); + + if (!acct) return ; + + const api = createVerbandApi(client); + const templates = await api.listSharedTemplates(acct.id); + + return ( + + + + + ); +} diff --git a/apps/web/i18n/messages/de/verband.json b/apps/web/i18n/messages/de/verband.json new file mode 100644 index 000000000..4705460eb --- /dev/null +++ b/apps/web/i18n/messages/de/verband.json @@ -0,0 +1,306 @@ +{ + "nav": { + "overview": "Übersicht", + "clubs": "Vereine", + "hierarchy": "Hierarchie", + "memberSearch": "Mitgliedersuche", + "events": "Veranstaltungen", + "reporting": "Berichte", + "templates": "Vorlagen", + "statistics": "Statistik", + "settings": "Einstellungen" + }, + "pages": { + "overviewTitle": "Verbandsverwaltung", + "clubsTitle": "Verbandsverwaltung - Vereine", + "hierarchyTitle": "Verbandsverwaltung - Hierarchie", + "hierarchyDescription": "Verwalten Sie die Organisationsstruktur Ihres Verbands", + "memberSearchTitle": "Verbandsverwaltung - Mitgliedersuche", + "memberSearchDescription": "Suchen Sie Mitglieder in allen verknüpften Organisationen", + "eventsTitle": "Verbandsverwaltung - Veranstaltungen", + "eventsDescription": "Veranstaltungen aller verknüpften Organisationen anzeigen und filtern", + "reportingTitle": "Verbandsverwaltung - Berichte", + "reportingDescription": "Aggregierte Berichte und Kennzahlen aller Organisationen im Verband", + "templatesTitle": "Verbandsverwaltung - Vorlagen", + "templatesDescription": "Geteilte Vorlagen aus der Verbandshierarchie klonen und verwenden", + "statisticsTitle": "Verbandsverwaltung - Statistik", + "settingsTitle": "Verbandsverwaltung - Einstellungen" + }, + "common": { + "search": "Suchen", + "filter": "Filtern", + "cancel": "Abbrechen", + "save": "Speichern", + "delete": "Löschen", + "edit": "Bearbeiten", + "add": "Hinzufügen", + "create": "Erstellen", + "back": "Zurück", + "next": "Weiter", + "saving": "Wird gespeichert...", + "name": "Name", + "email": "E-Mail", + "phone": "Telefon", + "location": "Ort", + "status": "Status", + "actions": "Aktionen", + "action": "Aktion", + "type": "Typ", + "date": "Datum", + "description": "Beschreibung", + "noEntries": "Keine Einträge vorhanden.", + "allTypes": "Alle Typen", + "allStatuses": "Alle Status", + "allOrganizations": "Alle Organisationen", + "organization": "Organisation", + "archived": "Archiviert", + "showArchived": "Archivierte anzeigen", + "hideArchived": "Archivierte ausblenden" + }, + "pagination": { + "page": "Seite {page} von {totalPages} ({total} Einträge)", + "back": "Zurück", + "next": "Weiter" + }, + "dashboard": { + "title": "Verbandsverwaltung – Übersicht", + "subtitle": "Vereine, Beiträge, Kontakte und Aufgaben verwalten", + "activeClubs": "Aktive Vereine", + "totalMembers": "Gesamtmitglieder", + "openFees": "Offene Beiträge", + "invoiceCount": "{count} Rechnungen", + "openTasks": "Offene Aufgaben", + "clubTypes": "Vereinstypen", + "archivedClubs": "Archivierte Vereine", + "clubsWithoutContact": "Vereine ohne Ansprechpartner", + "allClubsWithContact": "Alle Vereine haben mindestens einen Ansprechpartner.", + "addContact": "Kontakt hinzufügen" + }, + "clubs": { + "searchPlaceholder": "Verein suchen...", + "newClub": "Neuer Verein", + "title": "Vereine ({count})", + "noClubs": "Keine Vereine vorhanden", + "createFirst": "Erstellen Sie Ihren ersten Verein, um loszulegen.", + "members": "Mitglieder", + "contact": "Kontakt", + "founded": "Gegr. {year}" + }, + "clubForm": { + "basicData": "Grunddaten", + "name": "Name *", + "shortName": "Kurzname", + "associationType": "Vereinstyp", + "noType": "— Kein Typ —", + "foundingYear": "Gründungsjahr", + "memberCount": "Mitgliederanzahl", + "address": "Adresse", + "street": "Straße", + "zip": "PLZ", + "city": "Ort", + "website": "Website", + "bankData": "Bankdaten", + "accountHolder": "Kontoinhaber", + "iban": "IBAN", + "bic": "BIC", + "updateClub": "Verein aktualisieren", + "createClub": "Verein erstellen", + "clubUpdated": "Verein aktualisiert", + "clubCreated": "Verein erstellt", + "errorSaving": "Fehler beim Speichern" + }, + "contacts": { + "title": "Ansprechpartner", + "addContact": "Kontakt hinzufügen", + "noContacts": "Keine Ansprechpartner vorhanden.", + "firstName": "Vorname *", + "lastName": "Nachname *", + "role": "Funktion", + "update": "Aktualisieren", + "created": "Kontakt erstellt", + "updated": "Kontakt aktualisiert", + "deleted": "Kontakt gelöscht", + "errorCreating": "Fehler beim Erstellen", + "errorUpdating": "Fehler beim Aktualisieren", + "errorDeleting": "Fehler beim Löschen" + }, + "billing": { + "title": "Beitragsabrechnungen", + "feeType": "Beitragsart", + "year": "Jahr", + "amount": "Betrag", + "dueDate": "Fällig", + "paymentMethod": "Zahlung", + "noBillings": "Keine Beitragsabrechnungen vorhanden.", + "showAll": "Alle anzeigen", + "showOpen": "Nur offene", + "markAsPaid": "Als bezahlt markieren", + "paid": "Beitrag als bezahlt markiert", + "deleted": "Beitragsabrechnung gelöscht", + "errorUpdating": "Fehler beim Aktualisieren", + "errorDeleting": "Fehler beim Löschen" + }, + "notes": { + "title": "Notizen & Aufgaben ({count} offen)", + "noNotes": "Keine Notizen vorhanden.", + "dueDate": "Fällig: {date}", + "completed": "Erledigt ({count})", + "markDone": "Als erledigt markieren", + "taskCompleted": "Aufgabe erledigt", + "noteDeleted": "Notiz gelöscht", + "errorUpdating": "Fehler beim Aktualisieren", + "errorDeleting": "Fehler beim Löschen" + }, + "hierarchy": { + "structure": "Organisationsstruktur", + "directChildren": "Direkte Unterverbände", + "totalOrganizations": "Organisationen gesamt", + "availableToLink": "Verfügbar zum Verknüpfen", + "addOrganization": "Organisation hinzufügen", + "availableOrganizations": "Verfügbare Organisationen", + "selectOrganization": "Organisation auswählen...", + "link": "Verknüpfen", + "linking": "Wird verknüpft...", + "linked": "Organisation erfolgreich verknüpft", + "unlinkTitle": "Verknüpfung lösen", + "unlinked": "Verknüpfung gelöst", + "directCount": "{count} direkt", + "rootLevel": "Dachverband", + "subLevel": "Unterverband", + "clubLevel": "Verein", + "errorLinking": "Fehler beim Verknüpfen der Organisation", + "errorUnlinking": "Fehler beim Entfernen der Verknüpfung" + }, + "memberSearch": { + "searchPlaceholder": "Name, E-Mail oder Mitgliedsnr. suchen...", + "title": "Mitglieder ({count})", + "noMembers": "Keine Mitglieder gefunden", + "tryOtherSearch": "Versuchen Sie einen anderen Suchbegriff.", + "noMembersInHierarchy": "In den verknüpften Organisationen sind noch keine Mitglieder vorhanden.", + "joinDate": "Eintritt", + "memberStatus": { + "active": "Aktiv", + "inactive": "Inaktiv", + "pending": "Ausstehend", + "resigned": "Ausgetreten", + "excluded": "Ausgeschlossen", + "deceased": "Verstorben" + } + }, + "transfer": { + "title": "Mitglied transferieren", + "description": "{name} wird von {source} in eine andere Organisation verschoben.", + "loadingPreview": "Lade Transfervorschau...", + "targetOrganization": "Zielorganisation", + "selectTarget": "Organisation auswählen...", + "keepSepaData": "SEPA-Bankdaten (IBAN/BIC) übernehmen", + "keepSepaHelp": "Bankverbindung wird übernommen, Mandat muss im Zielverein neu bestätigt werden.", + "reason": "Grund (optional)", + "reasonPlaceholder": "z.B. Umzug, Vereinswechsel...", + "transferring": "Wird transferiert...", + "confirm": "Transferieren", + "transferred": "Mitglied erfolgreich transferiert", + "errorTransfer": "Fehler beim Transfer", + "activeEnrollments": "{count} aktive Kurseinschreibung(en)", + "retained": "bleibt erhalten", + "openInvoices": "{count} offene Rechnung(en)", + "remainsAtSource": "verbleibt beim Quellverein", + "activeMandates": "{count} aktive(s) SEPA-Mandat(e)", + "willReset": "wird zurückgesetzt", + "newsletters": "{count} Newsletter-Abonnement(s)", + "resetSection": "Wird zurückgesetzt:", + "memberNumberReset": "Mitgliedsnr. #{number} — Neuvergabe im Zielverein nötig", + "duesCategoryReset": "Beitragskategorie — muss im Zielverein neu zugewiesen werden", + "sepaStatusReset": "SEPA-Mandatstatus → \"ausstehend\" (Neubestätigung nötig)", + "noSideEffects": "Keine aktiven Verknüpfungen gefunden", + "noSideEffectsHelp": "Transfer kann ohne Seiteneffekte durchgeführt werden." + }, + "events": { + "title": "Veranstaltungen ({count})", + "noEvents": "Keine Veranstaltungen gefunden", + "tryOtherFilters": "Versuchen Sie andere Filterkriterien.", + "noEventsInHierarchy": "In den verknüpften Organisationen sind noch keine Veranstaltungen vorhanden.", + "event": "Veranstaltung", + "capacity": "Kapazität", + "fee": "Gebühr", + "shared": "Geteilt", + "sharedOnly": "Nur geteilte", + "eventStatus": { + "planned": "Geplant", + "open": "Offen", + "full": "Ausgebucht", + "running": "Laufend", + "completed": "Abgeschlossen", + "cancelled": "Abgesagt" + } + }, + "reporting": { + "perOrganization": "Bericht pro Organisation", + "noOrganizations": "Keine Organisationen vorhanden", + "hierarchyEmpty": "Die Hierarchie enthält noch keine Organisationen.", + "organizations": "Organisationen", + "activeMembers": "Aktive Mitglieder", + "ofTotal": "von {total} gesamt", + "newThisYear": "Neue Mitglieder (Jahr)", + "upcomingEvents": "Anstehende Termine", + "activeCourses": "Aktive Kurse", + "openInvoices": "Offene Rechnungen", + "invoiceCount": "{count} Rechnungen", + "level": "Ebene", + "activeMembersShort": "Aktive Mitgl.", + "totalShort": "Gesamt", + "newYearShort": "Neu (Jahr)", + "courses": "Kurse", + "eventsShort": "Termine", + "openInvoicesShort": "Offene Rechn.", + "openAmount": "Offener Betrag" + }, + "templates": { + "sharedTemplates": "Geteilte Vorlagen", + "noTemplates": "Keine geteilten Vorlagen vorhanden", + "templatesHelp": "Vorlagen, die von anderen Organisationen in Ihrer Hierarchie geteilt werden, erscheinen hier.", + "filterAll": "Alle", + "filterNewsletter": "Newsletter", + "filterDocument": "Dokumente", + "templateType": "Template-Typ", + "created": "Erstellt", + "clone": "Klonen", + "cloneTitle": "Vorlage klonen", + "cloneDescription": "Erstellen Sie eine Kopie der Vorlage \"{name}\" in Ihrer Organisation.", + "cloneName": "Name der Kopie", + "cloneNamePlaceholder": "Name der neuen Vorlage", + "cloning": "Wird geklont...", + "cloneConfirm": "Vorlage klonen", + "cloned": "Vorlage wurde erfolgreich geklont", + "errorCloning": "Fehler beim Klonen der Vorlage", + "newsletter": "Newsletter", + "document": "Dokument", + "templateTypes": { + "generic": "Allgemein", + "member_card": "Mitgliedsausweis", + "invoice": "Rechnung", + "receipt": "Quittung", + "certificate": "Urkunde", + "letter": "Brief", + "label": "Etikett", + "report": "Bericht" + } + }, + "settings": { + "title": "Einstellungen", + "subtitle": "Funktionen, Vereinstypen und Beitragsarten verwalten", + "roles": "Funktionen (Rollen)", + "types": "Vereinstypen", + "feeTypes": "Beitragsarten", + "descriptionOptional": "Beschreibung (optional)" + }, + "statistics": { + "title": "Statistik", + "subtitle": "Entwicklung der Mitgliedsvereine und Gesamtmitglieder im Zeitverlauf", + "clubDevelopment": "Vereinsentwicklung", + "memberDevelopment": "Mitgliederentwicklung", + "clubsLabel": "Vereine", + "membersLabel": "Mitglieder", + "helpText": "Die Statistiken werden automatisch aus den Vereinsdaten und der Verbandshistorie berechnet." + } +} diff --git a/apps/web/i18n/messages/en/verband.json b/apps/web/i18n/messages/en/verband.json new file mode 100644 index 000000000..7e799297e --- /dev/null +++ b/apps/web/i18n/messages/en/verband.json @@ -0,0 +1,306 @@ +{ + "nav": { + "overview": "Overview", + "clubs": "Clubs", + "hierarchy": "Hierarchy", + "memberSearch": "Member Search", + "events": "Events", + "reporting": "Reports", + "templates": "Templates", + "statistics": "Statistics", + "settings": "Settings" + }, + "pages": { + "overviewTitle": "Association Management", + "clubsTitle": "Association Management - Clubs", + "hierarchyTitle": "Association Management - Hierarchy", + "hierarchyDescription": "Manage the organizational structure of your association", + "memberSearchTitle": "Association Management - Member Search", + "memberSearchDescription": "Search members across all linked organizations", + "eventsTitle": "Association Management - Events", + "eventsDescription": "View and filter events across all linked organizations", + "reportingTitle": "Association Management - Reports", + "reportingDescription": "Aggregated reports and metrics across all organizations", + "templatesTitle": "Association Management - Templates", + "templatesDescription": "Clone and use shared templates from the association hierarchy", + "statisticsTitle": "Association Management - Statistics", + "settingsTitle": "Association Management - Settings" + }, + "common": { + "search": "Search", + "filter": "Filter", + "cancel": "Cancel", + "save": "Save", + "delete": "Delete", + "edit": "Edit", + "add": "Add", + "create": "Create", + "back": "Back", + "next": "Next", + "saving": "Saving...", + "name": "Name", + "email": "Email", + "phone": "Phone", + "location": "Location", + "status": "Status", + "actions": "Actions", + "action": "Action", + "type": "Type", + "date": "Date", + "description": "Description", + "noEntries": "No entries found.", + "allTypes": "All Types", + "allStatuses": "All Statuses", + "allOrganizations": "All Organizations", + "organization": "Organization", + "archived": "Archived", + "showArchived": "Show Archived", + "hideArchived": "Hide Archived" + }, + "pagination": { + "page": "Page {page} of {totalPages} ({total} entries)", + "back": "Back", + "next": "Next" + }, + "dashboard": { + "title": "Association Management – Overview", + "subtitle": "Manage clubs, fees, contacts, and tasks", + "activeClubs": "Active Clubs", + "totalMembers": "Total Members", + "openFees": "Open Fees", + "invoiceCount": "{count} invoices", + "openTasks": "Open Tasks", + "clubTypes": "Club Types", + "archivedClubs": "Archived Clubs", + "clubsWithoutContact": "Clubs Without Contact", + "allClubsWithContact": "All clubs have at least one contact person.", + "addContact": "Add Contact" + }, + "clubs": { + "searchPlaceholder": "Search clubs...", + "newClub": "New Club", + "title": "Clubs ({count})", + "noClubs": "No clubs found", + "createFirst": "Create your first club to get started.", + "members": "Members", + "contact": "Contact", + "founded": "Est. {year}" + }, + "clubForm": { + "basicData": "Basic Data", + "name": "Name *", + "shortName": "Short Name", + "associationType": "Association Type", + "noType": "— No Type —", + "foundingYear": "Founding Year", + "memberCount": "Member Count", + "address": "Address", + "street": "Street", + "zip": "ZIP", + "city": "City", + "website": "Website", + "bankData": "Bank Details", + "accountHolder": "Account Holder", + "iban": "IBAN", + "bic": "BIC", + "updateClub": "Update Club", + "createClub": "Create Club", + "clubUpdated": "Club updated", + "clubCreated": "Club created", + "errorSaving": "Error saving" + }, + "contacts": { + "title": "Contacts", + "addContact": "Add Contact", + "noContacts": "No contacts found.", + "firstName": "First Name *", + "lastName": "Last Name *", + "role": "Role", + "update": "Update", + "created": "Contact created", + "updated": "Contact updated", + "deleted": "Contact deleted", + "errorCreating": "Error creating contact", + "errorUpdating": "Error updating contact", + "errorDeleting": "Error deleting contact" + }, + "billing": { + "title": "Fee Billings", + "feeType": "Fee Type", + "year": "Year", + "amount": "Amount", + "dueDate": "Due Date", + "paymentMethod": "Payment", + "noBillings": "No fee billings found.", + "showAll": "Show All", + "showOpen": "Open Only", + "markAsPaid": "Mark as Paid", + "paid": "Fee marked as paid", + "deleted": "Fee billing deleted", + "errorUpdating": "Error updating", + "errorDeleting": "Error deleting" + }, + "notes": { + "title": "Notes & Tasks ({count} open)", + "noNotes": "No notes found.", + "dueDate": "Due: {date}", + "completed": "Completed ({count})", + "markDone": "Mark as done", + "taskCompleted": "Task completed", + "noteDeleted": "Note deleted", + "errorUpdating": "Error updating", + "errorDeleting": "Error deleting" + }, + "hierarchy": { + "structure": "Organization Structure", + "directChildren": "Direct Sub-associations", + "totalOrganizations": "Total Organizations", + "availableToLink": "Available to Link", + "addOrganization": "Add Organization", + "availableOrganizations": "Available Organizations", + "selectOrganization": "Select organization...", + "link": "Link", + "linking": "Linking...", + "linked": "Organization linked successfully", + "unlinkTitle": "Unlink", + "unlinked": "Link removed", + "directCount": "{count} direct", + "rootLevel": "Federation", + "subLevel": "Sub-association", + "clubLevel": "Club", + "errorLinking": "Error linking organization", + "errorUnlinking": "Error removing link" + }, + "memberSearch": { + "searchPlaceholder": "Search name, email, or member no...", + "title": "Members ({count})", + "noMembers": "No members found", + "tryOtherSearch": "Try a different search term.", + "noMembersInHierarchy": "No members found in linked organizations.", + "joinDate": "Joined", + "memberStatus": { + "active": "Active", + "inactive": "Inactive", + "pending": "Pending", + "resigned": "Resigned", + "excluded": "Excluded", + "deceased": "Deceased" + } + }, + "transfer": { + "title": "Transfer Member", + "description": "{name} will be moved from {source} to another organization.", + "loadingPreview": "Loading transfer preview...", + "targetOrganization": "Target Organization", + "selectTarget": "Select organization...", + "keepSepaData": "Keep SEPA bank data (IBAN/BIC)", + "keepSepaHelp": "Bank details will be kept, mandate must be re-confirmed in the target organization.", + "reason": "Reason (optional)", + "reasonPlaceholder": "e.g. Relocation, club change...", + "transferring": "Transferring...", + "confirm": "Transfer", + "transferred": "Member transferred successfully", + "errorTransfer": "Error during transfer", + "activeEnrollments": "{count} active course enrollment(s)", + "retained": "retained", + "openInvoices": "{count} open invoice(s)", + "remainsAtSource": "remains at source organization", + "activeMandates": "{count} active SEPA mandate(s)", + "willReset": "will be reset", + "newsletters": "{count} newsletter subscription(s)", + "resetSection": "Will be reset:", + "memberNumberReset": "Member no. #{number} — reassignment needed in target org", + "duesCategoryReset": "Dues category — must be reassigned in target organization", + "sepaStatusReset": "SEPA mandate status → \"pending\" (re-confirmation needed)", + "noSideEffects": "No active relationships found", + "noSideEffectsHelp": "Transfer can be performed without side effects." + }, + "events": { + "title": "Events ({count})", + "noEvents": "No events found", + "tryOtherFilters": "Try different filter criteria.", + "noEventsInHierarchy": "No events found in linked organizations.", + "event": "Event", + "capacity": "Capacity", + "fee": "Fee", + "shared": "Shared", + "sharedOnly": "Shared Only", + "eventStatus": { + "planned": "Planned", + "open": "Open", + "full": "Full", + "running": "Running", + "completed": "Completed", + "cancelled": "Cancelled" + } + }, + "reporting": { + "perOrganization": "Report per Organization", + "noOrganizations": "No organizations found", + "hierarchyEmpty": "The hierarchy does not contain any organizations yet.", + "organizations": "Organizations", + "activeMembers": "Active Members", + "ofTotal": "of {total} total", + "newThisYear": "New Members (Year)", + "upcomingEvents": "Upcoming Events", + "activeCourses": "Active Courses", + "openInvoices": "Open Invoices", + "invoiceCount": "{count} invoices", + "level": "Level", + "activeMembersShort": "Active", + "totalShort": "Total", + "newYearShort": "New (Year)", + "courses": "Courses", + "eventsShort": "Events", + "openInvoicesShort": "Open Inv.", + "openAmount": "Open Amount" + }, + "templates": { + "sharedTemplates": "Shared Templates", + "noTemplates": "No shared templates found", + "templatesHelp": "Templates shared by other organizations in your hierarchy appear here.", + "filterAll": "All", + "filterNewsletter": "Newsletter", + "filterDocument": "Documents", + "templateType": "Template Type", + "created": "Created", + "clone": "Clone", + "cloneTitle": "Clone Template", + "cloneDescription": "Create a copy of template \"{name}\" in your organization.", + "cloneName": "Copy Name", + "cloneNamePlaceholder": "Name for the new template", + "cloning": "Cloning...", + "cloneConfirm": "Clone Template", + "cloned": "Template cloned successfully", + "errorCloning": "Error cloning template", + "newsletter": "Newsletter", + "document": "Document", + "templateTypes": { + "generic": "General", + "member_card": "Member Card", + "invoice": "Invoice", + "receipt": "Receipt", + "certificate": "Certificate", + "letter": "Letter", + "label": "Label", + "report": "Report" + } + }, + "settings": { + "title": "Settings", + "subtitle": "Manage roles, association types, and fee types", + "roles": "Roles", + "types": "Association Types", + "feeTypes": "Fee Types", + "descriptionOptional": "Description (optional)" + }, + "statistics": { + "title": "Statistics", + "subtitle": "Club and member development over time", + "clubDevelopment": "Club Development", + "memberDevelopment": "Member Development", + "clubsLabel": "Clubs", + "membersLabel": "Members", + "helpText": "Statistics are automatically calculated from club data and association history." + } +} diff --git a/apps/web/i18n/request.ts b/apps/web/i18n/request.ts index 5f367e388..ee1f797d0 100644 --- a/apps/web/i18n/request.ts +++ b/apps/web/i18n/request.ts @@ -20,6 +20,7 @@ const namespaces = [ 'billing', 'marketing', 'cms', + 'verband', ] as const; const isDevelopment = process.env.NODE_ENV === 'development'; diff --git a/apps/web/lib/database.types.ts b/apps/web/lib/database.types.ts index 66b5fe309..8a49b1cc1 100644 --- a/apps/web/lib/database.types.ts +++ b/apps/web/lib/database.types.ts @@ -136,6 +136,7 @@ export type Database = { id: string is_personal_account: boolean name: string + parent_account_id: string | null picture_url: string | null primary_owner_user_id: string public_data: Json @@ -150,6 +151,7 @@ export type Database = { id?: string is_personal_account?: boolean name: string + parent_account_id?: string | null picture_url?: string | null primary_owner_user_id?: string public_data?: Json @@ -164,6 +166,7 @@ export type Database = { id?: string is_personal_account?: boolean name?: string + parent_account_id?: string | null picture_url?: string | null primary_owner_user_id?: string public_data?: Json @@ -171,7 +174,29 @@ export type Database = { updated_at?: string | null updated_by?: string | null } - Relationships: [] + Relationships: [ + { + foreignKeyName: "accounts_parent_account_id_fkey" + columns: ["parent_account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "accounts_parent_account_id_fkey" + columns: ["parent_account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "accounts_parent_account_id_fkey" + columns: ["parent_account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] } accounts_memberships: { Row: { @@ -232,6 +257,123 @@ export type Database = { }, ] } + association_history: { + Row: { + account_id: string + club_id: string | null + created_at: string + created_by: string | null + description: string + event_date: string + event_type: string + id: string + new_value: string | null + old_value: string | null + } + Insert: { + account_id: string + club_id?: string | null + created_at?: string + created_by?: string | null + description: string + event_date?: string + event_type?: string + id?: string + new_value?: string | null + old_value?: string | null + } + Update: { + account_id?: string + club_id?: string | null + created_at?: string + created_by?: string | null + description?: string + event_date?: string + event_type?: string + id?: string + new_value?: string | null + old_value?: string | null + } + Relationships: [ + { + foreignKeyName: "association_history_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_history_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_history_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_history_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + ] + } + association_types: { + Row: { + account_id: string + created_at: string + description: string | null + id: string + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + description?: string | null + id?: string + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + description?: string | null + id?: string + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "association_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } audit_log: { Row: { account_id: string @@ -429,6 +571,518 @@ export type Database = { }, ] } + catch_books: { + Row: { + account_id: string + card_numbers: string | null + created_at: string + created_by: string | null + fishing_days_count: number + id: string + is_checked: boolean + is_empty: boolean + is_fly_fisher: boolean + is_hejfish: boolean + is_submitted: boolean + member_birth_date: string | null + member_id: string + member_name: string | null + not_fished: boolean + remarks: string | null + status: Database["public"]["Enums"]["catch_book_status"] + submitted_at: string | null + total_fish_caught: number + updated_at: string + updated_by: string | null + verification: + | Database["public"]["Enums"]["catch_book_verification"] + | null + year: number + } + Insert: { + account_id: string + card_numbers?: string | null + created_at?: string + created_by?: string | null + fishing_days_count?: number + id?: string + is_checked?: boolean + is_empty?: boolean + is_fly_fisher?: boolean + is_hejfish?: boolean + is_submitted?: boolean + member_birth_date?: string | null + member_id: string + member_name?: string | null + not_fished?: boolean + remarks?: string | null + status?: Database["public"]["Enums"]["catch_book_status"] + submitted_at?: string | null + total_fish_caught?: number + updated_at?: string + updated_by?: string | null + verification?: + | Database["public"]["Enums"]["catch_book_verification"] + | null + year: number + } + Update: { + account_id?: string + card_numbers?: string | null + created_at?: string + created_by?: string | null + fishing_days_count?: number + id?: string + is_checked?: boolean + is_empty?: boolean + is_fly_fisher?: boolean + is_hejfish?: boolean + is_submitted?: boolean + member_birth_date?: string | null + member_id?: string + member_name?: string | null + not_fished?: boolean + remarks?: string | null + status?: Database["public"]["Enums"]["catch_book_status"] + submitted_at?: string | null + total_fish_caught?: number + updated_at?: string + updated_by?: string | null + verification?: + | Database["public"]["Enums"]["catch_book_verification"] + | null + year?: number + } + Relationships: [ + { + foreignKeyName: "catch_books_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catch_books_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catch_books_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catch_books_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + ] + } + catches: { + Row: { + catch_book_id: string + catch_date: string + competition_id: string | null + competition_participant_id: string | null + created_at: string + gender: Database["public"]["Enums"]["fish_gender"] | null + has_error: boolean + hejfish_id: string | null + id: string + is_empty_entry: boolean + length_cm: number | null + member_id: string | null + permit_id: string | null + quantity: number + remarks: string | null + size_category: + | Database["public"]["Enums"]["fish_size_category"] + | null + species_id: string + water_id: string | null + weight_g: number | null + } + Insert: { + catch_book_id: string + catch_date: string + competition_id?: string | null + competition_participant_id?: string | null + created_at?: string + gender?: Database["public"]["Enums"]["fish_gender"] | null + has_error?: boolean + hejfish_id?: string | null + id?: string + is_empty_entry?: boolean + length_cm?: number | null + member_id?: string | null + permit_id?: string | null + quantity?: number + remarks?: string | null + size_category?: + | Database["public"]["Enums"]["fish_size_category"] + | null + species_id: string + water_id?: string | null + weight_g?: number | null + } + Update: { + catch_book_id?: string + catch_date?: string + competition_id?: string | null + competition_participant_id?: string | null + created_at?: string + gender?: Database["public"]["Enums"]["fish_gender"] | null + has_error?: boolean + hejfish_id?: string | null + id?: string + is_empty_entry?: boolean + length_cm?: number | null + member_id?: string | null + permit_id?: string | null + quantity?: number + remarks?: string | null + size_category?: + | Database["public"]["Enums"]["fish_size_category"] + | null + species_id?: string + water_id?: string | null + weight_g?: number | null + } + Relationships: [ + { + foreignKeyName: "catches_catch_book_id_fkey" + columns: ["catch_book_id"] + isOneToOne: false + referencedRelation: "catch_books" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_permit_id_fkey" + columns: ["permit_id"] + isOneToOne: false + referencedRelation: "fishing_permits" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_species_id_fkey" + columns: ["species_id"] + isOneToOne: false + referencedRelation: "fish_species" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk_catches_competition" + columns: ["competition_id"] + isOneToOne: false + referencedRelation: "competitions" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk_catches_competition_participant" + columns: ["competition_participant_id"] + isOneToOne: false + referencedRelation: "competition_participants" + referencedColumns: ["id"] + }, + ] + } + club_contacts: { + Row: { + club_id: string + created_at: string + email: string | null + first_name: string + id: string + is_active: boolean + last_name: string + mobile: string | null + phone: string | null + role_id: string | null + updated_at: string + valid_from: string | null + valid_until: string | null + } + Insert: { + club_id: string + created_at?: string + email?: string | null + first_name: string + id?: string + is_active?: boolean + last_name: string + mobile?: string | null + phone?: string | null + role_id?: string | null + updated_at?: string + valid_from?: string | null + valid_until?: string | null + } + Update: { + club_id?: string + created_at?: string + email?: string | null + first_name?: string + id?: string + is_active?: boolean + last_name?: string + mobile?: string | null + phone?: string | null + role_id?: string | null + updated_at?: string + valid_from?: string | null + valid_until?: string | null + } + Relationships: [ + { + foreignKeyName: "club_contacts_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_contacts_role_id_fkey" + columns: ["role_id"] + isOneToOne: false + referencedRelation: "club_roles" + referencedColumns: ["id"] + }, + ] + } + club_fee_billings: { + Row: { + amount: number + billing_period: string | null + billing_year: number + club_id: string + created_at: string + fee_type_id: string + id: string + invoice_date: string | null + invoice_number: string | null + member_count_at_billing: number | null + paid_amount: number | null + paid_date: string | null + remarks: string | null + status: string + updated_at: string + } + Insert: { + amount?: number + billing_period?: string | null + billing_year: number + club_id: string + created_at?: string + fee_type_id: string + id?: string + invoice_date?: string | null + invoice_number?: string | null + member_count_at_billing?: number | null + paid_amount?: number | null + paid_date?: string | null + remarks?: string | null + status?: string + updated_at?: string + } + Update: { + amount?: number + billing_period?: string | null + billing_year?: number + club_id?: string + created_at?: string + fee_type_id?: string + id?: string + invoice_date?: string | null + invoice_number?: string | null + member_count_at_billing?: number | null + paid_amount?: number | null + paid_date?: string | null + remarks?: string | null + status?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "club_fee_billings_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_fee_billings_fee_type_id_fkey" + columns: ["fee_type_id"] + isOneToOne: false + referencedRelation: "club_fee_types" + referencedColumns: ["id"] + }, + ] + } + club_fee_types: { + Row: { + account_id: string + created_at: string + default_amount: number + description: string | null + id: string + is_active: boolean + is_per_member: boolean + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + default_amount?: number + description?: string | null + id?: string + is_active?: boolean + is_per_member?: boolean + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + default_amount?: number + description?: string | null + id?: string + is_active?: boolean + is_per_member?: boolean + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "club_fee_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_fee_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_fee_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + club_notes: { + Row: { + club_id: string + content: string + created_at: string + created_by: string | null + id: string + note_date: string + title: string | null + } + Insert: { + club_id: string + content: string + created_at?: string + created_by?: string | null + id?: string + note_date?: string + title?: string | null + } + Update: { + club_id?: string + content?: string + created_at?: string + created_by?: string | null + id?: string + note_date?: string + title?: string | null + } + Relationships: [ + { + foreignKeyName: "club_notes_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + ] + } + club_roles: { + Row: { + account_id: string + created_at: string + description: string | null + id: string + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + description?: string | null + id?: string + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + description?: string | null + id?: string + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "club_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } cms_files: { Row: { account_id: string @@ -563,6 +1217,243 @@ export type Database = { }, ] } + competition_categories: { + Row: { + account_id: string + created_at: string + id: string + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + id?: string + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + id?: string + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "competition_categories_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_categories_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_categories_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + competition_participants: { + Row: { + address: string | null + birth_date: string | null + category_id: string | null + competition_id: string + created_at: string + email: string | null + heaviest_catch_g: number + id: string + longest_catch_cm: number + member_id: string | null + participant_name: string + participated: boolean + phone: string | null + total_catch_count: number + total_length_cm: number + total_weight_g: number + } + Insert: { + address?: string | null + birth_date?: string | null + category_id?: string | null + competition_id: string + created_at?: string + email?: string | null + heaviest_catch_g?: number + id?: string + longest_catch_cm?: number + member_id?: string | null + participant_name: string + participated?: boolean + phone?: string | null + total_catch_count?: number + total_length_cm?: number + total_weight_g?: number + } + Update: { + address?: string | null + birth_date?: string | null + category_id?: string | null + competition_id?: string + created_at?: string + email?: string | null + heaviest_catch_g?: number + id?: string + longest_catch_cm?: number + member_id?: string | null + participant_name?: string + participated?: boolean + phone?: string | null + total_catch_count?: number + total_length_cm?: number + total_weight_g?: number + } + Relationships: [ + { + foreignKeyName: "competition_participants_category_id_fkey" + columns: ["category_id"] + isOneToOne: false + referencedRelation: "competition_categories" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_participants_competition_id_fkey" + columns: ["competition_id"] + isOneToOne: false + referencedRelation: "competitions" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_participants_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + ] + } + competitions: { + Row: { + account_id: string + competition_date: string + created_at: string + created_by: string | null + event_id: string | null + id: string + max_participants: number | null + name: string + permit_id: string | null + result_count_count: number | null + result_count_length: number | null + result_count_weight: number | null + score_by_count: boolean + score_by_heaviest: boolean + score_by_longest: boolean + score_by_total_length: boolean + score_by_total_weight: boolean + separate_member_guest_scoring: boolean + updated_at: string + water_id: string | null + } + Insert: { + account_id: string + competition_date: string + created_at?: string + created_by?: string | null + event_id?: string | null + id?: string + max_participants?: number | null + name: string + permit_id?: string | null + result_count_count?: number | null + result_count_length?: number | null + result_count_weight?: number | null + score_by_count?: boolean + score_by_heaviest?: boolean + score_by_longest?: boolean + score_by_total_length?: boolean + score_by_total_weight?: boolean + separate_member_guest_scoring?: boolean + updated_at?: string + water_id?: string | null + } + Update: { + account_id?: string + competition_date?: string + created_at?: string + created_by?: string | null + event_id?: string | null + id?: string + max_participants?: number | null + name?: string + permit_id?: string | null + result_count_count?: number | null + result_count_length?: number | null + result_count_weight?: number | null + score_by_count?: boolean + score_by_heaviest?: boolean + score_by_longest?: boolean + score_by_total_length?: boolean + score_by_total_weight?: boolean + separate_member_guest_scoring?: boolean + updated_at?: string + water_id?: string | null + } + Relationships: [ + { + foreignKeyName: "competitions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_event_id_fkey" + columns: ["event_id"] + isOneToOne: false + referencedRelation: "events" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_permit_id_fkey" + columns: ["permit_id"] + isOneToOne: false + referencedRelation: "fishing_permits" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } config: { Row: { billing_provider: Database["public"]["Enums"]["billing_provider"] @@ -584,6 +1475,58 @@ export type Database = { } Relationships: [] } + cost_centers: { + Row: { + account_id: string + code: string | null + created_at: string + description: string | null + id: string + is_active: boolean + name: string + } + Insert: { + account_id: string + code?: string | null + created_at?: string + description?: string | null + id?: string + is_active?: boolean + name: string + } + Update: { + account_id?: string + code?: string | null + created_at?: string + description?: string | null + id?: string + is_active?: boolean + name?: string + } + Relationships: [ + { + foreignKeyName: "cost_centers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "cost_centers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "cost_centers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } course_attendance: { Row: { id: string @@ -1021,6 +1964,82 @@ export type Database = { }, ] } + document_templates: { + Row: { + account_id: string + body_html: string + created_at: string + created_by: string | null + description: string | null + id: string + is_default: boolean + name: string + orientation: string + page_format: string + shared_with_hierarchy: boolean + sort_order: number + template_type: string + updated_at: string + variables: Json + } + Insert: { + account_id: string + body_html?: string + created_at?: string + created_by?: string | null + description?: string | null + id?: string + is_default?: boolean + name: string + orientation?: string + page_format?: string + shared_with_hierarchy?: boolean + sort_order?: number + template_type?: string + updated_at?: string + variables?: Json + } + Update: { + account_id?: string + body_html?: string + created_at?: string + created_by?: string | null + description?: string | null + id?: string + is_default?: boolean + name?: string + orientation?: string + page_format?: string + shared_with_hierarchy?: boolean + sort_order?: number + template_type?: string + updated_at?: string + variables?: Json + } + Relationships: [ + { + foreignKeyName: "document_templates_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "document_templates_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "document_templates_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } dues_categories: { Row: { account_id: string @@ -1158,6 +2177,7 @@ export type Database = { min_age: number | null name: string registration_deadline: string | null + shared_with_hierarchy: boolean status: string updated_at: string } @@ -1180,6 +2200,7 @@ export type Database = { min_age?: number | null name: string registration_deadline?: string | null + shared_with_hierarchy?: boolean status?: string updated_at?: string } @@ -1202,6 +2223,7 @@ export type Database = { min_age?: number | null name?: string registration_deadline?: string | null + shared_with_hierarchy?: boolean status?: string updated_at?: string } @@ -1229,6 +2251,470 @@ export type Database = { }, ] } + fish_species: { + Row: { + account_id: string + created_at: string + has_special_spawning_season: boolean + id: string + individual_recording: boolean + is_active: boolean + k_factor_avg: number | null + k_factor_max: number | null + k_factor_min: number | null + max_age_years: number | null + max_catch_per_day: number | null + max_catch_per_year: number | null + max_length_cm: number | null + max_weight_kg: number | null + name: string + name_latin: string | null + name_local: string | null + price_per_unit: number | null + protected_min_size_cm: number | null + protection_period_end: string | null + protection_period_start: string | null + sort_order: number + spawning_season_end: string | null + spawning_season_start: string | null + updated_at: string + } + Insert: { + account_id: string + created_at?: string + has_special_spawning_season?: boolean + id?: string + individual_recording?: boolean + is_active?: boolean + k_factor_avg?: number | null + k_factor_max?: number | null + k_factor_min?: number | null + max_age_years?: number | null + max_catch_per_day?: number | null + max_catch_per_year?: number | null + max_length_cm?: number | null + max_weight_kg?: number | null + name: string + name_latin?: string | null + name_local?: string | null + price_per_unit?: number | null + protected_min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + sort_order?: number + spawning_season_end?: string | null + spawning_season_start?: string | null + updated_at?: string + } + Update: { + account_id?: string + created_at?: string + has_special_spawning_season?: boolean + id?: string + individual_recording?: boolean + is_active?: boolean + k_factor_avg?: number | null + k_factor_max?: number | null + k_factor_min?: number | null + max_age_years?: number | null + max_catch_per_day?: number | null + max_catch_per_year?: number | null + max_length_cm?: number | null + max_weight_kg?: number | null + name?: string + name_latin?: string | null + name_local?: string | null + price_per_unit?: number | null + protected_min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + sort_order?: number + spawning_season_end?: string | null + spawning_season_start?: string | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "fish_species_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_species_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_species_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + fish_stocking: { + Row: { + account_id: string + age_class: Database["public"]["Enums"]["fish_age_class"] + cost_euros: number | null + created_at: string + created_by: string | null + id: string + quantity: number + remarks: string | null + species_id: string + stocking_date: string + supplier_id: string | null + updated_at: string + updated_by: string | null + water_id: string + weight_kg: number | null + } + Insert: { + account_id: string + age_class?: Database["public"]["Enums"]["fish_age_class"] + cost_euros?: number | null + created_at?: string + created_by?: string | null + id?: string + quantity?: number + remarks?: string | null + species_id: string + stocking_date: string + supplier_id?: string | null + updated_at?: string + updated_by?: string | null + water_id: string + weight_kg?: number | null + } + Update: { + account_id?: string + age_class?: Database["public"]["Enums"]["fish_age_class"] + cost_euros?: number | null + created_at?: string + created_by?: string | null + id?: string + quantity?: number + remarks?: string | null + species_id?: string + stocking_date?: string + supplier_id?: string | null + updated_at?: string + updated_by?: string | null + water_id?: string + weight_kg?: number | null + } + Relationships: [ + { + foreignKeyName: "fish_stocking_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_species_id_fkey" + columns: ["species_id"] + isOneToOne: false + referencedRelation: "fish_species" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_supplier_id_fkey" + columns: ["supplier_id"] + isOneToOne: false + referencedRelation: "fish_suppliers" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + fish_suppliers: { + Row: { + account_id: string + address: string | null + contact_person: string | null + created_at: string + email: string | null + id: string + is_active: boolean + name: string + notes: string | null + phone: string | null + updated_at: string + } + Insert: { + account_id: string + address?: string | null + contact_person?: string | null + created_at?: string + email?: string | null + id?: string + is_active?: boolean + name: string + notes?: string | null + phone?: string | null + updated_at?: string + } + Update: { + account_id?: string + address?: string | null + contact_person?: string | null + created_at?: string + email?: string | null + id?: string + is_active?: boolean + name?: string + notes?: string | null + phone?: string | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "fish_suppliers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_suppliers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_suppliers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + fishing_leases: { + Row: { + account_holder: string | null + account_id: string + bic: string | null + created_at: string + created_by: string | null + duration_years: number | null + end_date: string | null + fixed_annual_increase: number | null + iban: string | null + id: string + initial_amount: number + is_archived: boolean + lessor_address: string | null + lessor_email: string | null + lessor_name: string + lessor_phone: string | null + location_details: string | null + payment_method: + | Database["public"]["Enums"]["lease_payment_method"] + | null + percentage_annual_increase: number | null + special_agreements: string | null + start_date: string + updated_at: string + updated_by: string | null + water_id: string + } + Insert: { + account_holder?: string | null + account_id: string + bic?: string | null + created_at?: string + created_by?: string | null + duration_years?: number | null + end_date?: string | null + fixed_annual_increase?: number | null + iban?: string | null + id?: string + initial_amount?: number + is_archived?: boolean + lessor_address?: string | null + lessor_email?: string | null + lessor_name: string + lessor_phone?: string | null + location_details?: string | null + payment_method?: + | Database["public"]["Enums"]["lease_payment_method"] + | null + percentage_annual_increase?: number | null + special_agreements?: string | null + start_date: string + updated_at?: string + updated_by?: string | null + water_id: string + } + Update: { + account_holder?: string | null + account_id?: string + bic?: string | null + created_at?: string + created_by?: string | null + duration_years?: number | null + end_date?: string | null + fixed_annual_increase?: number | null + iban?: string | null + id?: string + initial_amount?: number + is_archived?: boolean + lessor_address?: string | null + lessor_email?: string | null + lessor_name?: string + lessor_phone?: string | null + location_details?: string | null + payment_method?: + | Database["public"]["Enums"]["lease_payment_method"] + | null + percentage_annual_increase?: number | null + special_agreements?: string | null + start_date?: string + updated_at?: string + updated_by?: string | null + water_id?: string + } + Relationships: [ + { + foreignKeyName: "fishing_leases_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_leases_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_leases_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_leases_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + fishing_permits: { + Row: { + account_id: string + cost_center_id: string | null + created_at: string + hejfish_id: string | null + id: string + is_archived: boolean + is_for_sale: boolean + name: string + primary_water_id: string | null + short_code: string | null + total_quantity: number | null + updated_at: string + } + Insert: { + account_id: string + cost_center_id?: string | null + created_at?: string + hejfish_id?: string | null + id?: string + is_archived?: boolean + is_for_sale?: boolean + name: string + primary_water_id?: string | null + short_code?: string | null + total_quantity?: number | null + updated_at?: string + } + Update: { + account_id?: string + cost_center_id?: string | null + created_at?: string + hejfish_id?: string | null + id?: string + is_archived?: boolean + is_for_sale?: boolean + name?: string + primary_water_id?: string | null + short_code?: string | null + total_quantity?: number | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "fishing_permits_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_cost_center_id_fkey" + columns: ["cost_center_id"] + isOneToOne: false + referencedRelation: "cost_centers" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_primary_water_id_fkey" + columns: ["primary_water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } gdpr_processing_register: { Row: { account_id: string @@ -1372,6 +2858,58 @@ export type Database = { }, ] } + hejfish_sync: { + Row: { + account_id: string + entity_type: string + hejfish_id: string + id: string + last_synced_at: string + local_id: string + sync_data: Json | null + } + Insert: { + account_id: string + entity_type: string + hejfish_id: string + id?: string + last_synced_at?: string + local_id: string + sync_data?: Json | null + } + Update: { + account_id?: string + entity_type?: string + hejfish_id?: string + id?: string + last_synced_at?: string + local_id?: string + sync_data?: Json | null + } + Relationships: [ + { + foreignKeyName: "hejfish_sync_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "hejfish_sync_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "hejfish_sync_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } holiday_pass_activities: { Row: { activity_date: string | null @@ -1682,6 +3220,211 @@ export type Database = { }, ] } + meeting_protocol_attachments: { + Row: { + content_type: string | null + created_at: string + created_by: string | null + file_name: string + file_path: string + file_size: number | null + id: string + item_id: string | null + protocol_id: string + } + Insert: { + content_type?: string | null + created_at?: string + created_by?: string | null + file_name: string + file_path: string + file_size?: number | null + id?: string + item_id?: string | null + protocol_id: string + } + Update: { + content_type?: string | null + created_at?: string + created_by?: string | null + file_name?: string + file_path?: string + file_size?: number | null + id?: string + item_id?: string | null + protocol_id?: string + } + Relationships: [ + { + foreignKeyName: "meeting_protocol_attachments_item_id_fkey" + columns: ["item_id"] + isOneToOne: false + referencedRelation: "meeting_protocol_items" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocol_attachments_item_id_fkey" + columns: ["item_id"] + isOneToOne: false + referencedRelation: "open_meeting_tasks" + referencedColumns: ["item_id"] + }, + { + foreignKeyName: "meeting_protocol_attachments_protocol_id_fkey" + columns: ["protocol_id"] + isOneToOne: false + referencedRelation: "meeting_protocols" + referencedColumns: ["id"] + }, + ] + } + meeting_protocol_items: { + Row: { + content: string | null + created_at: string + decision_text: string | null + due_date: string | null + id: string + item_number: number + item_type: string + protocol_id: string + responsible_person: string | null + sort_order: number + status: Database["public"]["Enums"]["meeting_item_status"] + title: string + updated_at: string + } + Insert: { + content?: string | null + created_at?: string + decision_text?: string | null + due_date?: string | null + id?: string + item_number?: number + item_type?: string + protocol_id: string + responsible_person?: string | null + sort_order?: number + status?: Database["public"]["Enums"]["meeting_item_status"] + title: string + updated_at?: string + } + Update: { + content?: string | null + created_at?: string + decision_text?: string | null + due_date?: string | null + id?: string + item_number?: number + item_type?: string + protocol_id?: string + responsible_person?: string | null + sort_order?: number + status?: Database["public"]["Enums"]["meeting_item_status"] + title?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "meeting_protocol_items_protocol_id_fkey" + columns: ["protocol_id"] + isOneToOne: false + referencedRelation: "meeting_protocols" + referencedColumns: ["id"] + }, + ] + } + meeting_protocols: { + Row: { + absent: Json + account_id: string + attendees: Json + chair: string | null + created_at: string + created_by: string | null + end_time: string | null + id: string + is_archived: boolean + location: string | null + meeting_date: string + meeting_time: string | null + next_meeting_date: string | null + protocol_number: string | null + recorder: string | null + status: string + summary: string | null + title: string + updated_at: string + updated_by: string | null + } + Insert: { + absent?: Json + account_id: string + attendees?: Json + chair?: string | null + created_at?: string + created_by?: string | null + end_time?: string | null + id?: string + is_archived?: boolean + location?: string | null + meeting_date: string + meeting_time?: string | null + next_meeting_date?: string | null + protocol_number?: string | null + recorder?: string | null + status?: string + summary?: string | null + title: string + updated_at?: string + updated_by?: string | null + } + Update: { + absent?: Json + account_id?: string + attendees?: Json + chair?: string | null + created_at?: string + created_by?: string | null + end_time?: string | null + id?: string + is_archived?: boolean + location?: string | null + meeting_date?: string + meeting_time?: string | null + next_meeting_date?: string | null + protocol_number?: string | null + recorder?: string | null + status?: string + summary?: string | null + title?: string + updated_at?: string + updated_by?: string | null + } + Relationships: [ + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } member_cards: { Row: { account_id: string @@ -1744,6 +3487,122 @@ export type Database = { }, ] } + member_clubs: { + Row: { + account_holder: string | null + account_id: string + address_city: string | null + address_street: string | null + address_zip: string | null + association_type_id: string | null + bic: string | null + club_number: string | null + created_at: string + created_by: string | null + custom_data: Json + email: string | null + founding_year: number | null + iban: string | null + id: string + is_active: boolean + is_archived: boolean + member_count: number + name: string + notes: string | null + phone: string | null + short_name: string | null + updated_at: string + updated_by: string | null + website: string | null + youth_count: number + } + Insert: { + account_holder?: string | null + account_id: string + address_city?: string | null + address_street?: string | null + address_zip?: string | null + association_type_id?: string | null + bic?: string | null + club_number?: string | null + created_at?: string + created_by?: string | null + custom_data?: Json + email?: string | null + founding_year?: number | null + iban?: string | null + id?: string + is_active?: boolean + is_archived?: boolean + member_count?: number + name: string + notes?: string | null + phone?: string | null + short_name?: string | null + updated_at?: string + updated_by?: string | null + website?: string | null + youth_count?: number + } + Update: { + account_holder?: string | null + account_id?: string + address_city?: string | null + address_street?: string | null + address_zip?: string | null + association_type_id?: string | null + bic?: string | null + club_number?: string | null + created_at?: string + created_by?: string | null + custom_data?: Json + email?: string | null + founding_year?: number | null + iban?: string | null + id?: string + is_active?: boolean + is_archived?: boolean + member_count?: number + name?: string + notes?: string | null + phone?: string | null + short_name?: string | null + updated_at?: string + updated_by?: string | null + website?: string | null + youth_count?: number + } + Relationships: [ + { + foreignKeyName: "member_clubs_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_clubs_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_clubs_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_clubs_association_type_id_fkey" + columns: ["association_type_id"] + isOneToOne: false + referencedRelation: "association_types" + referencedColumns: ["id"] + }, + ] + } member_department_assignments: { Row: { department_id: string @@ -2012,6 +3871,89 @@ export type Database = { }, ] } + member_transfers: { + Row: { + cleared_data: Json + id: string + member_id: string + reason: string | null + source_account_id: string + target_account_id: string + transferred_at: string + transferred_by: string + } + Insert: { + cleared_data?: Json + id?: string + member_id: string + reason?: string | null + source_account_id: string + target_account_id: string + transferred_at?: string + transferred_by: string + } + Update: { + cleared_data?: Json + id?: string + member_id?: string + reason?: string | null + source_account_id?: string + target_account_id?: string + transferred_at?: string + transferred_by?: string + } + Relationships: [ + { + foreignKeyName: "member_transfers_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_source_account_id_fkey" + columns: ["source_account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_source_account_id_fkey" + columns: ["source_account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_source_account_id_fkey" + columns: ["source_account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_target_account_id_fkey" + columns: ["target_account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_target_account_id_fkey" + columns: ["target_account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_target_account_id_fkey" + columns: ["target_account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } members: { Row: { account_holder: string | null @@ -2924,6 +4866,7 @@ export type Database = { created_at: string id: string name: string + shared_with_hierarchy: boolean subject: string updated_at: string variables: Json @@ -2935,6 +4878,7 @@ export type Database = { created_at?: string id?: string name: string + shared_with_hierarchy?: boolean subject: string updated_at?: string variables?: Json @@ -2946,6 +4890,7 @@ export type Database = { created_at?: string id?: string name?: string + shared_with_hierarchy?: boolean subject?: string updated_at?: string variables?: Json @@ -3278,6 +5223,44 @@ export type Database = { }, ] } + permit_quotas: { + Row: { + business_year: number + category_name: string | null + conversion_factor: number | null + created_at: string + id: string + permit_id: string + quota_quantity: number + } + Insert: { + business_year: number + category_name?: string | null + conversion_factor?: number | null + created_at?: string + id?: string + permit_id: string + quota_quantity?: number + } + Update: { + business_year?: number + category_name?: string | null + conversion_factor?: number | null + created_at?: string + id?: string + permit_id?: string + quota_quantity?: number + } + Relationships: [ + { + foreignKeyName: "permit_quotas_permit_id_fkey" + columns: ["permit_id"] + isOneToOne: false + referencedRelation: "fishing_permits" + referencedColumns: ["id"] + }, + ] + } role_permissions: { Row: { id: number @@ -3395,6 +5378,7 @@ export type Database = { description: string | null execution_date: string id: string + is_consolidated: boolean item_count: number pain_format: string status: Database["public"]["Enums"]["sepa_batch_status"] @@ -3410,6 +5394,7 @@ export type Database = { description?: string | null execution_date: string id?: string + is_consolidated?: boolean item_count?: number pain_format?: string status?: Database["public"]["Enums"]["sepa_batch_status"] @@ -3425,6 +5410,7 @@ export type Database = { description?: string | null execution_date?: string id?: string + is_consolidated?: boolean item_count?: number pain_format?: string status?: Database["public"]["Enums"]["sepa_batch_status"] @@ -3902,8 +5888,296 @@ export type Database = { }, ] } + water_inspectors: { + Row: { + account_id: string + assignment_end: string | null + assignment_start: string + created_at: string + id: string + member_id: string + water_id: string + } + Insert: { + account_id: string + assignment_end?: string | null + assignment_start?: string + created_at?: string + id?: string + member_id: string + water_id: string + } + Update: { + account_id?: string + assignment_end?: string | null + assignment_start?: string + created_at?: string + id?: string + member_id?: string + water_id?: string + } + Relationships: [ + { + foreignKeyName: "water_inspectors_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + water_species_rules: { + Row: { + created_at: string + id: string + max_catch_per_day: number | null + max_catch_per_year: number | null + min_size_cm: number | null + protection_period_end: string | null + protection_period_start: string | null + species_id: string + water_id: string + } + Insert: { + created_at?: string + id?: string + max_catch_per_day?: number | null + max_catch_per_year?: number | null + min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + species_id: string + water_id: string + } + Update: { + created_at?: string + id?: string + max_catch_per_day?: number | null + max_catch_per_year?: number | null + min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + species_id?: string + water_id?: string + } + Relationships: [ + { + foreignKeyName: "water_species_rules_species_id_fkey" + columns: ["species_id"] + isOneToOne: false + referencedRelation: "fish_species" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_species_rules_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + waters: { + Row: { + account_id: string + avg_depth_m: number | null + classification_order: number | null + cost_center_id: string | null + cost_share_ds: number | null + cost_share_kalk: number | null + county: string | null + created_at: string + created_by: string | null + description: string | null + electrofishing_permit_requested: boolean + geo_lat: number | null + geo_lng: number | null + hejfish_id: string | null + id: string + is_archived: boolean + length_m: number | null + lfv_name: string | null + lfv_number: string | null + location: string | null + max_depth_m: number | null + name: string + outflow: string | null + short_name: string | null + surface_area_ha: number | null + updated_at: string + updated_by: string | null + water_type: Database["public"]["Enums"]["water_type"] + width_m: number | null + } + Insert: { + account_id: string + avg_depth_m?: number | null + classification_order?: number | null + cost_center_id?: string | null + cost_share_ds?: number | null + cost_share_kalk?: number | null + county?: string | null + created_at?: string + created_by?: string | null + description?: string | null + electrofishing_permit_requested?: boolean + geo_lat?: number | null + geo_lng?: number | null + hejfish_id?: string | null + id?: string + is_archived?: boolean + length_m?: number | null + lfv_name?: string | null + lfv_number?: string | null + location?: string | null + max_depth_m?: number | null + name: string + outflow?: string | null + short_name?: string | null + surface_area_ha?: number | null + updated_at?: string + updated_by?: string | null + water_type?: Database["public"]["Enums"]["water_type"] + width_m?: number | null + } + Update: { + account_id?: string + avg_depth_m?: number | null + classification_order?: number | null + cost_center_id?: string | null + cost_share_ds?: number | null + cost_share_kalk?: number | null + county?: string | null + created_at?: string + created_by?: string | null + description?: string | null + electrofishing_permit_requested?: boolean + geo_lat?: number | null + geo_lng?: number | null + hejfish_id?: string | null + id?: string + is_archived?: boolean + length_m?: number | null + lfv_name?: string | null + lfv_number?: string | null + location?: string | null + max_depth_m?: number | null + name?: string + outflow?: string | null + short_name?: string | null + surface_area_ha?: number | null + updated_at?: string + updated_by?: string | null + water_type?: Database["public"]["Enums"]["water_type"] + width_m?: number | null + } + Relationships: [ + { + foreignKeyName: "waters_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "waters_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "waters_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "waters_cost_center_id_fkey" + columns: ["cost_center_id"] + isOneToOne: false + referencedRelation: "cost_centers" + referencedColumns: ["id"] + }, + ] + } } Views: { + open_meeting_tasks: { + Row: { + account_id: string | null + due_date: string | null + is_overdue: boolean | null + item_id: string | null + item_number: number | null + meeting_date: string | null + protocol_id: string | null + protocol_title: string | null + responsible_person: string | null + status: Database["public"]["Enums"]["meeting_item_status"] | null + task_description: string | null + task_title: string | null + } + Relationships: [ + { + foreignKeyName: "meeting_protocol_items_protocol_id_fkey" + columns: ["protocol_id"] + isOneToOne: false + referencedRelation: "meeting_protocols" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } user_account_workspace: { Row: { id: string | null @@ -3967,6 +6241,25 @@ export type Database = { status: Database["public"]["Enums"]["membership_status"] }[] } + clone_template: { + Args: { + p_new_name?: string + p_target_account_id: string + p_template_id: string + p_template_type: string + } + Returns: string + } + compute_lease_amount: { + Args: { + p_fixed_increase: number + p_initial_amount: number + p_percentage_increase: number + p_start_year: number + p_target_year: number + } + Returns: number + } create_invitation: { Args: { account_id: string; email: string; role: string } Returns: { @@ -4007,6 +6300,7 @@ export type Database = { id: string is_personal_account: boolean name: string + parent_account_id: string | null picture_url: string | null primary_owner_user_id: string public_data: Json @@ -4021,6 +6315,9 @@ export type Database = { isSetofReturn: false } } + get_account_ancestors: { Args: { child_id: string }; Returns: string[] } + get_account_depth: { Args: { account_id: string }; Returns: number } + get_account_descendants: { Args: { root_id: string }; Returns: string[] } get_account_invitations: { Args: { account_slug: string } Returns: { @@ -4052,9 +6349,75 @@ export type Database = { user_id: string }[] } + get_catch_statistics: { + Args: { p_account_id: string; p_water_id?: string; p_year?: number } + Returns: { + avg_k_factor: number + avg_length_cm: number + avg_weight_g: number + species_id: string + species_name: string + total_count: number + total_weight_kg: number + }[] + } + get_child_accounts: { + Args: { parent_id: string } + Returns: { + created_at: string + id: string + name: string + parent_account_id: string + slug: string + }[] + } get_config: { Args: never; Returns: Json } + get_hierarchy_report: { + Args: { root_account_id: string } + Returns: { + active_courses: number + active_members: number + depth: number + inactive_members: number + new_members_this_year: number + open_invoice_amount: number + open_invoices: number + org_id: string + org_name: string + org_slug: string + sepa_batches_this_year: number + total_members: number + upcoming_events: number + }[] + } + get_hierarchy_summary: { + Args: { root_account_id: string } + Returns: { + new_members_this_year: number + total_active_courses: number + total_active_members: number + total_members: number + total_open_invoice_amount: number + total_open_invoices: number + total_orgs: number + total_sepa_batches_this_year: number + total_upcoming_events: number + }[] + } get_nonce_status: { Args: { p_id: string }; Returns: Json } get_upper_system_role: { Args: never; Returns: string } + get_user_visible_accounts: { Args: never; Returns: string[] } + get_verband_dashboard_stats: { + Args: { p_account_id: string } + Returns: { + active_clubs: number + open_fees: number + open_fees_amount: number + total_clubs: number + total_members: number + total_youth: number + }[] + } has_active_subscription: { Args: { target_account_id: string } Returns: boolean @@ -4075,10 +6438,22 @@ export type Database = { } Returns: boolean } + has_permission_or_ancestor: { + Args: { + permission_name: Database["public"]["Enums"]["app_permissions"] + target_account_id: string + user_id: string + } + Returns: boolean + } has_role_on_account: { Args: { account_id: string; account_role?: string } Returns: boolean } + has_role_on_account_or_ancestor: { + Args: { account_role?: string; target_account_id: string } + Returns: boolean + } has_same_role_hierarchy_level: { Args: { role_name: string @@ -4104,6 +6479,64 @@ export type Database = { Args: { p_invite_token: string; p_user_id: string } Returns: string } + list_hierarchy_events: { + Args: { + p_from_date?: string + p_page?: number + p_page_size?: number + p_shared_only?: boolean + p_status?: string + root_account_id: string + } + Returns: { + account_id: string + account_name: string + capacity: number + description: string + end_date: string + event_date: string + event_time: string + fee: number + id: string + location: string + name: string + registration_count: number + registration_deadline: string + shared_with_hierarchy: boolean + status: string + total_count: number + }[] + } + list_hierarchy_sepa_eligible_members: { + Args: { p_account_filter?: string; root_account_id: string } + Returns: { + account_holder: string + account_id: string + account_name: string + bic: string + dues_amount: number + first_name: string + iban: string + last_name: string + mandate_date: string + mandate_id: string + member_id: string + }[] + } + list_hierarchy_shared_templates: { + Args: { p_template_type?: string; root_account_id: string } + Returns: { + account_id: string + account_name: string + created_at: string + description: string + id: string + name: string + shared_with_hierarchy: boolean + template_source: string + template_type: string + }[] + } module_query: { Args: { p_filters?: Json @@ -4120,6 +6553,33 @@ export type Database = { Args: { p_id: string; p_reason?: string } Returns: boolean } + search_members_across_hierarchy: { + Args: { + account_filter?: string + page_number?: number + page_size?: number + root_account_id: string + search_term?: string + status_filter?: string + } + Returns: { + account_id: string + account_name: string + account_slug: string + city: string + email: string + entry_date: string + first_name: string + id: string + last_name: string + member_number: string + phone: string + status: Database["public"]["Enums"]["membership_status"] + total_count: number + }[] + } + show_limit: { Args: never; Returns: number } + show_trgm: { Args: { "": string }; Returns: string[] } team_account_workspace: { Args: { account_slug: string } Returns: { @@ -4134,6 +6594,15 @@ export type Database = { subscription_status: Database["public"]["Enums"]["subscription_status"] }[] } + transfer_member: { + Args: { + p_keep_sepa?: boolean + p_member_id: string + p_reason?: string + p_target_account_id: string + } + Returns: string + } transfer_team_account_ownership: { Args: { new_owner_id: string; target_account_id: string } Returns: undefined @@ -4246,9 +6715,28 @@ export type Database = { | "finance.sepa" | "documents.generate" | "newsletter.send" + | "fischerei.read" + | "fischerei.write" + | "meetings.read" + | "meetings.write" + | "verband.read" + | "verband.write" application_status: "submitted" | "review" | "approved" | "rejected" audit_action: "insert" | "update" | "delete" | "lock" billing_provider: "stripe" | "lemon-squeezy" | "paddle" + catch_book_status: + | "offen" + | "eingereicht" + | "geprueft" + | "akzeptiert" + | "abgelehnt" + catch_book_verification: + | "sehrgut" + | "gut" + | "ok" + | "schlecht" + | "falsch" + | "leer" cms_account_type: "verein" | "vhs" | "hotel" | "kommune" | "generic" cms_field_type: | "text" @@ -4274,6 +6762,18 @@ export type Database = { cms_module_status: "active" | "inactive" | "archived" cms_record_status: "active" | "locked" | "deleted" | "archived" enrollment_status: "enrolled" | "waitlisted" | "cancelled" | "completed" + fish_age_class: + | "brut" + | "soemmerlinge" + | "einsoemmerig" + | "zweisoemmerig" + | "dreisoemmerig" + | "vorgestreckt" + | "setzlinge" + | "laichfische" + | "sonstige" + fish_gender: "maennlich" | "weiblich" | "unbekannt" + fish_size_category: "gross" | "mittel" | "klein" gdpr_legal_basis: | "consent" | "contract" @@ -4288,6 +6788,8 @@ export type Database = { | "overdue" | "cancelled" | "credited" + lease_payment_method: "bar" | "lastschrift" | "ueberweisung" + meeting_item_status: "offen" | "in_bearbeitung" | "erledigt" | "vertagt" membership_status: | "active" | "inactive" @@ -4319,6 +6821,16 @@ export type Database = { | "incomplete" | "incomplete_expired" | "paused" + water_type: + | "fluss" + | "bach" + | "see" + | "teich" + | "weiher" + | "kanal" + | "stausee" + | "baggersee" + | "sonstige" } CompositeTypes: { invitation: { @@ -5012,10 +7524,31 @@ export const Constants = { "finance.sepa", "documents.generate", "newsletter.send", + "fischerei.read", + "fischerei.write", + "meetings.read", + "meetings.write", + "verband.read", + "verband.write", ], application_status: ["submitted", "review", "approved", "rejected"], audit_action: ["insert", "update", "delete", "lock"], billing_provider: ["stripe", "lemon-squeezy", "paddle"], + catch_book_status: [ + "offen", + "eingereicht", + "geprueft", + "akzeptiert", + "abgelehnt", + ], + catch_book_verification: [ + "sehrgut", + "gut", + "ok", + "schlecht", + "falsch", + "leer", + ], cms_account_type: ["verein", "vhs", "hotel", "kommune", "generic"], cms_field_type: [ "text", @@ -5042,6 +7575,19 @@ export const Constants = { cms_module_status: ["active", "inactive", "archived"], cms_record_status: ["active", "locked", "deleted", "archived"], enrollment_status: ["enrolled", "waitlisted", "cancelled", "completed"], + fish_age_class: [ + "brut", + "soemmerlinge", + "einsoemmerig", + "zweisoemmerig", + "dreisoemmerig", + "vorgestreckt", + "setzlinge", + "laichfische", + "sonstige", + ], + fish_gender: ["maennlich", "weiblich", "unbekannt"], + fish_size_category: ["gross", "mittel", "klein"], gdpr_legal_basis: [ "consent", "contract", @@ -5058,6 +7604,8 @@ export const Constants = { "cancelled", "credited", ], + lease_payment_method: ["bar", "lastschrift", "ueberweisung"], + meeting_item_status: ["offen", "in_bearbeitung", "erledigt", "vertagt"], membership_status: [ "active", "inactive", @@ -5092,6 +7640,17 @@ export const Constants = { "incomplete_expired", "paused", ], + water_type: [ + "fluss", + "bach", + "see", + "teich", + "weiher", + "kanal", + "stausee", + "baggersee", + "sonstige", + ], }, }, storage: { diff --git a/apps/web/supabase/config.toml b/apps/web/supabase/config.toml index 532eb7dcd..a08fba464 100644 --- a/apps/web/supabase/config.toml +++ b/apps/web/supabase/config.toml @@ -60,9 +60,10 @@ verify_enabled = true enroll_enabled = true # Enable WebAuthn/Passkey MFA -[auth.mfa.webauthn] -verify_enabled = true -enroll_enabled = true +# Note: requires Supabase CLI >= 2.90. Uncomment when upgrading. +# [auth.mfa.webauthn] +# verify_enabled = true +# enroll_enabled = true [auth.email] # Allow/disallow new user signups via email to your project. diff --git a/apps/web/supabase/migrations/20260411900001_fischerei_enum_values.sql b/apps/web/supabase/migrations/20260411900001_fischerei_enum_values.sql new file mode 100644 index 000000000..867adb9d4 --- /dev/null +++ b/apps/web/supabase/migrations/20260411900001_fischerei_enum_values.sql @@ -0,0 +1,13 @@ +/* + * Pre-module enum values: add all module-specific permission enum values + * in a separate transaction. Postgres requires new enum values to be + * committed before they can be used in policies/functions. + * + * Covers: fischerei, sitzungsprotokolle, verbandsverwaltung + */ +ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'fischerei.read'; +ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'fischerei.write'; +ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'meetings.read'; +ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'meetings.write'; +ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'verband.read'; +ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'verband.write'; diff --git a/apps/web/supabase/migrations/20260412000001_fischerei.sql b/apps/web/supabase/migrations/20260412000001_fischerei.sql index 969600ced..4eba4c5f1 100644 --- a/apps/web/supabase/migrations/20260412000001_fischerei.sql +++ b/apps/web/supabase/migrations/20260412000001_fischerei.sql @@ -40,11 +40,10 @@ CREATE TYPE public.fish_size_category AS ENUM( -- ===================================================== -- 2. Extend app_permissions +-- (Moved to 20260411900001_fischerei_enum_values.sql — Postgres requires +-- new enum values to be committed in a separate transaction) -- ===================================================== -ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'fischerei.read'; -ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'fischerei.write'; - -- ===================================================== -- 3. cost_centers (shared, may already exist) -- ===================================================== diff --git a/apps/web/supabase/migrations/20260413000001_sitzungsprotokolle.sql b/apps/web/supabase/migrations/20260413000001_sitzungsprotokolle.sql index 558501a86..a110cc5f6 100644 --- a/apps/web/supabase/migrations/20260413000001_sitzungsprotokolle.sql +++ b/apps/web/supabase/migrations/20260413000001_sitzungsprotokolle.sql @@ -7,11 +7,9 @@ -- ===================================================== -- 1. Extend app_permissions +-- (Moved to 20260411900001_fischerei_enum_values.sql) -- ===================================================== -ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'meetings.read'; -ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'meetings.write'; - -- ===================================================== -- 2. Enums -- ===================================================== diff --git a/apps/web/supabase/migrations/20260413000002_verbandsverwaltung.sql b/apps/web/supabase/migrations/20260413000002_verbandsverwaltung.sql index e41624039..6f0887c76 100644 --- a/apps/web/supabase/migrations/20260413000002_verbandsverwaltung.sql +++ b/apps/web/supabase/migrations/20260413000002_verbandsverwaltung.sql @@ -8,11 +8,9 @@ -- ===================================================== -- 1. Extend app_permissions +-- (Moved to 20260411900001_fischerei_enum_values.sql) -- ===================================================== -ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'verband.read'; -ALTER TYPE public.app_permissions ADD VALUE IF NOT EXISTS 'verband.write'; - -- ===================================================== -- 2. association_types (Verbandsarten) -- ===================================================== diff --git a/apps/web/supabase/migrations/20260414000001_account_hierarchy.sql b/apps/web/supabase/migrations/20260414000001_account_hierarchy.sql index edcab7e6c..60e145594 100644 --- a/apps/web/supabase/migrations/20260414000001_account_hierarchy.sql +++ b/apps/web/supabase/migrations/20260414000001_account_hierarchy.sql @@ -54,16 +54,16 @@ BEGIN -- Walk up from the proposed parent; if we find NEW.id, it's a cycle IF EXISTS ( WITH RECURSIVE ancestors AS ( - SELECT id, parent_account_id - FROM public.accounts - WHERE id = NEW.parent_account_id + SELECT acc.id, acc.parent_account_id, ARRAY[acc.id] AS path + FROM public.accounts acc + WHERE acc.id = NEW.parent_account_id UNION ALL - SELECT a.id, a.parent_account_id + SELECT a.id, a.parent_account_id, anc.path || a.id FROM public.accounts a JOIN ancestors anc ON a.id = anc.parent_account_id + WHERE NOT a.id = ANY(anc.path) -- cycle guard ) - CYCLE id SET is_cycle USING path - SELECT 1 FROM ancestors WHERE id = NEW.id AND NOT is_cycle + SELECT 1 FROM ancestors WHERE id = NEW.id ) THEN RAISE EXCEPTION 'Setting parent_account_id would create a hierarchy cycle'; END IF; @@ -73,7 +73,9 @@ BEGIN END; $$; -CREATE OR REPLACE TRIGGER prevent_account_hierarchy_cycle +DROP TRIGGER IF EXISTS prevent_account_hierarchy_cycle ON public.accounts; + +CREATE TRIGGER prevent_account_hierarchy_cycle BEFORE INSERT OR UPDATE OF parent_account_id ON public.accounts FOR EACH ROW @@ -82,7 +84,8 @@ CREATE OR REPLACE TRIGGER prevent_account_hierarchy_cycle -- ------------------------------------------------------- -- Helper: get all descendant account IDs (recursive, cycle-safe) --- Restricted to service_role — called via RLS helper functions +-- Uses path array for cycle detection (works on Postgres 14+) +-- Restricted to service_role — called via RLS SECURITY DEFINER functions -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_account_descendants(root_id uuid) RETURNS SETOF uuid @@ -90,15 +93,15 @@ LANGUAGE sql STABLE SET search_path = '' AS $$ WITH RECURSIVE tree AS ( - SELECT id, parent_account_id - FROM public.accounts WHERE id = root_id + SELECT acc.id, ARRAY[acc.id] AS path + FROM public.accounts acc WHERE acc.id = root_id UNION ALL - SELECT a.id, a.parent_account_id + SELECT a.id, t.path || a.id FROM public.accounts a JOIN tree t ON a.parent_account_id = t.id + WHERE NOT a.id = ANY(t.path) ) - CYCLE id SET is_cycle USING path - SELECT id FROM tree WHERE NOT is_cycle; + SELECT tree.id FROM tree; $$; GRANT EXECUTE ON FUNCTION public.get_account_descendants(uuid) @@ -106,7 +109,7 @@ GRANT EXECUTE ON FUNCTION public.get_account_descendants(uuid) -- ------------------------------------------------------- -- Helper: get all ancestor account IDs (walk up, cycle-safe) --- Restricted to service_role — called via RLS helper functions +-- Restricted to service_role -- ------------------------------------------------------- CREATE OR REPLACE FUNCTION public.get_account_ancestors(child_id uuid) RETURNS SETOF uuid @@ -114,15 +117,15 @@ LANGUAGE sql STABLE SET search_path = '' AS $$ WITH RECURSIVE tree AS ( - SELECT id, parent_account_id - FROM public.accounts WHERE id = child_id + SELECT acc.id, acc.parent_account_id, ARRAY[acc.id] AS path + FROM public.accounts acc WHERE acc.id = child_id UNION ALL - SELECT a.id, a.parent_account_id + SELECT a.id, a.parent_account_id, t.path || a.id FROM public.accounts a JOIN tree t ON a.id = t.parent_account_id + WHERE NOT a.id = ANY(t.path) ) - CYCLE id SET is_cycle USING path - SELECT id FROM tree WHERE NOT is_cycle; + SELECT tree.id FROM tree; $$; GRANT EXECUTE ON FUNCTION public.get_account_ancestors(uuid) @@ -138,16 +141,16 @@ LANGUAGE sql STABLE SET search_path = '' AS $$ WITH RECURSIVE tree AS ( - SELECT id, parent_account_id, 0 AS depth - FROM public.accounts - WHERE id = account_id + SELECT acc.id, acc.parent_account_id, 0 AS depth, ARRAY[acc.id] AS path + FROM public.accounts acc + WHERE acc.id = get_account_depth.account_id UNION ALL - SELECT a.id, a.parent_account_id, t.depth + 1 + SELECT a.id, a.parent_account_id, t.depth + 1, t.path || a.id FROM public.accounts a JOIN tree t ON a.id = t.parent_account_id + WHERE NOT a.id = ANY(t.path) ) - CYCLE id SET is_cycle USING path - SELECT MAX(depth) FROM tree WHERE NOT is_cycle; + SELECT MAX(depth) FROM tree; $$; GRANT EXECUTE ON FUNCTION public.get_account_depth(uuid) diff --git a/apps/web/supabase/migrations/20260414000002_hierarchy_functions_rls.sql b/apps/web/supabase/migrations/20260414000002_hierarchy_functions_rls.sql index 2bfa82bb6..cd1c920f4 100644 --- a/apps/web/supabase/migrations/20260414000002_hierarchy_functions_rls.sql +++ b/apps/web/supabase/migrations/20260414000002_hierarchy_functions_rls.sql @@ -30,7 +30,7 @@ AS $$ FROM public.accounts_memberships membership WHERE membership.user_id = (SELECT auth.uid()) AND membership.account_id IN ( - SELECT id FROM public.get_account_ancestors(target_account_id) + SELECT public.get_account_ancestors(target_account_id) ) AND ( membership.account_role = has_role_on_account_or_ancestor.account_role @@ -62,7 +62,7 @@ BEGIN JOIN public.role_permissions rp ON am.account_role = rp.role WHERE am.user_id = has_permission_or_ancestor.user_id AND am.account_id IN ( - SELECT id FROM public.get_account_ancestors(target_account_id) + SELECT public.get_account_ancestors(target_account_id) ) AND rp.permission = has_permission_or_ancestor.permission_name ); diff --git a/apps/web/supabase/migrations/20260414000003_cross_org_member_search.sql b/apps/web/supabase/migrations/20260414000003_cross_org_member_search.sql new file mode 100644 index 000000000..31f35bd6f --- /dev/null +++ b/apps/web/supabase/migrations/20260414000003_cross_org_member_search.sql @@ -0,0 +1,109 @@ +/* + * ------------------------------------------------------- + * Cross-Organization Member Search + * + * Enables Verband admins to search members across all + * descendant accounts in the hierarchy. Uses the existing + * members_hierarchy_read RLS policy for access control. + * ------------------------------------------------------- + */ + +-- Enable pg_trgm for trigram-based text search +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- Full-text search index on member names for fast lookups +CREATE INDEX IF NOT EXISTS ix_members_name_trgm + ON public.members USING gin ( + (lower(first_name || ' ' || last_name)) gin_trgm_ops + ); + +-- Search members across a hierarchy with text search + filters +CREATE OR REPLACE FUNCTION public.search_members_across_hierarchy( + root_account_id uuid, + search_term text DEFAULT NULL, + status_filter text DEFAULT NULL, + account_filter uuid DEFAULT NULL, + page_number int DEFAULT 1, + page_size int DEFAULT 25 +) +RETURNS TABLE ( + id uuid, + account_id uuid, + account_name varchar, + account_slug text, + member_number text, + first_name text, + last_name text, + email text, + phone text, + city text, + status public.membership_status, + entry_date date, + total_count bigint +) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + v_offset int; + v_total bigint; +BEGIN + -- Verify caller has a role on the root account + IF NOT public.has_role_on_account(root_account_id) THEN + RETURN; + END IF; + + v_offset := (page_number - 1) * page_size; + + -- Count total matches first + SELECT count(*) INTO v_total + FROM public.members m + JOIN public.accounts a ON a.id = m.account_id + WHERE m.account_id IN ( + SELECT d.id FROM public.get_account_descendants(root_account_id) d(id) + ) + AND (search_term IS NULL OR search_term = '' OR + lower(m.first_name || ' ' || m.last_name) LIKE '%' || lower(search_term) || '%' OR + lower(m.email) LIKE '%' || lower(search_term) || '%' OR + m.member_number ILIKE '%' || search_term || '%' + ) + AND (status_filter IS NULL OR m.status = status_filter::public.membership_status) + AND (account_filter IS NULL OR m.account_id = account_filter); + + -- Return results with total count + RETURN QUERY + SELECT + m.id, + m.account_id, + a.name AS account_name, + a.slug AS account_slug, + m.member_number, + m.first_name, + m.last_name, + m.email, + m.phone, + m.city, + m.status, + m.entry_date, + v_total AS total_count + FROM public.members m + JOIN public.accounts a ON a.id = m.account_id + WHERE m.account_id IN ( + SELECT d.id FROM public.get_account_descendants(root_account_id) d(id) + ) + AND (search_term IS NULL OR search_term = '' OR + lower(m.first_name || ' ' || m.last_name) LIKE '%' || lower(search_term) || '%' OR + lower(m.email) LIKE '%' || lower(search_term) || '%' OR + m.member_number ILIKE '%' || search_term || '%' + ) + AND (status_filter IS NULL OR m.status = status_filter::public.membership_status) + AND (account_filter IS NULL OR m.account_id = account_filter) + ORDER BY m.last_name, m.first_name + LIMIT page_size + OFFSET v_offset; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.search_members_across_hierarchy(uuid, text, text, uuid, int, int) + TO authenticated, service_role; diff --git a/apps/web/supabase/migrations/20260414000004_member_transfer.sql b/apps/web/supabase/migrations/20260414000004_member_transfer.sql new file mode 100644 index 000000000..64d287727 --- /dev/null +++ b/apps/web/supabase/migrations/20260414000004_member_transfer.sql @@ -0,0 +1,198 @@ +/* + * ------------------------------------------------------- + * Member Transfer Between Accounts + * + * Enables transferring a member from one Verein to another + * within the same Verband hierarchy. + * + * Design: + * - Personal data always moves with the member + * - Course enrollments, event registrations, bookings are + * linked via member_id (not account_id) so they survive + * the transfer automatically + * - Only org-specific admin data is cleared: + * dues_category_id (org-specific pricing), + * member_number (unique per org) + * - SEPA bank data (IBAN/BIC) is preserved, but mandate + * status resets to 'pending' (needs re-confirmation) + * - Financial records (invoices, SEPA batches) stay in + * source org — they're legally tied to that entity + * ------------------------------------------------------- + */ + +-- Transfer log table +CREATE TABLE IF NOT EXISTS public.member_transfers ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE, + source_account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE SET NULL, + target_account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE SET NULL, + transferred_by uuid NOT NULL REFERENCES auth.users(id) ON DELETE SET NULL, + reason text, + -- Snapshot what was cleared so it can be reviewed later + cleared_data jsonb NOT NULL DEFAULT '{}'::jsonb, + transferred_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS ix_member_transfers_member + ON public.member_transfers(member_id); + +CREATE INDEX IF NOT EXISTS ix_member_transfers_source + ON public.member_transfers(source_account_id); + +CREATE INDEX IF NOT EXISTS ix_member_transfers_target + ON public.member_transfers(target_account_id); + +ALTER TABLE public.member_transfers ENABLE ROW LEVEL SECURITY; + +REVOKE ALL ON public.member_transfers FROM authenticated, service_role; +GRANT SELECT, INSERT ON public.member_transfers TO authenticated; +GRANT ALL ON public.member_transfers TO service_role; + +-- Readable by members of source or target account (or ancestor via hierarchy) +CREATE POLICY member_transfers_read ON public.member_transfers + FOR SELECT TO authenticated + USING ( + public.has_role_on_account_or_ancestor(source_account_id) OR + public.has_role_on_account_or_ancestor(target_account_id) + ); + +-- ------------------------------------------------------- +-- Transfer function +-- +-- Only clears org-specific admin data. All cross-org +-- relationships (courses, events, bookings) survive because +-- they reference member_id, not account_id. +-- ------------------------------------------------------- +CREATE OR REPLACE FUNCTION public.transfer_member( + p_member_id uuid, + p_target_account_id uuid, + p_reason text DEFAULT NULL, + p_keep_sepa boolean DEFAULT true +) +RETURNS uuid -- returns the transfer log ID +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + v_source_account_id uuid; + v_transfer_id uuid; + v_caller_id uuid; + v_old_member_number text; + v_old_dues_category_id uuid; + v_old_sepa_mandate_id text; + v_source_name text; + v_target_name text; + v_active_courses int; + v_active_events int; +BEGIN + v_caller_id := (SELECT auth.uid()); + + -- Get the member's current account and data we'll clear + SELECT account_id, member_number, dues_category_id, sepa_mandate_id + INTO v_source_account_id, v_old_member_number, v_old_dues_category_id, v_old_sepa_mandate_id + FROM public.members + WHERE id = p_member_id; + + IF v_source_account_id IS NULL THEN + RAISE EXCEPTION 'Mitglied nicht gefunden'; + END IF; + + IF v_source_account_id = p_target_account_id THEN + RAISE EXCEPTION 'Mitglied ist bereits in dieser Organisation'; + END IF; + + -- Target must be a team account + IF NOT EXISTS ( + SELECT 1 FROM public.accounts + WHERE id = p_target_account_id AND is_personal_account = false + ) THEN + RAISE EXCEPTION 'Zielorganisation nicht gefunden'; + END IF; + + -- Verify caller has visibility on BOTH accounts via hierarchy + IF NOT ( + public.has_role_on_account_or_ancestor(v_source_account_id) + AND public.has_role_on_account_or_ancestor(p_target_account_id) + ) THEN + RAISE EXCEPTION 'Keine Berechtigung für den Transfer'; + END IF; + + -- Verify both accounts share a common ancestor (same Verband) + IF NOT EXISTS ( + SELECT 1 + FROM public.get_account_ancestors(v_source_account_id) sa + JOIN public.get_account_ancestors(p_target_account_id) ta ON sa = ta + ) THEN + RAISE EXCEPTION 'Organisationen gehören nicht zum selben Verband'; + END IF; + + -- Get org names for the transfer note + SELECT name INTO v_source_name FROM public.accounts WHERE id = v_source_account_id; + SELECT name INTO v_target_name FROM public.accounts WHERE id = p_target_account_id; + + -- Count active relationships (informational, for the log) + SELECT count(*) INTO v_active_courses + FROM public.course_participants cp + JOIN public.courses c ON c.id = cp.course_id + WHERE cp.member_id = p_member_id AND cp.status = 'enrolled'; + + SELECT count(*) INTO v_active_events + FROM public.event_registrations er + JOIN public.events e ON e.id = er.event_id + WHERE er.email = (SELECT email FROM public.members WHERE id = p_member_id) + AND er.status IN ('confirmed', 'pending') + AND e.event_date >= current_date; + + -- Perform the transfer + UPDATE public.members + SET + account_id = p_target_account_id, + -- Clear org-specific admin data + dues_category_id = NULL, + member_number = NULL, + -- SEPA: keep bank data (IBAN/BIC/account_holder), just reset mandate status + sepa_mandate_id = CASE WHEN p_keep_sepa THEN sepa_mandate_id ELSE NULL END, + sepa_mandate_date = CASE WHEN p_keep_sepa THEN sepa_mandate_date ELSE NULL END, + sepa_mandate_status = 'pending', -- always needs re-confirmation in new org + -- Append transfer note + notes = COALESCE(notes, '') || + E'\n[Transfer ' || now()::date || '] ' || + v_source_name || ' → ' || v_target_name || + COALESCE(E' — ' || p_reason, '') || + CASE WHEN v_active_courses > 0 + THEN E'\n ↳ ' || v_active_courses || ' aktive Kurseinschreibungen bleiben erhalten' + ELSE '' END || + CASE WHEN v_active_events > 0 + THEN E'\n ↳ ' || v_active_events || ' aktive Veranstaltungsanmeldungen bleiben erhalten' + ELSE '' END, + updated_by = v_caller_id, + updated_at = now() + WHERE id = p_member_id; + + -- Log the transfer with snapshot of cleared data + INSERT INTO public.member_transfers ( + member_id, source_account_id, target_account_id, transferred_by, reason, cleared_data + ) VALUES ( + p_member_id, + v_source_account_id, + p_target_account_id, + v_caller_id, + p_reason, + jsonb_build_object( + 'old_member_number', v_old_member_number, + 'old_dues_category_id', v_old_dues_category_id, + 'old_sepa_mandate_id', v_old_sepa_mandate_id, + 'active_courses_at_transfer', v_active_courses, + 'active_events_at_transfer', v_active_events, + 'sepa_kept', p_keep_sepa + ) + ) + RETURNING id INTO v_transfer_id; + + RETURN v_transfer_id; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.transfer_member(uuid, uuid, text, boolean) + TO authenticated, service_role; diff --git a/apps/web/supabase/migrations/20260414000005_shared_events_consolidated_billing_reporting.sql b/apps/web/supabase/migrations/20260414000005_shared_events_consolidated_billing_reporting.sql new file mode 100644 index 000000000..530ffab5b --- /dev/null +++ b/apps/web/supabase/migrations/20260414000005_shared_events_consolidated_billing_reporting.sql @@ -0,0 +1,270 @@ +/* + * ------------------------------------------------------- + * Feature: Shared Events, Consolidated SEPA, Reporting + * + * 1. Shared events: flag on events to share across hierarchy + * 2. Consolidated SEPA: Verband-level batch across child accounts + * 3. Reporting: aggregated stats RPC functions + * ------------------------------------------------------- + */ + +-- ===================================================== +-- 1. Shared Events +-- ===================================================== + +-- Flag to share an event with the entire hierarchy +ALTER TABLE public.events + ADD COLUMN IF NOT EXISTS shared_with_hierarchy boolean NOT NULL DEFAULT false; + +-- Hierarchy-aware event listing: events visible to current user +-- either directly (own account) or via hierarchy sharing +CREATE OR REPLACE FUNCTION public.list_hierarchy_events( + root_account_id uuid, + p_from_date date DEFAULT NULL, + p_status text DEFAULT NULL, + p_shared_only boolean DEFAULT false, + p_page int DEFAULT 1, + p_page_size int DEFAULT 25 +) +RETURNS TABLE ( + id uuid, + account_id uuid, + account_name varchar, + name text, + description text, + event_date date, + event_time time, + end_date date, + location text, + capacity integer, + fee numeric, + status text, + registration_deadline date, + registration_count bigint, + shared_with_hierarchy boolean, + total_count bigint +) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + v_offset int; + v_total bigint; +BEGIN + IF NOT public.has_role_on_account(root_account_id) THEN + RETURN; + END IF; + + v_offset := (p_page - 1) * p_page_size; + + -- Count + SELECT count(*) INTO v_total + FROM public.events e + WHERE e.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d) + AND (NOT p_shared_only OR e.shared_with_hierarchy = true) + AND (p_from_date IS NULL OR e.event_date >= p_from_date) + AND (p_status IS NULL OR e.status = p_status); + + RETURN QUERY + SELECT + e.id, + e.account_id, + a.name AS account_name, + e.name, + e.description, + e.event_date, + e.event_time, + e.end_date, + e.location, + e.capacity, + e.fee, + e.status, + e.registration_deadline, + (SELECT count(*) FROM public.event_registrations er WHERE er.event_id = e.id AND er.status IN ('confirmed', 'pending'))::bigint AS registration_count, + e.shared_with_hierarchy, + v_total AS total_count + FROM public.events e + JOIN public.accounts a ON a.id = e.account_id + WHERE e.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d) + AND (NOT p_shared_only OR e.shared_with_hierarchy = true) + AND (p_from_date IS NULL OR e.event_date >= p_from_date) + AND (p_status IS NULL OR e.status = p_status) + ORDER BY e.event_date ASC + LIMIT p_page_size OFFSET v_offset; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.list_hierarchy_events(uuid, date, text, boolean, int, int) + TO authenticated, service_role; + +-- ===================================================== +-- 2. Consolidated SEPA Billing +-- ===================================================== + +-- Track which child accounts are included in a consolidated batch +ALTER TABLE public.sepa_batches + ADD COLUMN IF NOT EXISTS is_consolidated boolean NOT NULL DEFAULT false; + +-- RPC: gather members with active SEPA mandates across hierarchy +CREATE OR REPLACE FUNCTION public.list_hierarchy_sepa_eligible_members( + root_account_id uuid, + p_account_filter uuid DEFAULT NULL +) +RETURNS TABLE ( + member_id uuid, + account_id uuid, + account_name varchar, + first_name text, + last_name text, + iban text, + bic text, + account_holder text, + mandate_id text, + mandate_date date, + dues_amount numeric +) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +BEGIN + IF NOT public.has_role_on_account(root_account_id) THEN + RETURN; + END IF; + + RETURN QUERY + SELECT + m.id AS member_id, + m.account_id, + a.name AS account_name, + m.first_name, + m.last_name, + m.iban, + m.bic, + m.account_holder, + m.sepa_mandate_id AS mandate_id, + m.sepa_mandate_date AS mandate_date, + COALESCE(dc.amount, 0) AS dues_amount + FROM public.members m + JOIN public.accounts a ON a.id = m.account_id + LEFT JOIN public.dues_categories dc ON dc.id = m.dues_category_id + WHERE m.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d) + AND m.status = 'active' + AND m.iban IS NOT NULL + AND m.sepa_mandate_status = 'active' + AND (p_account_filter IS NULL OR m.account_id = p_account_filter) + ORDER BY a.name, m.last_name, m.first_name; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.list_hierarchy_sepa_eligible_members(uuid, uuid) + TO authenticated, service_role; + +-- ===================================================== +-- 3. Aggregated Reporting +-- ===================================================== + +-- Comprehensive stats across the hierarchy +CREATE OR REPLACE FUNCTION public.get_hierarchy_report( + root_account_id uuid +) +RETURNS TABLE ( + org_id uuid, + org_name varchar, + org_slug text, + depth int, + active_members bigint, + inactive_members bigint, + total_members bigint, + new_members_this_year bigint, + active_courses bigint, + upcoming_events bigint, + open_invoices bigint, + open_invoice_amount numeric, + sepa_batches_this_year bigint +) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +BEGIN + IF NOT public.has_role_on_account(root_account_id) THEN + RETURN; + END IF; + + RETURN QUERY + WITH descendants AS ( + SELECT d AS id FROM public.get_account_descendants(root_account_id) d + ) + SELECT + a.id AS org_id, + a.name AS org_name, + a.slug AS org_slug, + (SELECT public.get_account_depth(a.id)) AS depth, + -- Members + (SELECT count(*) FROM public.members m WHERE m.account_id = a.id AND m.status = 'active')::bigint AS active_members, + (SELECT count(*) FROM public.members m WHERE m.account_id = a.id AND m.status != 'active')::bigint AS inactive_members, + (SELECT count(*) FROM public.members m WHERE m.account_id = a.id)::bigint AS total_members, + (SELECT count(*) FROM public.members m WHERE m.account_id = a.id AND m.entry_date >= date_trunc('year', current_date))::bigint AS new_members_this_year, + -- Courses + (SELECT count(*) FROM public.courses c WHERE c.account_id = a.id AND c.end_date >= current_date)::bigint AS active_courses, + -- Events + (SELECT count(*) FROM public.events e WHERE e.account_id = a.id AND e.event_date >= current_date AND e.status IN ('planned', 'open'))::bigint AS upcoming_events, + -- Finance + (SELECT count(*) FROM public.invoices i WHERE i.account_id = a.id AND i.status IN ('sent', 'overdue'))::bigint AS open_invoices, + COALESCE((SELECT sum(i.total_amount - i.paid_amount) FROM public.invoices i WHERE i.account_id = a.id AND i.status IN ('sent', 'overdue')), 0) AS open_invoice_amount, + (SELECT count(*) FROM public.sepa_batches sb WHERE sb.account_id = a.id AND sb.created_at >= date_trunc('year', current_date))::bigint AS sepa_batches_this_year + FROM public.accounts a + JOIN descendants d ON d.id = a.id + WHERE a.is_personal_account = false + ORDER BY a.name; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.get_hierarchy_report(uuid) + TO authenticated, service_role; + +-- Summary totals across entire hierarchy +CREATE OR REPLACE FUNCTION public.get_hierarchy_summary( + root_account_id uuid +) +RETURNS TABLE ( + total_orgs bigint, + total_active_members bigint, + total_members bigint, + new_members_this_year bigint, + total_upcoming_events bigint, + total_active_courses bigint, + total_open_invoices bigint, + total_open_invoice_amount numeric, + total_sepa_batches_this_year bigint +) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +BEGIN + IF NOT public.has_role_on_account(root_account_id) THEN + RETURN; + END IF; + + RETURN QUERY + WITH desc_ids AS ( + SELECT d AS id FROM public.get_account_descendants(root_account_id) d + ) + SELECT + (SELECT count(*) FROM desc_ids di JOIN public.accounts ac ON ac.id = di.id WHERE ac.is_personal_account = false)::bigint, + (SELECT count(*) FROM public.members m WHERE m.account_id IN (SELECT di.id FROM desc_ids di) AND m.status = 'active')::bigint, + (SELECT count(*) FROM public.members m WHERE m.account_id IN (SELECT di.id FROM desc_ids di))::bigint, + (SELECT count(*) FROM public.members m WHERE m.account_id IN (SELECT di.id FROM desc_ids di) AND m.entry_date >= date_trunc('year', current_date))::bigint, + (SELECT count(*) FROM public.events e WHERE e.account_id IN (SELECT di.id FROM desc_ids di) AND e.event_date >= current_date AND e.status IN ('planned', 'open'))::bigint, + (SELECT count(*) FROM public.courses c WHERE c.account_id IN (SELECT di.id FROM desc_ids di) AND c.end_date >= current_date)::bigint, + (SELECT count(*) FROM public.invoices i WHERE i.account_id IN (SELECT di.id FROM desc_ids di) AND i.status IN ('sent', 'overdue'))::bigint, + COALESCE((SELECT sum(i.total_amount - i.paid_amount) FROM public.invoices i WHERE i.account_id IN (SELECT di.id FROM desc_ids di) AND i.status IN ('sent', 'overdue')), 0), + (SELECT count(*) FROM public.sepa_batches sb WHERE sb.account_id IN (SELECT di.id FROM desc_ids di) AND sb.created_at >= date_trunc('year', current_date))::bigint; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.get_hierarchy_summary(uuid) + TO authenticated, service_role; diff --git a/apps/web/supabase/migrations/20260414000006_shared_templates.sql b/apps/web/supabase/migrations/20260414000006_shared_templates.sql new file mode 100644 index 000000000..1aaae579a --- /dev/null +++ b/apps/web/supabase/migrations/20260414000006_shared_templates.sql @@ -0,0 +1,282 @@ +/* + * ------------------------------------------------------- + * Shared Templates Across Hierarchy + * + * 1. Add shared_with_hierarchy flag to newsletter_templates + * 2. Create document_templates table with hierarchy sharing + * 3. Template cloning function for child orgs + * 4. Hierarchy-aware RLS for template reads + * ------------------------------------------------------- + */ + +-- ===================================================== +-- 1. Newsletter Templates: add sharing flag +-- ===================================================== + +ALTER TABLE public.newsletter_templates + ADD COLUMN IF NOT EXISTS shared_with_hierarchy boolean NOT NULL DEFAULT false; + +-- Allow child orgs to read parent's shared templates +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' AND tablename = 'newsletter_templates' + AND policyname = 'newsletter_templates_hierarchy_read' + ) THEN + EXECUTE 'CREATE POLICY newsletter_templates_hierarchy_read + ON public.newsletter_templates + FOR SELECT TO authenticated + USING ( + shared_with_hierarchy = true + AND public.has_role_on_account_or_ancestor(account_id) + )'; + END IF; +END $$; + +-- ===================================================== +-- 2. Document Templates table +-- ===================================================== + +CREATE TABLE IF NOT EXISTS public.document_templates ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE, + name text NOT NULL, + description text, + template_type text NOT NULL DEFAULT 'generic' + CHECK (template_type IN ( + 'generic', 'member_card', 'invoice', 'receipt', + 'certificate', 'letter', 'label', 'report' + )), + -- Template content: HTML with variable placeholders like {{member.first_name}} + body_html text NOT NULL DEFAULT '', + -- Available variables for this template + variables jsonb NOT NULL DEFAULT '[]'::jsonb, + -- Page settings + page_format text NOT NULL DEFAULT 'A4' + CHECK (page_format IN ('A4', 'A5', 'A6', 'letter', 'label')), + orientation text NOT NULL DEFAULT 'portrait' + CHECK (orientation IN ('portrait', 'landscape')), + -- Hierarchy sharing + shared_with_hierarchy boolean NOT NULL DEFAULT false, + -- Meta + is_default boolean NOT NULL DEFAULT false, + sort_order integer NOT NULL DEFAULT 0, + created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS ix_document_templates_account + ON public.document_templates(account_id); + +CREATE INDEX IF NOT EXISTS ix_document_templates_type + ON public.document_templates(account_id, template_type); + +ALTER TABLE public.document_templates ENABLE ROW LEVEL SECURITY; + +REVOKE ALL ON public.document_templates FROM authenticated, service_role; +GRANT SELECT, INSERT, UPDATE, DELETE ON public.document_templates TO authenticated; +GRANT ALL ON public.document_templates TO service_role; + +-- Own account read/write +CREATE POLICY document_templates_select ON public.document_templates + FOR SELECT TO authenticated + USING (public.has_role_on_account(account_id)); + +CREATE POLICY document_templates_mutate ON public.document_templates + FOR ALL TO authenticated + USING (public.has_permission(auth.uid(), account_id, 'documents.generate'::public.app_permissions)); + +-- Hierarchy: child orgs can read parent's shared templates +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' AND tablename = 'document_templates' + AND policyname = 'document_templates_hierarchy_read' + ) THEN + EXECUTE 'CREATE POLICY document_templates_hierarchy_read + ON public.document_templates + FOR SELECT TO authenticated + USING ( + shared_with_hierarchy = true + AND public.has_role_on_account_or_ancestor(account_id) + )'; + END IF; +END $$; + +CREATE TRIGGER trg_document_templates_updated_at + BEFORE UPDATE ON public.document_templates + FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp(); + +-- ===================================================== +-- 3. Template cloning function +-- ===================================================== + +-- Clone a shared template into a child org's own templates +CREATE OR REPLACE FUNCTION public.clone_template( + p_template_type text, -- 'newsletter' or 'document' + p_template_id uuid, + p_target_account_id uuid, + p_new_name text DEFAULT NULL +) +RETURNS uuid +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + v_new_id uuid; + v_caller_id uuid; + v_source_account_id uuid; +BEGIN + v_caller_id := (SELECT auth.uid()); + + -- Verify caller has a role on the target account + IF NOT public.has_role_on_account(p_target_account_id) THEN + RAISE EXCEPTION 'Keine Berechtigung für die Zielorganisation'; + END IF; + + IF p_template_type = 'newsletter' THEN + -- Get source account for verification + SELECT account_id INTO v_source_account_id + FROM public.newsletter_templates WHERE id = p_template_id; + + IF v_source_account_id IS NULL THEN + RAISE EXCEPTION 'Vorlage nicht gefunden'; + END IF; + + -- Must be shared or from own account + IF v_source_account_id != p_target_account_id THEN + IF NOT EXISTS ( + SELECT 1 FROM public.newsletter_templates + WHERE id = p_template_id AND shared_with_hierarchy = true + ) THEN + RAISE EXCEPTION 'Vorlage ist nicht für die Hierarchie freigegeben'; + END IF; + END IF; + + INSERT INTO public.newsletter_templates ( + account_id, name, subject, body_html, body_text, variables + ) + SELECT + p_target_account_id, + COALESCE(p_new_name, name || ' (Kopie)'), + subject, body_html, body_text, variables + FROM public.newsletter_templates + WHERE id = p_template_id + RETURNING id INTO v_new_id; + + ELSIF p_template_type = 'document' THEN + SELECT account_id INTO v_source_account_id + FROM public.document_templates WHERE id = p_template_id; + + IF v_source_account_id IS NULL THEN + RAISE EXCEPTION 'Vorlage nicht gefunden'; + END IF; + + IF v_source_account_id != p_target_account_id THEN + IF NOT EXISTS ( + SELECT 1 FROM public.document_templates + WHERE id = p_template_id AND shared_with_hierarchy = true + ) THEN + RAISE EXCEPTION 'Vorlage ist nicht für die Hierarchie freigegeben'; + END IF; + END IF; + + INSERT INTO public.document_templates ( + account_id, name, description, template_type, + body_html, variables, page_format, orientation, created_by + ) + SELECT + p_target_account_id, + COALESCE(p_new_name, name || ' (Kopie)'), + description, template_type, + body_html, variables, page_format, orientation, v_caller_id + FROM public.document_templates + WHERE id = p_template_id + RETURNING id INTO v_new_id; + + ELSE + RAISE EXCEPTION 'Ungültiger Vorlagentyp: %', p_template_type; + END IF; + + RETURN v_new_id; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.clone_template(text, uuid, uuid, text) + TO authenticated, service_role; + +-- ===================================================== +-- 4. List shared templates across hierarchy +-- ===================================================== + +CREATE OR REPLACE FUNCTION public.list_hierarchy_shared_templates( + root_account_id uuid, + p_template_type text DEFAULT NULL -- 'newsletter', 'document', or NULL for both +) +RETURNS TABLE ( + id uuid, + account_id uuid, + account_name varchar, + template_source text, -- 'newsletter' or 'document' + name text, + description text, + template_type text, + shared_with_hierarchy boolean, + created_at timestamptz +) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +BEGIN + IF NOT public.has_role_on_account(root_account_id) THEN + RETURN; + END IF; + + RETURN QUERY + -- Newsletter templates + SELECT + nt.id, + nt.account_id, + a.name AS account_name, + 'newsletter'::text AS template_source, + nt.name, + NULL::text AS description, + 'newsletter'::text AS template_type, + nt.shared_with_hierarchy, + nt.created_at + FROM public.newsletter_templates nt + JOIN public.accounts a ON a.id = nt.account_id + WHERE nt.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d) + AND nt.shared_with_hierarchy = true + AND (p_template_type IS NULL OR p_template_type = 'newsletter') + + UNION ALL + + -- Document templates + SELECT + dt.id, + dt.account_id, + a.name AS account_name, + 'document'::text AS template_source, + dt.name, + dt.description, + dt.template_type, + dt.shared_with_hierarchy, + dt.created_at + FROM public.document_templates dt + JOIN public.accounts a ON a.id = dt.account_id + WHERE dt.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d) + AND dt.shared_with_hierarchy = true + AND (p_template_type IS NULL OR p_template_type = 'document') + + ORDER BY created_at DESC; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.list_hierarchy_shared_templates(uuid, text) + TO authenticated, service_role; diff --git a/docker-compose.yml b/docker-compose.yml index d22bbcf07..c80f269dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: postgres healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + test: ['CMD-SHELL', 'pg_isready -U postgres -d postgres'] interval: 10s timeout: 5s retries: 10 @@ -49,7 +49,7 @@ services: environment: PGPASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - entrypoint: ["/bin/sh", "-c"] + entrypoint: ['/bin/sh', '-c'] command: - | echo "🔑 Ensuring role passwords are set (idempotent)..." @@ -63,7 +63,7 @@ services: echo "✅ App migrations complete." echo "" sh /app-seed/dev-bootstrap.sh - restart: "no" + restart: 'no' # ===================================================== # Supabase Auth (GoTrue) @@ -102,7 +102,15 @@ services: GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"] + test: + [ + 'CMD', + 'wget', + '--no-verbose', + '--tries=1', + '--spider', + 'http://localhost:9999/health', + ] interval: 10s timeout: 5s retries: 5 @@ -123,9 +131,9 @@ services: PGRST_DB_SCHEMAS: public,storage,graphql_public PGRST_DB_ANON_ROLE: anon PGRST_JWT_SECRET: ${JWT_SECRET} - PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_DB_USE_LEGACY_GUCS: 'false' healthcheck: - test: ["CMD-SHELL", "head -c0 r.statusCode === 200 ? process.exit(0) : process.exit(1))"] + test: + [ + 'CMD', + 'node', + '-e', + "require('http').get('http://localhost:3000/api/profile', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))", + ] interval: 10s timeout: 5s retries: 5 @@ -284,7 +302,7 @@ services: entrypoint: > sh -c "sed 's|\$${SUPABASE_ANON_KEY}|'\"$$SUPABASE_ANON_KEY\"'|g; s|\$${SUPABASE_SERVICE_KEY}|'\"$$SUPABASE_SERVICE_KEY\"'|g' /var/lib/kong/kong.yml.tpl > /tmp/kong.yml && KONG_DECLARATIVE_CONFIG=/tmp/kong.yml /docker-entrypoint.sh kong docker-start" environment: - KONG_DATABASE: "off" + KONG_DATABASE: 'off' KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml KONG_DNS_ORDER: LAST,A,CNAME KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth @@ -295,7 +313,7 @@ services: volumes: - ./docker/kong.yml:/var/lib/kong/kong.yml.tpl:ro healthcheck: - test: ["CMD", "kong", "health"] + test: ['CMD', 'kong', 'health'] interval: 10s timeout: 5s retries: 5 @@ -331,12 +349,12 @@ services: SUPABASE_DB_WEBHOOK_SECRET: ${DB_WEBHOOK_SECRET:-webhooksecret} EMAIL_SENDER: ${EMAIL_SENDER:-noreply@myeasycms.de} NEXT_PUBLIC_PRODUCT_NAME: MyEasyCMS - NEXT_PUBLIC_ENABLE_THEME_TOGGLE: "true" - NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS: "true" - NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION: "true" - NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: "false" - NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: "false" - NEXT_PUBLIC_ENABLE_NOTIFICATIONS: "true" + NEXT_PUBLIC_ENABLE_THEME_TOGGLE: 'true' + NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS: 'true' + NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION: 'true' + NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING: 'false' + NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING: 'false' + NEXT_PUBLIC_ENABLE_NOTIFICATIONS: 'true' volumes: supabase-db-data: diff --git a/packages/features/verbandsverwaltung/src/components/club-contacts-manager.tsx b/packages/features/verbandsverwaltung/src/components/club-contacts-manager.tsx index 92e05694d..20970e9aa 100644 --- a/packages/features/verbandsverwaltung/src/components/club-contacts-manager.tsx +++ b/packages/features/verbandsverwaltung/src/components/club-contacts-manager.tsx @@ -142,6 +142,7 @@ export function ClubContactsManager({ Ansprechpartner {!showForm && ( { setEditingId(null); @@ -240,6 +241,7 @@ export function ClubContactsManager({ { @@ -249,7 +251,11 @@ export function ClubContactsManager({ > Abbrechen - + {isCreating || isUpdating ? 'Wird gespeichert...' : editingId @@ -301,6 +307,7 @@ export function ClubContactsManager({ handleEdit(contact)} @@ -308,6 +315,7 @@ export function ClubContactsManager({ diff --git a/packages/features/verbandsverwaltung/src/components/clubs-data-table.tsx b/packages/features/verbandsverwaltung/src/components/clubs-data-table.tsx index ee62a8f80..bb044e14b 100644 --- a/packages/features/verbandsverwaltung/src/components/clubs-data-table.tsx +++ b/packages/features/verbandsverwaltung/src/components/clubs-data-table.tsx @@ -93,30 +93,38 @@ export function ClubsDataTable({ - + Suchen Alle Typen - {types.map((t) => ( - - {t.name} + {types.map((associationType) => ( + + {associationType.name} ))} - + Neuer Verein diff --git a/packages/features/verbandsverwaltung/src/components/create-club-form.tsx b/packages/features/verbandsverwaltung/src/components/create-club-form.tsx index a771ce0cb..b2c0246c4 100644 --- a/packages/features/verbandsverwaltung/src/components/create-club-form.tsx +++ b/packages/features/verbandsverwaltung/src/components/create-club-form.tsx @@ -128,9 +128,12 @@ export function CreateClubForm({ className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm" > — Kein Typ — - {types.map((t) => ( - - {t.name} + {types.map((associationType) => ( + + {associationType.name} ))} diff --git a/packages/features/verbandsverwaltung/src/components/cross-org-member-search.tsx b/packages/features/verbandsverwaltung/src/components/cross-org-member-search.tsx new file mode 100644 index 000000000..ea3727218 --- /dev/null +++ b/packages/features/verbandsverwaltung/src/components/cross-org-member-search.tsx @@ -0,0 +1,595 @@ +'use client'; + +import { useCallback, useState } from 'react'; + +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; + +import { ArrowRightLeft, Search, Users } from 'lucide-react'; +import { useAction } from 'next-safe-action/hooks'; +import { useForm } from 'react-hook-form'; + +import { formatDate } from '@kit/shared/dates'; +import { Badge } from '@kit/ui/badge'; +import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@kit/ui/dialog'; +import { Input } from '@kit/ui/input'; +import { toast } from '@kit/ui/sonner'; +import { Textarea } from '@kit/ui/textarea'; + +import { + getTransferPreview, + transferMember, +} from '../server/actions/hierarchy-actions'; +import type { CrossOrgMember, TransferPreview } from '../server/api'; + +const STATUS_LABELS: Record = { + active: 'Aktiv', + inactive: 'Inaktiv', + pending: 'Ausstehend', + resigned: 'Ausgetreten', + excluded: 'Ausgeschlossen', + deceased: 'Verstorben', +}; + +const STATUS_COLORS: Record = { + active: 'default', + inactive: 'secondary', + pending: 'outline', + resigned: 'secondary', + excluded: 'destructive', + deceased: 'secondary', +}; + +interface CrossOrgMemberSearchProps { + account: string; + members: CrossOrgMember[]; + total: number; + page: number; + pageSize: number; + childAccounts: Array<{ id: string; name: string }>; +} + +export function CrossOrgMemberSearch({ + account, + members, + total, + page, + pageSize, + childAccounts, +}: CrossOrgMemberSearchProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const [transferTarget, setTransferTarget] = useState( + null, + ); + const [targetAccountId, setTargetAccountId] = useState(''); + const [transferReason, setTransferReason] = useState(''); + const [keepSepa, setKeepSepa] = useState(true); + const [preview, setPreview] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + + const currentSearch = searchParams.get('q') ?? ''; + const currentStatus = searchParams.get('status') ?? ''; + const currentAccount = searchParams.get('accountId') ?? ''; + + const totalPages = Math.ceil(total / pageSize); + + const form = useForm({ + defaultValues: { search: currentSearch }, + }); + + const { execute: executePreview } = useAction(getTransferPreview, { + onSuccess: ({ data }) => { + if (data) setPreview(data); + setPreviewLoading(false); + }, + onError: () => { + setPreviewLoading(false); + }, + }); + + const { execute: executeTransfer, isPending: isTransferring } = useAction( + transferMember, + { + onSuccess: () => { + toast.success('Mitglied erfolgreich transferiert'); + setTransferTarget(null); + setTargetAccountId(''); + setTransferReason(''); + setKeepSepa(true); + setPreview(null); + }, + onError: ({ error }) => { + toast.error(error.serverError ?? 'Fehler beim Transfer'); + }, + }, + ); + + const buildUrl = useCallback( + (params: Record) => { + const urlSearchParams = new URLSearchParams(searchParams.toString()); + + for (const [key, val] of Object.entries(params)) { + if (val === null || val === '' || val === undefined) { + urlSearchParams.delete(key); + } else { + urlSearchParams.set(key, String(val)); + } + } + + return `/home/${account}/verband/members?${urlSearchParams.toString()}`; + }, + [account, searchParams], + ); + + const handleSearch = useCallback( + (values: { search: string }) => { + router.push(buildUrl({ q: values.search || null, page: null })); + }, + [router, buildUrl], + ); + + const handleStatusChange = useCallback( + (e: React.ChangeEvent) => { + router.push(buildUrl({ status: e.target.value || null, page: null })); + }, + [router, buildUrl], + ); + + const handleAccountChange = useCallback( + (e: React.ChangeEvent) => { + router.push(buildUrl({ accountId: e.target.value || null, page: null })); + }, + [router, buildUrl], + ); + + const handlePageChange = useCallback( + (newPage: number) => { + router.push(buildUrl({ page: newPage > 1 ? newPage : null })); + }, + [router, buildUrl], + ); + + function handleTransferConfirm() { + if (!transferTarget || !targetAccountId) return; + + executeTransfer({ + memberId: transferTarget.id, + targetAccountId, + reason: transferReason || undefined, + keepSepa, + }); + } + + // Accounts available as transfer targets (exclude member's current account) + const transferTargetAccounts = transferTarget + ? childAccounts.filter((a) => a.id !== transferTarget.account_id) + : []; + + return ( + + {/* Search + Filters */} + + + + + + Suchen + + + + + + Alle Status + {Object.entries(STATUS_LABELS).map(([val, label]) => ( + + {label} + + ))} + + + {childAccounts.length > 0 && ( + + Alle Organisationen + {childAccounts.map((childAccount) => ( + + {childAccount.name} + + ))} + + )} + + + + {/* Results */} + + + + + Mitglieder ({total}) + + + + {members.length === 0 ? ( + + + Keine Mitglieder gefunden + + + {currentSearch + ? 'Versuchen Sie einen anderen Suchbegriff.' + : 'In den verknüpften Organisationen sind noch keine Mitglieder vorhanden.'} + + + ) : ( + + + + + Name + Organisation + E-Mail + Ort + Status + Eintritt + Aktion + + + + {members.map((member) => ( + + + {member.last_name}, {member.first_name} + {member.member_number && ( + + #{member.member_number} + + )} + + + {member.account_slug ? ( + + {member.account_name} + + ) : ( + {member.account_name} + )} + + + {member.email ?? '—'} + + + {member.city ?? '—'} + + + + {STATUS_LABELS[member.status] ?? member.status} + + + + {formatDate(member.entry_date)} + + + { + setTransferTarget(member); + setTargetAccountId(''); + setTransferReason(''); + setPreview(null); + setPreviewLoading(true); + executePreview({ memberId: member.id }); + }} + title="Mitglied transferieren" + data-test="transfer-member-btn" + > + + + + + ))} + + + + )} + + {/* Pagination */} + {totalPages > 1 && ( + + + Seite {page} von {totalPages} ({total} Einträge) + + + handlePageChange(page - 1)} + > + Zurück + + = totalPages} + onClick={() => handlePageChange(page + 1)} + > + Weiter + + + + )} + + + + {/* Transfer Dialog */} + { + if (!open) setTransferTarget(null); + }} + > + + + Mitglied transferieren + + {transferTarget && ( + <> + + {transferTarget.first_name} {transferTarget.last_name} + {' '} + wird von {transferTarget.account_name} in + eine andere Organisation verschoben. + > + )} + + + + + {/* Preview: side effects */} + {previewLoading && ( + + Lade Transfervorschau... + + )} + + {preview && !previewLoading && ( + + {/* Active courses */} + {preview.courses.length > 0 && ( + + + {preview.courses.length} aktive Kurseinschreibung(en) + + + {preview.courses.map((course) => ( + + {course.name} ({course.accountName}) + + bleibt erhalten + + + ))} + + + )} + + {/* Open invoices */} + {preview.invoices.length > 0 && ( + + + {preview.invoices.length} offene Rechnung(en) + + + {preview.invoices.map((inv) => ( + + {inv.invoiceNumber} — {inv.amount.toFixed(2)} EUR ( + {inv.accountName}) + + verbleibt beim Quellverein + + + ))} + + + )} + + {/* SEPA mandates */} + {preview.mandates.length > 0 && ( + + + {preview.mandates.length} aktive(s) SEPA-Mandat(e) + + + {preview.mandates.map((mandate) => ( + + {mandate.reference} (Status: {mandate.status}) + + wird zurückgesetzt + + + ))} + + + )} + + {/* Newsletters */} + {preview.newsletters.length > 0 && ( + + + {preview.newsletters.length} Newsletter-Abonnement(s) + + + {preview.newsletters.map((newsletter) => ( + + {newsletter.name} ({newsletter.accountName}) + + bleibt erhalten + + + ))} + + + )} + + {/* What gets cleared */} + + Wird zurückgesetzt: + + {preview.member.memberNumber && ( + + Mitgliedsnr.{' '} + #{preview.member.memberNumber} — + Neuvergabe im Zielverein nötig + + )} + {preview.member.hasDuesCategory && ( + + Beitragskategorie — muss im Zielverein neu zugewiesen + werden + + )} + + SEPA-Mandatstatus → "ausstehend" (Neubestätigung + nötig) + + + + + {/* No side effects */} + {preview.courses.length === 0 && + preview.invoices.length === 0 && + preview.mandates.length === 0 && + preview.newsletters.length === 0 && ( + + + Keine aktiven Verknüpfungen gefunden + + + Transfer kann ohne Seiteneffekte durchgeführt werden. + + + )} + + )} + + + + Zielorganisation + + setTargetAccountId(e.target.value)} + className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm" + data-test="transfer-target-select" + > + Organisation auswählen... + {transferTargetAccounts.map((targetAccount) => ( + + {targetAccount.name} + + ))} + + + + + + setKeepSepa(e.target.checked)} + className="rounded border-gray-300" + data-test="transfer-keep-sepa" + /> + SEPA-Bankdaten (IBAN/BIC) übernehmen + + + Bankverbindung wird übernommen, Mandat muss im Zielverein neu + bestätigt werden. + + + + + + Grund (optional) + + setTransferReason(e.target.value)} + placeholder="z.B. Umzug, Vereinswechsel..." + rows={2} + data-test="transfer-reason-input" + /> + + + + + setTransferTarget(null)} + disabled={isTransferring} + > + Abbrechen + + + + {isTransferring ? 'Wird transferiert...' : 'Transferieren'} + + + + + + ); +} diff --git a/packages/features/verbandsverwaltung/src/components/hierarchy-events.tsx b/packages/features/verbandsverwaltung/src/components/hierarchy-events.tsx new file mode 100644 index 000000000..c4c952fe3 --- /dev/null +++ b/packages/features/verbandsverwaltung/src/components/hierarchy-events.tsx @@ -0,0 +1,313 @@ +'use client'; + +import { useCallback } from 'react'; + +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; + +import { CalendarDays, Share2 } from 'lucide-react'; +import { useForm } from 'react-hook-form'; + +import { formatDate } from '@kit/shared/dates'; +import { formatCurrencyAmount } from '@kit/shared/formatters'; +import { Badge } from '@kit/ui/badge'; +import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; +import { Input } from '@kit/ui/input'; + +interface HierarchyEvent { + id: string; + account_id: string; + account_name: string; + name: string; + description: string | null; + event_date: string; + event_time: string | null; + end_date: string | null; + location: string | null; + capacity: number | null; + fee: number; + status: string; + registration_deadline: string | null; + registration_count: number; + shared_with_hierarchy: boolean; +} + +interface HierarchyEventsProps { + account: string; + events: HierarchyEvent[]; + total: number; + page: number; + pageSize: number; +} + +const STATUS_LABELS: Record = { + planned: 'Geplant', + open: 'Offen', + full: 'Ausgebucht', + running: 'Laufend', + completed: 'Abgeschlossen', + cancelled: 'Abgesagt', +}; + +const STATUS_COLORS: Record = { + planned: 'outline', + open: 'default', + full: 'secondary', + running: 'default', + completed: 'secondary', + cancelled: 'destructive', +}; + +export function HierarchyEvents({ + account, + events, + total, + page, + pageSize, +}: HierarchyEventsProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const currentStatus = searchParams.get('status') ?? ''; + const currentSharedOnly = searchParams.get('sharedOnly') === 'true'; + const currentFromDate = searchParams.get('fromDate') ?? ''; + + const totalPages = Math.ceil(total / pageSize); + + const form = useForm({ + defaultValues: { fromDate: currentFromDate }, + }); + + const buildUrl = useCallback( + (params: Record) => { + const urlSearchParams = new URLSearchParams(searchParams.toString()); + + for (const [key, val] of Object.entries(params)) { + if (val === null || val === '' || val === undefined || val === false) { + urlSearchParams.delete(key); + } else { + urlSearchParams.set(key, String(val)); + } + } + + return `/home/${account}/verband/events?${urlSearchParams.toString()}`; + }, + [account, searchParams], + ); + + const handleStatusChange = useCallback( + (e: React.ChangeEvent) => { + router.push(buildUrl({ status: e.target.value || null, page: null })); + }, + [router, buildUrl], + ); + + const handleSharedToggle = useCallback(() => { + router.push( + buildUrl({ + sharedOnly: !currentSharedOnly || null, + page: null, + }), + ); + }, [router, buildUrl, currentSharedOnly]); + + const handleDateFilter = useCallback( + (values: { fromDate: string }) => { + router.push(buildUrl({ fromDate: values.fromDate || null, page: null })); + }, + [router, buildUrl], + ); + + const handlePageChange = useCallback( + (newPage: number) => { + router.push(buildUrl({ page: newPage > 1 ? newPage : null })); + }, + [router, buildUrl], + ); + + return ( + + {/* Filters */} + + + + + Filtern + + + + + + Alle Status + {Object.entries(STATUS_LABELS).map(([val, label]) => ( + + {label} + + ))} + + + + + Nur geteilte + + + + + {/* Results */} + + + + + Veranstaltungen ({total}) + + + + {events.length === 0 ? ( + + + Keine Veranstaltungen gefunden + + + {currentStatus || currentSharedOnly || currentFromDate + ? 'Versuchen Sie andere Filterkriterien.' + : 'In den verknüpften Organisationen sind noch keine Veranstaltungen vorhanden.'} + + + ) : ( + + + + + Veranstaltung + Organisation + Datum + Ort + Kapazität + Gebühr + Status + Geteilt + + + + {events.map((evt) => ( + + + + {evt.name} + + + {evt.account_name} + + {formatDate(evt.event_date)} + {evt.event_time && ( + {evt.event_time} + )} + + + {evt.location ?? '—'} + + + = evt.capacity + ? 'font-semibold text-red-600' + : '' + } + > + {evt.registration_count} + + {evt.capacity != null && ( + + /{evt.capacity} + + )} + + + {formatCurrencyAmount(evt.fee)} + + + + {STATUS_LABELS[evt.status] ?? evt.status} + + + + {evt.shared_with_hierarchy && ( + + + Geteilt + + )} + + + ))} + + + + )} + + {/* Pagination */} + {totalPages > 1 && ( + + + Seite {page} von {totalPages} ({total} Einträge) + + + handlePageChange(page - 1)} + > + Zurück + + = totalPages} + onClick={() => handlePageChange(page + 1)} + > + Weiter + + + + )} + + + + ); +} diff --git a/packages/features/verbandsverwaltung/src/components/hierarchy-report.tsx b/packages/features/verbandsverwaltung/src/components/hierarchy-report.tsx new file mode 100644 index 000000000..b8a208ac4 --- /dev/null +++ b/packages/features/verbandsverwaltung/src/components/hierarchy-report.tsx @@ -0,0 +1,268 @@ +'use client'; + +import { + Building2, + Users, + CalendarDays, + BookOpen, + Euro, + TrendingUp, +} from 'lucide-react'; + +import { formatNumber, formatCurrencyAmount } from '@kit/shared/formatters'; +import { Badge } from '@kit/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; + +interface HierarchyReportProps { + summary: { + total_orgs: number; + total_active_members: number; + total_members: number; + new_members_this_year: number; + total_upcoming_events: number; + total_active_courses: number; + total_open_invoices: number; + total_open_invoice_amount: number; + total_sepa_batches_this_year: number; + }; + report: Array<{ + org_id: string; + org_name: string; + org_slug: string | null; + depth: number; + active_members: number; + inactive_members: number; + total_members: number; + new_members_this_year: number; + active_courses: number; + upcoming_events: number; + open_invoices: number; + open_invoice_amount: number; + sepa_batches_this_year: number; + }>; +} + +function getDepthLabel(depth: number) { + switch (depth) { + case 0: + return 'Verband'; + case 1: + return 'Unterverband'; + default: + return 'Verein'; + } +} + +function getDepthVariant(depth: number) { + switch (depth) { + case 0: + return 'default' as const; + case 1: + return 'secondary' as const; + default: + return 'outline' as const; + } +} + +export function HierarchyReport({ summary, report }: HierarchyReportProps) { + return ( + + {/* Summary Cards */} + + + + + + + Organisationen + + + {formatNumber(summary.total_orgs)} + + + + + + + + + + + + + + + Aktive Mitglieder + + + {formatNumber(summary.total_active_members)} + + + von {formatNumber(summary.total_members)} gesamt + + + + + + + + + + + + + + + Neue Mitglieder (Jahr) + + + {formatNumber(summary.new_members_this_year)} + + + + + + + + + + + + + + + Anstehende Termine + + + {formatNumber(summary.total_upcoming_events)} + + + + + + + + + + + + + + + Aktive Kurse + + + {formatNumber(summary.total_active_courses)} + + + + + + + + + + + + + + + Offene Rechnungen + + + {formatCurrencyAmount(summary.total_open_invoice_amount)} + + + {formatNumber(summary.total_open_invoices)} Rechnungen + + + + + + + + + + + {/* Per-Org Report Table */} + + + Bericht pro Organisation + + + {report.length === 0 ? ( + + + Keine Organisationen vorhanden + + + Die Hierarchie enthält noch keine Organisationen. + + + ) : ( + + + + + Name + Ebene + + Aktive Mitgl. + + Gesamt + Neu (Jahr) + Kurse + Termine + + Offene Rechn. + + + Offener Betrag + + + + + {report.map((row) => ( + + + + {row.org_name} + + + + + {getDepthLabel(row.depth)} + + + + {formatNumber(row.active_members)} + + + {formatNumber(row.total_members)} + + + {formatNumber(row.new_members_this_year)} + + + {formatNumber(row.active_courses)} + + + {formatNumber(row.upcoming_events)} + + + {formatNumber(row.open_invoices)} + + + {formatCurrencyAmount(row.open_invoice_amount)} + + + ))} + + + + )} + + + + ); +} diff --git a/packages/features/verbandsverwaltung/src/components/hierarchy-tree.tsx b/packages/features/verbandsverwaltung/src/components/hierarchy-tree.tsx index d413ca816..9baed0509 100644 --- a/packages/features/verbandsverwaltung/src/components/hierarchy-tree.tsx +++ b/packages/features/verbandsverwaltung/src/components/hierarchy-tree.tsx @@ -62,6 +62,7 @@ function TreeNodeRow({ > {hasChildren ? ( setExpanded(!expanded)} className="text-muted-foreground hover:text-foreground" > @@ -108,6 +109,7 @@ function TreeNodeRow({ {!isRoot && ( setSelectedAccountId(e.target.value)} className="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm" > Organisation auswählen... - {availableAccounts.map((a) => ( - - {a.name} - {a.slug ? ` (/${a.slug})` : ''} + {availableAccounts.map((availableAccount) => ( + + {availableAccount.name} + {availableAccount.slug + ? ` (/${availableAccount.slug})` + : ''} ))} diff --git a/packages/features/verbandsverwaltung/src/components/index.ts b/packages/features/verbandsverwaltung/src/components/index.ts index 604ef2b0e..04f3456db 100644 --- a/packages/features/verbandsverwaltung/src/components/index.ts +++ b/packages/features/verbandsverwaltung/src/components/index.ts @@ -6,3 +6,7 @@ export { ClubContactsManager } from './club-contacts-manager'; export { ClubFeeBillingTable } from './club-fee-billing-table'; export { ClubNotesList } from './club-notes-list'; export { HierarchyTree } from './hierarchy-tree'; +export { CrossOrgMemberSearch } from './cross-org-member-search'; +export { HierarchyEvents } from './hierarchy-events'; +export { HierarchyReport } from './hierarchy-report'; +export { SharedTemplates } from './shared-templates'; diff --git a/packages/features/verbandsverwaltung/src/components/shared-templates.tsx b/packages/features/verbandsverwaltung/src/components/shared-templates.tsx new file mode 100644 index 000000000..0848563a2 --- /dev/null +++ b/packages/features/verbandsverwaltung/src/components/shared-templates.tsx @@ -0,0 +1,263 @@ +'use client'; + +import { useState } from 'react'; + +import { Copy, FileText, Mail } from 'lucide-react'; +import { useAction } from 'next-safe-action/hooks'; + +import { formatDate } from '@kit/shared/dates'; +import { Badge } from '@kit/ui/badge'; +import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@kit/ui/dialog'; +import { Input } from '@kit/ui/input'; +import { toast } from '@kit/ui/sonner'; + +import { cloneTemplate } from '../server/actions/hierarchy-actions'; + +interface SharedTemplate { + id: string; + account_id: string; + account_name: string; + template_source: 'newsletter' | 'document'; + name: string; + description: string | null; + template_type: string; + shared_with_hierarchy: boolean; + created_at: string; +} + +interface SharedTemplatesProps { + accountId: string; + templates: SharedTemplate[]; +} + +type FilterType = 'all' | 'newsletter' | 'document'; + +export function SharedTemplates({ + accountId, + templates, +}: SharedTemplatesProps) { + const [filter, setFilter] = useState('all'); + const [cloneDialogOpen, setCloneDialogOpen] = useState(false); + const [selectedTemplate, setSelectedTemplate] = + useState(null); + const [newName, setNewName] = useState(''); + + const { execute: executeClone, isPending: isCloning } = useAction( + cloneTemplate, + { + onSuccess: () => { + toast.success('Vorlage wurde erfolgreich geklont'); + setCloneDialogOpen(false); + setSelectedTemplate(null); + setNewName(''); + }, + onError: ({ error }) => { + toast.error(error.serverError ?? 'Fehler beim Klonen der Vorlage'); + }, + }, + ); + + const filteredTemplates = templates.filter((t) => { + if (filter === 'all') return true; + return t.template_source === filter; + }); + + function openCloneDialog(template: SharedTemplate) { + setSelectedTemplate(template); + setNewName(`${template.name} (Kopie)`); + setCloneDialogOpen(true); + } + + function handleClone() { + if (!selectedTemplate) return; + + executeClone({ + templateType: selectedTemplate.template_source, + templateId: selectedTemplate.id, + targetAccountId: accountId, + newName: newName.trim() || undefined, + }); + } + + const filterButtons: { value: FilterType; label: string }[] = [ + { value: 'all', label: 'Alle' }, + { value: 'newsletter', label: 'Newsletter' }, + { value: 'document', label: 'Dokumente' }, + ]; + + function getTemplateTypeLabel(type: string): string { + const labels: Record = { + generic: 'Allgemein', + member_card: 'Mitgliedsausweis', + invoice: 'Rechnung', + certificate: 'Urkunde', + confirmation: 'Bestätigung', + letter: 'Brief', + }; + return labels[type] ?? type; + } + + return ( + <> + + + + Geteilte Vorlagen + + {filterButtons.map((btn) => ( + setFilter(btn.value)} + > + {btn.label} + + ))} + + + + + {filteredTemplates.length === 0 ? ( + + + + Keine geteilten Vorlagen vorhanden + + + Vorlagen, die von anderen Organisationen in Ihrer Hierarchie + geteilt werden, erscheinen hier. + + + ) : ( + + + + + Name + Typ + Template-Typ + Organisation + Erstellt + Aktion + + + + {filteredTemplates.map((template) => ( + + + + {template.template_source === 'newsletter' ? ( + + ) : ( + + )} + {template.name} + + {template.description && ( + + {template.description} + + )} + + + + {template.template_source === 'newsletter' + ? 'Newsletter' + : 'Dokument'} + + + + {getTemplateTypeLabel(template.template_type)} + + + {template.account_name} + + + {formatDate(template.created_at)} + + + openCloneDialog(template)} + > + + Klonen + + + + ))} + + + + )} + + + + + + + Vorlage klonen + + Erstellen Sie eine Kopie der Vorlage{' '} + {selectedTemplate?.name} in Ihrer Organisation. + + + + + Name der Kopie + + setNewName(e.target.value)} + placeholder="Name der neuen Vorlage" + maxLength={200} + /> + + + setCloneDialogOpen(false)} + disabled={isCloning} + > + Abbrechen + + + {isCloning ? 'Wird geklont...' : 'Vorlage klonen'} + + + + + > + ); +} diff --git a/packages/features/verbandsverwaltung/src/components/verband-tab-navigation.tsx b/packages/features/verbandsverwaltung/src/components/verband-tab-navigation.tsx index 0e4636fa1..e6f292bba 100644 --- a/packages/features/verbandsverwaltung/src/components/verband-tab-navigation.tsx +++ b/packages/features/verbandsverwaltung/src/components/verband-tab-navigation.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; +import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; interface VerbandTabNavigationProps { @@ -10,11 +11,15 @@ interface VerbandTabNavigationProps { } const TABS = [ - { id: 'overview', label: 'Übersicht', path: '' }, - { id: 'clubs', label: 'Vereine', path: '/clubs' }, - { id: 'hierarchy', label: 'Hierarchie', path: '/hierarchy' }, - { id: 'statistics', label: 'Statistik', path: '/statistics' }, - { id: 'settings', label: 'Einstellungen', path: '/settings' }, + { id: 'overview', i18nKey: 'verband:nav.overview', path: '' }, + { id: 'clubs', i18nKey: 'verband:nav.clubs', path: '/clubs' }, + { id: 'hierarchy', i18nKey: 'verband:nav.hierarchy', path: '/hierarchy' }, + { id: 'members', i18nKey: 'verband:nav.memberSearch', path: '/members' }, + { id: 'events', i18nKey: 'verband:nav.events', path: '/events' }, + { id: 'reporting', i18nKey: 'verband:nav.reporting', path: '/reporting' }, + { id: 'templates', i18nKey: 'verband:nav.templates', path: '/templates' }, + { id: 'statistics', i18nKey: 'verband:nav.statistics', path: '/statistics' }, + { id: 'settings', i18nKey: 'verband:nav.settings', path: '/settings' }, ] as const; export function VerbandTabNavigation({ @@ -27,7 +32,7 @@ export function VerbandTabNavigation({ {TABS.map((tab) => { const isActive = tab.id === activeTab; @@ -43,7 +48,7 @@ export function VerbandTabNavigation({ : 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent', )} > - {tab.label} + ); })} diff --git a/packages/features/verbandsverwaltung/src/schema/verband.schema.ts b/packages/features/verbandsverwaltung/src/schema/verband.schema.ts index 4f024c7b7..8ece9e976 100644 --- a/packages/features/verbandsverwaltung/src/schema/verband.schema.ts +++ b/packages/features/verbandsverwaltung/src/schema/verband.schema.ts @@ -218,3 +218,35 @@ export const RemoveAccountParentSchema = z.object({ export type RemoveAccountParentInput = z.infer< typeof RemoveAccountParentSchema >; + +// ===================================================== +// Member Transfer +// ===================================================== + +export const TransferPreviewSchema = z.object({ + memberId: z.string().uuid(), +}); + +export type TransferPreviewInput = z.infer; + +export const TransferMemberSchema = z.object({ + memberId: z.string().uuid(), + targetAccountId: z.string().uuid(), + reason: z.string().max(500).optional(), + keepSepa: z.boolean().default(true), +}); + +export type TransferMemberInput = z.infer; + +// ===================================================== +// Clone Template +// ===================================================== + +export const CloneTemplateSchema = z.object({ + templateType: z.enum(['newsletter', 'document']), + templateId: z.string().uuid(), + targetAccountId: z.string().uuid(), + newName: z.string().max(200).optional(), +}); + +export type CloneTemplateInput = z.infer; diff --git a/packages/features/verbandsverwaltung/src/server/actions/hierarchy-actions.ts b/packages/features/verbandsverwaltung/src/server/actions/hierarchy-actions.ts index 2fe3969fb..669a79591 100644 --- a/packages/features/verbandsverwaltung/src/server/actions/hierarchy-actions.ts +++ b/packages/features/verbandsverwaltung/src/server/actions/hierarchy-actions.ts @@ -9,7 +9,11 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { SetAccountParentSchema, RemoveAccountParentSchema, + TransferPreviewSchema, + TransferMemberSchema, + CloneTemplateSchema, } from '../../schema/verband.schema'; +import { createVerbandApi } from '../api'; const REVALIDATE_PATH = '/home/[account]/verband'; @@ -124,3 +128,79 @@ export const unlinkChildAccount = authActionClient revalidatePath(REVALIDATE_PATH, 'page'); return { success: true }; }); + +export const getTransferPreview = authActionClient + .inputSchema(TransferPreviewSchema) + .action(async ({ parsedInput: input }) => { + const client = getSupabaseServerClient(); + const api = createVerbandApi(client); + + return api.getTransferPreview(input.memberId); + }); + +export const transferMember = authActionClient + .inputSchema(TransferMemberSchema) + .action(async ({ parsedInput: input, ctx }) => { + const client = getSupabaseServerClient(); + const logger = await getLogger(); + const api = createVerbandApi(client); + + logger.info( + { + name: 'verband.member.transfer', + memberId: input.memberId, + targetAccountId: input.targetAccountId, + userId: ctx.user.id, + }, + 'Transferring member to another organisation...', + ); + + try { + const { transferId } = await api.transferMember( + input.memberId, + input.targetAccountId, + input.reason, + input.keepSepa, + ); + + logger.info( + { name: 'verband.member.transfer', transferId }, + 'Member transferred successfully', + ); + + revalidatePath(REVALIDATE_PATH, 'page'); + return { success: true, transferId }; + } catch (err) { + const message = + err instanceof Error ? err.message : 'Fehler beim Transfer'; + logger.error({ error: err }, 'Member transfer failed'); + throw new Error(message); + } + }); + +export const cloneTemplate = authActionClient + .inputSchema(CloneTemplateSchema) + .action(async ({ parsedInput: input }) => { + const client = getSupabaseServerClient(); + const logger = await getLogger(); + const api = createVerbandApi(client); + + logger.info( + { + name: 'verband.template.clone', + templateId: input.templateId, + targetAccountId: input.targetAccountId, + }, + 'Cloning shared template...', + ); + + const { newTemplateId } = await api.cloneTemplate( + input.templateType, + input.templateId, + input.targetAccountId, + input.newName, + ); + + revalidatePath(REVALIDATE_PATH, 'page'); + return { success: true, newTemplateId }; + }); diff --git a/packages/features/verbandsverwaltung/src/server/api.ts b/packages/features/verbandsverwaltung/src/server/api.ts index 9a2ac4701..461ad37ab 100644 --- a/packages/features/verbandsverwaltung/src/server/api.ts +++ b/packages/features/verbandsverwaltung/src/server/api.ts @@ -787,16 +787,16 @@ export function createVerbandApi(client: SupabaseClient) { async getParentAccount( accountId: string, ): Promise { - const { data: acct, error: acctError } = await client + const { data: accountData, error: accountError } = await client .from('accounts') .select( 'id, name, slug, email, is_personal_account, parent_account_id, created_at' as '*', ) .eq('id', accountId) .single(); - if (acctError) throw acctError; + if (accountError) throw accountError; - const account = acct as unknown as HierarchyAccount; + const account = accountData as unknown as HierarchyAccount; if (!account.parent_account_id) return null; const { data, error } = await client @@ -880,14 +880,14 @@ export function createVerbandApi(client: SupabaseClient) { } // Fallback - const { data: fallback, error: fbErr } = await client + const { data: fallback, error: fallbackError } = await client .from('accounts') .select( 'id, name, slug, email, is_personal_account, parent_account_id, created_at' as '*', ) .eq('id', rootAccountId) .single(); - if (fbErr) throw fbErr; + if (fallbackError) throw fallbackError; return { ...(fallback as unknown as HierarchyAccount), children: [], @@ -935,5 +935,478 @@ export function createVerbandApi(client: SupabaseClient) { if (error) throw error; return (data ?? []) as unknown as HierarchyAccount[]; }, + + // ===================================================== + // Cross-Org Member Search + // ===================================================== + + async searchMembersAcrossHierarchy( + rootAccountId: string, + opts?: { + search?: string; + status?: string; + accountId?: string; + page?: number; + pageSize?: number; + }, + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc( + 'search_members_across_hierarchy' as any, + { + root_account_id: rootAccountId, + search_term: opts?.search || null, + status_filter: opts?.status || null, + account_filter: opts?.accountId || null, + page_number: opts?.page ?? 1, + page_size: opts?.pageSize ?? 25, + }, + ); + + if (error) throw error; + + const rows = (data ?? []) as unknown as CrossOrgMemberRow[]; + const total = rows.length > 0 ? Number(rows[0]!.total_count) : 0; + + return { + data: rows.map(({ total_count: _, ...member }) => member), + total, + }; + }, + + // ===================================================== + // Member Transfer + // ===================================================== + + /** + * Pre-flight check: fetch all active relationships for a member + * so the admin can see side effects before confirming transfer. + */ + async getTransferPreview(memberId: string): Promise { + // Fetch member basic info + const { data: member } = await client + .from('members') + .select( + 'id, first_name, last_name, account_id, email, iban, sepa_mandate_status, dues_category_id, member_number', + ) + .eq('id', memberId) + .single(); + + if (!member) throw new Error('Mitglied nicht gefunden'); + + // Active course enrollments + const { data: courses } = await client + .from('course_participants') + .select( + 'id, course_id, status, courses(id, name, account_id, start_date, end_date, accounts(name))', + ) + .eq('member_id', memberId) + .eq('status', 'enrolled'); + + // Open invoices referencing this member + const { data: invoices } = await client + .from('invoices') + .select( + 'id, invoice_number, total_amount, status, account_id, accounts(name)', + ) + .eq('member_id', memberId) + .in('status', ['draft', 'sent', 'overdue']); + + // Active SEPA mandates + const { data: mandates } = await client + .from('sepa_mandates') + .select('id, mandate_reference, status, account_id') + .eq('member_id', memberId) + .in('status', ['active', 'pending']); + + // Newsletter subscriptions + const { data: newsletters } = await client + .from('newsletter_recipients') + .select( + 'id, newsletter_id, newsletters(id, name, account_id, accounts(name))', + ) + .eq('member_id', memberId); + + const courseList = (courses ?? []).map((courseParticipant) => { + const course = courseParticipant.courses as Record< + string, + unknown + > | null; + const courseAccount = course?.accounts as Record< + string, + unknown + > | null; + return { + id: String(courseParticipant.id), + name: String(course?.name ?? '—'), + accountName: String(courseAccount?.name ?? '—'), + startDate: course?.start_date as string | null, + endDate: course?.end_date as string | null, + survives: true, // FK is member_id, enrollment stays + }; + }); + + const invoiceList = (invoices ?? []).map((invoiceRecord) => { + const invAccount = invoiceRecord.accounts as Record< + string, + unknown + > | null; + return { + id: String(invoiceRecord.id), + invoiceNumber: String(invoiceRecord.invoice_number ?? '—'), + amount: Number(invoiceRecord.total_amount ?? 0), + status: String(invoiceRecord.status), + accountName: String(invAccount?.name ?? '—'), + survives: true, // stays in source org, member_id FK preserved + }; + }); + + const mandateList = (mandates ?? []).map((mandateRecord) => ({ + id: String(mandateRecord.id), + reference: String(mandateRecord.mandate_reference ?? '—'), + status: String(mandateRecord.status), + survives: false, // mandate is org-specific, gets reset + })); + + const newsletterList = (newsletters ?? []).map((recipientRecord) => { + const newsletterData = recipientRecord.newsletters as Record< + string, + unknown + > | null; + const newsletterAccountData = newsletterData?.accounts as Record< + string, + unknown + > | null; + return { + id: String(recipientRecord.id), + name: String(newsletterData?.name ?? '—'), + accountName: String(newsletterAccountData?.name ?? '—'), + survives: true, // FK on member_id, stays linked + }; + }); + + return { + member: { + id: String(member.id), + firstName: String(member.first_name), + lastName: String(member.last_name), + email: member.email as string | null, + hasIban: !!member.iban, + hasMandate: + member.sepa_mandate_status !== 'pending' && + !!member.sepa_mandate_status, + hasDuesCategory: !!member.dues_category_id, + memberNumber: member.member_number as string | null, + }, + courses: courseList, + invoices: invoiceList, + mandates: mandateList, + newsletters: newsletterList, + }; + }, + + async transferMember( + memberId: string, + targetAccountId: string, + reason?: string, + keepSepa = true, + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc('transfer_member' as any, { + p_member_id: memberId, + p_target_account_id: targetAccountId, + p_reason: reason || null, + p_keep_sepa: keepSepa, + }); + + if (error) throw error; + return { transferId: data as unknown as string }; + }, + + async getMemberTransferHistory(memberId: string) { + const { data, error } = await client + .from('member_transfers' as string) + .select( + 'id, member_id, source_account_id, target_account_id, reason, transferred_at' as '*', + ) + .eq('member_id', memberId) + .order('transferred_at', { ascending: false }); + + if (error) throw error; + return (data ?? []) as unknown as MemberTransfer[]; + }, + + // ===================================================== + // Shared Events (Hierarchy) + // ===================================================== + + async listHierarchyEvents( + rootAccountId: string, + opts?: { + fromDate?: string; + status?: string; + sharedOnly?: boolean; + page?: number; + pageSize?: number; + }, + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc('list_hierarchy_events' as any, { + root_account_id: rootAccountId, + p_from_date: opts?.fromDate || null, + p_status: opts?.status || null, + p_shared_only: opts?.sharedOnly ?? false, + p_page: opts?.page ?? 1, + p_page_size: opts?.pageSize ?? 25, + }); + + if (error) throw error; + const rows = (data ?? []) as unknown as HierarchyEventRow[]; + const total = rows.length > 0 ? Number(rows[0]!.total_count) : 0; + return { + data: rows.map(({ total_count: _, ...event }) => event), + total, + }; + }, + + // ===================================================== + // Consolidated SEPA Billing + // ===================================================== + + async listSepaEligibleMembers( + rootAccountId: string, + accountFilter?: string, + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc( + 'list_hierarchy_sepa_eligible_members' as any, + { + root_account_id: rootAccountId, + p_account_filter: accountFilter || null, + }, + ); + + if (error) throw error; + return (data ?? []) as unknown as SepaEligibleMember[]; + }, + + // ===================================================== + // Aggregated Reporting + // ===================================================== + + async getHierarchyReport(rootAccountId: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc('get_hierarchy_report' as any, { + root_account_id: rootAccountId, + }); + + if (error) throw error; + return (data ?? []) as unknown as HierarchyReportRow[]; + }, + + async getHierarchySummary(rootAccountId: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc('get_hierarchy_summary' as any, { + root_account_id: rootAccountId, + }); + + if (error) throw error; + const rows = (data ?? []) as unknown as HierarchySummary[]; + return ( + rows[0] ?? { + total_orgs: 0, + total_active_members: 0, + total_members: 0, + new_members_this_year: 0, + total_upcoming_events: 0, + total_active_courses: 0, + total_open_invoices: 0, + total_open_invoice_amount: 0, + total_sepa_batches_this_year: 0, + } + ); + }, + + // ===================================================== + // Shared Templates + // ===================================================== + + async listSharedTemplates( + rootAccountId: string, + templateType?: 'newsletter' | 'document', + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc( + 'list_hierarchy_shared_templates' as any, + { + root_account_id: rootAccountId, + p_template_type: templateType || null, + }, + ); + + if (error) throw error; + return (data ?? []) as unknown as SharedTemplate[]; + }, + + async cloneTemplate( + templateType: 'newsletter' | 'document', + templateId: string, + targetAccountId: string, + newName?: string, + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await client.rpc('clone_template' as any, { + p_template_type: templateType, + p_template_id: templateId, + p_target_account_id: targetAccountId, + p_new_name: newName || null, + }); + + if (error) throw error; + return { newTemplateId: data as unknown as string }; + }, }; } + +export interface MemberTransfer { + id: string; + member_id: string; + source_account_id: string; + target_account_id: string; + reason: string | null; + transferred_at: string; +} + +export interface CrossOrgMemberRow { + id: string; + account_id: string; + account_name: string; + account_slug: string | null; + member_number: string | null; + first_name: string; + last_name: string; + email: string | null; + phone: string | null; + city: string | null; + status: string; + entry_date: string | null; + total_count: number; +} + +export type CrossOrgMember = Omit; + +export interface TransferPreview { + member: { + id: string; + firstName: string; + lastName: string; + email: string | null; + hasIban: boolean; + hasMandate: boolean; + hasDuesCategory: boolean; + memberNumber: string | null; + }; + courses: Array<{ + id: string; + name: string; + accountName: string; + startDate: string | null; + endDate: string | null; + survives: boolean; + }>; + invoices: Array<{ + id: string; + invoiceNumber: string; + amount: number; + status: string; + accountName: string; + survives: boolean; + }>; + mandates: Array<{ + id: string; + reference: string; + status: string; + survives: boolean; + }>; + newsletters: Array<{ + id: string; + name: string; + accountName: string; + survives: boolean; + }>; +} + +export interface HierarchyEventRow { + id: string; + account_id: string; + account_name: string; + name: string; + description: string | null; + event_date: string; + event_time: string | null; + end_date: string | null; + location: string | null; + capacity: number | null; + fee: number; + status: string; + registration_deadline: string | null; + registration_count: number; + shared_with_hierarchy: boolean; + total_count: number; +} + +export type HierarchyEvent = Omit; + +export interface SepaEligibleMember { + member_id: string; + account_id: string; + account_name: string; + first_name: string; + last_name: string; + iban: string; + bic: string | null; + account_holder: string | null; + mandate_id: string | null; + mandate_date: string | null; + dues_amount: number; +} + +export interface HierarchyReportRow { + org_id: string; + org_name: string; + org_slug: string | null; + depth: number; + active_members: number; + inactive_members: number; + total_members: number; + new_members_this_year: number; + active_courses: number; + upcoming_events: number; + open_invoices: number; + open_invoice_amount: number; + sepa_batches_this_year: number; +} + +export interface HierarchySummary { + total_orgs: number; + total_active_members: number; + total_members: number; + new_members_this_year: number; + total_upcoming_events: number; + total_active_courses: number; + total_open_invoices: number; + total_open_invoice_amount: number; + total_sepa_batches_this_year: number; +} + +export interface SharedTemplate { + id: string; + account_id: string; + account_name: string; + template_source: 'newsletter' | 'document'; + name: string; + description: string | null; + template_type: string; + shared_with_hierarchy: boolean; + created_at: string; +} diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 66b5fe309..8a49b1cc1 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -136,6 +136,7 @@ export type Database = { id: string is_personal_account: boolean name: string + parent_account_id: string | null picture_url: string | null primary_owner_user_id: string public_data: Json @@ -150,6 +151,7 @@ export type Database = { id?: string is_personal_account?: boolean name: string + parent_account_id?: string | null picture_url?: string | null primary_owner_user_id?: string public_data?: Json @@ -164,6 +166,7 @@ export type Database = { id?: string is_personal_account?: boolean name?: string + parent_account_id?: string | null picture_url?: string | null primary_owner_user_id?: string public_data?: Json @@ -171,7 +174,29 @@ export type Database = { updated_at?: string | null updated_by?: string | null } - Relationships: [] + Relationships: [ + { + foreignKeyName: "accounts_parent_account_id_fkey" + columns: ["parent_account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "accounts_parent_account_id_fkey" + columns: ["parent_account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "accounts_parent_account_id_fkey" + columns: ["parent_account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] } accounts_memberships: { Row: { @@ -232,6 +257,123 @@ export type Database = { }, ] } + association_history: { + Row: { + account_id: string + club_id: string | null + created_at: string + created_by: string | null + description: string + event_date: string + event_type: string + id: string + new_value: string | null + old_value: string | null + } + Insert: { + account_id: string + club_id?: string | null + created_at?: string + created_by?: string | null + description: string + event_date?: string + event_type?: string + id?: string + new_value?: string | null + old_value?: string | null + } + Update: { + account_id?: string + club_id?: string | null + created_at?: string + created_by?: string | null + description?: string + event_date?: string + event_type?: string + id?: string + new_value?: string | null + old_value?: string | null + } + Relationships: [ + { + foreignKeyName: "association_history_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_history_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_history_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_history_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + ] + } + association_types: { + Row: { + account_id: string + created_at: string + description: string | null + id: string + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + description?: string | null + id?: string + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + description?: string | null + id?: string + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "association_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "association_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } audit_log: { Row: { account_id: string @@ -429,6 +571,518 @@ export type Database = { }, ] } + catch_books: { + Row: { + account_id: string + card_numbers: string | null + created_at: string + created_by: string | null + fishing_days_count: number + id: string + is_checked: boolean + is_empty: boolean + is_fly_fisher: boolean + is_hejfish: boolean + is_submitted: boolean + member_birth_date: string | null + member_id: string + member_name: string | null + not_fished: boolean + remarks: string | null + status: Database["public"]["Enums"]["catch_book_status"] + submitted_at: string | null + total_fish_caught: number + updated_at: string + updated_by: string | null + verification: + | Database["public"]["Enums"]["catch_book_verification"] + | null + year: number + } + Insert: { + account_id: string + card_numbers?: string | null + created_at?: string + created_by?: string | null + fishing_days_count?: number + id?: string + is_checked?: boolean + is_empty?: boolean + is_fly_fisher?: boolean + is_hejfish?: boolean + is_submitted?: boolean + member_birth_date?: string | null + member_id: string + member_name?: string | null + not_fished?: boolean + remarks?: string | null + status?: Database["public"]["Enums"]["catch_book_status"] + submitted_at?: string | null + total_fish_caught?: number + updated_at?: string + updated_by?: string | null + verification?: + | Database["public"]["Enums"]["catch_book_verification"] + | null + year: number + } + Update: { + account_id?: string + card_numbers?: string | null + created_at?: string + created_by?: string | null + fishing_days_count?: number + id?: string + is_checked?: boolean + is_empty?: boolean + is_fly_fisher?: boolean + is_hejfish?: boolean + is_submitted?: boolean + member_birth_date?: string | null + member_id?: string + member_name?: string | null + not_fished?: boolean + remarks?: string | null + status?: Database["public"]["Enums"]["catch_book_status"] + submitted_at?: string | null + total_fish_caught?: number + updated_at?: string + updated_by?: string | null + verification?: + | Database["public"]["Enums"]["catch_book_verification"] + | null + year?: number + } + Relationships: [ + { + foreignKeyName: "catch_books_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catch_books_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catch_books_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catch_books_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + ] + } + catches: { + Row: { + catch_book_id: string + catch_date: string + competition_id: string | null + competition_participant_id: string | null + created_at: string + gender: Database["public"]["Enums"]["fish_gender"] | null + has_error: boolean + hejfish_id: string | null + id: string + is_empty_entry: boolean + length_cm: number | null + member_id: string | null + permit_id: string | null + quantity: number + remarks: string | null + size_category: + | Database["public"]["Enums"]["fish_size_category"] + | null + species_id: string + water_id: string | null + weight_g: number | null + } + Insert: { + catch_book_id: string + catch_date: string + competition_id?: string | null + competition_participant_id?: string | null + created_at?: string + gender?: Database["public"]["Enums"]["fish_gender"] | null + has_error?: boolean + hejfish_id?: string | null + id?: string + is_empty_entry?: boolean + length_cm?: number | null + member_id?: string | null + permit_id?: string | null + quantity?: number + remarks?: string | null + size_category?: + | Database["public"]["Enums"]["fish_size_category"] + | null + species_id: string + water_id?: string | null + weight_g?: number | null + } + Update: { + catch_book_id?: string + catch_date?: string + competition_id?: string | null + competition_participant_id?: string | null + created_at?: string + gender?: Database["public"]["Enums"]["fish_gender"] | null + has_error?: boolean + hejfish_id?: string | null + id?: string + is_empty_entry?: boolean + length_cm?: number | null + member_id?: string | null + permit_id?: string | null + quantity?: number + remarks?: string | null + size_category?: + | Database["public"]["Enums"]["fish_size_category"] + | null + species_id?: string + water_id?: string | null + weight_g?: number | null + } + Relationships: [ + { + foreignKeyName: "catches_catch_book_id_fkey" + columns: ["catch_book_id"] + isOneToOne: false + referencedRelation: "catch_books" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_permit_id_fkey" + columns: ["permit_id"] + isOneToOne: false + referencedRelation: "fishing_permits" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_species_id_fkey" + columns: ["species_id"] + isOneToOne: false + referencedRelation: "fish_species" + referencedColumns: ["id"] + }, + { + foreignKeyName: "catches_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk_catches_competition" + columns: ["competition_id"] + isOneToOne: false + referencedRelation: "competitions" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk_catches_competition_participant" + columns: ["competition_participant_id"] + isOneToOne: false + referencedRelation: "competition_participants" + referencedColumns: ["id"] + }, + ] + } + club_contacts: { + Row: { + club_id: string + created_at: string + email: string | null + first_name: string + id: string + is_active: boolean + last_name: string + mobile: string | null + phone: string | null + role_id: string | null + updated_at: string + valid_from: string | null + valid_until: string | null + } + Insert: { + club_id: string + created_at?: string + email?: string | null + first_name: string + id?: string + is_active?: boolean + last_name: string + mobile?: string | null + phone?: string | null + role_id?: string | null + updated_at?: string + valid_from?: string | null + valid_until?: string | null + } + Update: { + club_id?: string + created_at?: string + email?: string | null + first_name?: string + id?: string + is_active?: boolean + last_name?: string + mobile?: string | null + phone?: string | null + role_id?: string | null + updated_at?: string + valid_from?: string | null + valid_until?: string | null + } + Relationships: [ + { + foreignKeyName: "club_contacts_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_contacts_role_id_fkey" + columns: ["role_id"] + isOneToOne: false + referencedRelation: "club_roles" + referencedColumns: ["id"] + }, + ] + } + club_fee_billings: { + Row: { + amount: number + billing_period: string | null + billing_year: number + club_id: string + created_at: string + fee_type_id: string + id: string + invoice_date: string | null + invoice_number: string | null + member_count_at_billing: number | null + paid_amount: number | null + paid_date: string | null + remarks: string | null + status: string + updated_at: string + } + Insert: { + amount?: number + billing_period?: string | null + billing_year: number + club_id: string + created_at?: string + fee_type_id: string + id?: string + invoice_date?: string | null + invoice_number?: string | null + member_count_at_billing?: number | null + paid_amount?: number | null + paid_date?: string | null + remarks?: string | null + status?: string + updated_at?: string + } + Update: { + amount?: number + billing_period?: string | null + billing_year?: number + club_id?: string + created_at?: string + fee_type_id?: string + id?: string + invoice_date?: string | null + invoice_number?: string | null + member_count_at_billing?: number | null + paid_amount?: number | null + paid_date?: string | null + remarks?: string | null + status?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "club_fee_billings_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_fee_billings_fee_type_id_fkey" + columns: ["fee_type_id"] + isOneToOne: false + referencedRelation: "club_fee_types" + referencedColumns: ["id"] + }, + ] + } + club_fee_types: { + Row: { + account_id: string + created_at: string + default_amount: number + description: string | null + id: string + is_active: boolean + is_per_member: boolean + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + default_amount?: number + description?: string | null + id?: string + is_active?: boolean + is_per_member?: boolean + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + default_amount?: number + description?: string | null + id?: string + is_active?: boolean + is_per_member?: boolean + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "club_fee_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_fee_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_fee_types_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + club_notes: { + Row: { + club_id: string + content: string + created_at: string + created_by: string | null + id: string + note_date: string + title: string | null + } + Insert: { + club_id: string + content: string + created_at?: string + created_by?: string | null + id?: string + note_date?: string + title?: string | null + } + Update: { + club_id?: string + content?: string + created_at?: string + created_by?: string | null + id?: string + note_date?: string + title?: string | null + } + Relationships: [ + { + foreignKeyName: "club_notes_club_id_fkey" + columns: ["club_id"] + isOneToOne: false + referencedRelation: "member_clubs" + referencedColumns: ["id"] + }, + ] + } + club_roles: { + Row: { + account_id: string + created_at: string + description: string | null + id: string + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + description?: string | null + id?: string + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + description?: string | null + id?: string + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "club_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "club_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } cms_files: { Row: { account_id: string @@ -563,6 +1217,243 @@ export type Database = { }, ] } + competition_categories: { + Row: { + account_id: string + created_at: string + id: string + name: string + sort_order: number + } + Insert: { + account_id: string + created_at?: string + id?: string + name: string + sort_order?: number + } + Update: { + account_id?: string + created_at?: string + id?: string + name?: string + sort_order?: number + } + Relationships: [ + { + foreignKeyName: "competition_categories_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_categories_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_categories_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + competition_participants: { + Row: { + address: string | null + birth_date: string | null + category_id: string | null + competition_id: string + created_at: string + email: string | null + heaviest_catch_g: number + id: string + longest_catch_cm: number + member_id: string | null + participant_name: string + participated: boolean + phone: string | null + total_catch_count: number + total_length_cm: number + total_weight_g: number + } + Insert: { + address?: string | null + birth_date?: string | null + category_id?: string | null + competition_id: string + created_at?: string + email?: string | null + heaviest_catch_g?: number + id?: string + longest_catch_cm?: number + member_id?: string | null + participant_name: string + participated?: boolean + phone?: string | null + total_catch_count?: number + total_length_cm?: number + total_weight_g?: number + } + Update: { + address?: string | null + birth_date?: string | null + category_id?: string | null + competition_id?: string + created_at?: string + email?: string | null + heaviest_catch_g?: number + id?: string + longest_catch_cm?: number + member_id?: string | null + participant_name?: string + participated?: boolean + phone?: string | null + total_catch_count?: number + total_length_cm?: number + total_weight_g?: number + } + Relationships: [ + { + foreignKeyName: "competition_participants_category_id_fkey" + columns: ["category_id"] + isOneToOne: false + referencedRelation: "competition_categories" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_participants_competition_id_fkey" + columns: ["competition_id"] + isOneToOne: false + referencedRelation: "competitions" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competition_participants_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + ] + } + competitions: { + Row: { + account_id: string + competition_date: string + created_at: string + created_by: string | null + event_id: string | null + id: string + max_participants: number | null + name: string + permit_id: string | null + result_count_count: number | null + result_count_length: number | null + result_count_weight: number | null + score_by_count: boolean + score_by_heaviest: boolean + score_by_longest: boolean + score_by_total_length: boolean + score_by_total_weight: boolean + separate_member_guest_scoring: boolean + updated_at: string + water_id: string | null + } + Insert: { + account_id: string + competition_date: string + created_at?: string + created_by?: string | null + event_id?: string | null + id?: string + max_participants?: number | null + name: string + permit_id?: string | null + result_count_count?: number | null + result_count_length?: number | null + result_count_weight?: number | null + score_by_count?: boolean + score_by_heaviest?: boolean + score_by_longest?: boolean + score_by_total_length?: boolean + score_by_total_weight?: boolean + separate_member_guest_scoring?: boolean + updated_at?: string + water_id?: string | null + } + Update: { + account_id?: string + competition_date?: string + created_at?: string + created_by?: string | null + event_id?: string | null + id?: string + max_participants?: number | null + name?: string + permit_id?: string | null + result_count_count?: number | null + result_count_length?: number | null + result_count_weight?: number | null + score_by_count?: boolean + score_by_heaviest?: boolean + score_by_longest?: boolean + score_by_total_length?: boolean + score_by_total_weight?: boolean + separate_member_guest_scoring?: boolean + updated_at?: string + water_id?: string | null + } + Relationships: [ + { + foreignKeyName: "competitions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_event_id_fkey" + columns: ["event_id"] + isOneToOne: false + referencedRelation: "events" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_permit_id_fkey" + columns: ["permit_id"] + isOneToOne: false + referencedRelation: "fishing_permits" + referencedColumns: ["id"] + }, + { + foreignKeyName: "competitions_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } config: { Row: { billing_provider: Database["public"]["Enums"]["billing_provider"] @@ -584,6 +1475,58 @@ export type Database = { } Relationships: [] } + cost_centers: { + Row: { + account_id: string + code: string | null + created_at: string + description: string | null + id: string + is_active: boolean + name: string + } + Insert: { + account_id: string + code?: string | null + created_at?: string + description?: string | null + id?: string + is_active?: boolean + name: string + } + Update: { + account_id?: string + code?: string | null + created_at?: string + description?: string | null + id?: string + is_active?: boolean + name?: string + } + Relationships: [ + { + foreignKeyName: "cost_centers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "cost_centers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "cost_centers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } course_attendance: { Row: { id: string @@ -1021,6 +1964,82 @@ export type Database = { }, ] } + document_templates: { + Row: { + account_id: string + body_html: string + created_at: string + created_by: string | null + description: string | null + id: string + is_default: boolean + name: string + orientation: string + page_format: string + shared_with_hierarchy: boolean + sort_order: number + template_type: string + updated_at: string + variables: Json + } + Insert: { + account_id: string + body_html?: string + created_at?: string + created_by?: string | null + description?: string | null + id?: string + is_default?: boolean + name: string + orientation?: string + page_format?: string + shared_with_hierarchy?: boolean + sort_order?: number + template_type?: string + updated_at?: string + variables?: Json + } + Update: { + account_id?: string + body_html?: string + created_at?: string + created_by?: string | null + description?: string | null + id?: string + is_default?: boolean + name?: string + orientation?: string + page_format?: string + shared_with_hierarchy?: boolean + sort_order?: number + template_type?: string + updated_at?: string + variables?: Json + } + Relationships: [ + { + foreignKeyName: "document_templates_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "document_templates_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "document_templates_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } dues_categories: { Row: { account_id: string @@ -1158,6 +2177,7 @@ export type Database = { min_age: number | null name: string registration_deadline: string | null + shared_with_hierarchy: boolean status: string updated_at: string } @@ -1180,6 +2200,7 @@ export type Database = { min_age?: number | null name: string registration_deadline?: string | null + shared_with_hierarchy?: boolean status?: string updated_at?: string } @@ -1202,6 +2223,7 @@ export type Database = { min_age?: number | null name?: string registration_deadline?: string | null + shared_with_hierarchy?: boolean status?: string updated_at?: string } @@ -1229,6 +2251,470 @@ export type Database = { }, ] } + fish_species: { + Row: { + account_id: string + created_at: string + has_special_spawning_season: boolean + id: string + individual_recording: boolean + is_active: boolean + k_factor_avg: number | null + k_factor_max: number | null + k_factor_min: number | null + max_age_years: number | null + max_catch_per_day: number | null + max_catch_per_year: number | null + max_length_cm: number | null + max_weight_kg: number | null + name: string + name_latin: string | null + name_local: string | null + price_per_unit: number | null + protected_min_size_cm: number | null + protection_period_end: string | null + protection_period_start: string | null + sort_order: number + spawning_season_end: string | null + spawning_season_start: string | null + updated_at: string + } + Insert: { + account_id: string + created_at?: string + has_special_spawning_season?: boolean + id?: string + individual_recording?: boolean + is_active?: boolean + k_factor_avg?: number | null + k_factor_max?: number | null + k_factor_min?: number | null + max_age_years?: number | null + max_catch_per_day?: number | null + max_catch_per_year?: number | null + max_length_cm?: number | null + max_weight_kg?: number | null + name: string + name_latin?: string | null + name_local?: string | null + price_per_unit?: number | null + protected_min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + sort_order?: number + spawning_season_end?: string | null + spawning_season_start?: string | null + updated_at?: string + } + Update: { + account_id?: string + created_at?: string + has_special_spawning_season?: boolean + id?: string + individual_recording?: boolean + is_active?: boolean + k_factor_avg?: number | null + k_factor_max?: number | null + k_factor_min?: number | null + max_age_years?: number | null + max_catch_per_day?: number | null + max_catch_per_year?: number | null + max_length_cm?: number | null + max_weight_kg?: number | null + name?: string + name_latin?: string | null + name_local?: string | null + price_per_unit?: number | null + protected_min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + sort_order?: number + spawning_season_end?: string | null + spawning_season_start?: string | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "fish_species_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_species_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_species_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + fish_stocking: { + Row: { + account_id: string + age_class: Database["public"]["Enums"]["fish_age_class"] + cost_euros: number | null + created_at: string + created_by: string | null + id: string + quantity: number + remarks: string | null + species_id: string + stocking_date: string + supplier_id: string | null + updated_at: string + updated_by: string | null + water_id: string + weight_kg: number | null + } + Insert: { + account_id: string + age_class?: Database["public"]["Enums"]["fish_age_class"] + cost_euros?: number | null + created_at?: string + created_by?: string | null + id?: string + quantity?: number + remarks?: string | null + species_id: string + stocking_date: string + supplier_id?: string | null + updated_at?: string + updated_by?: string | null + water_id: string + weight_kg?: number | null + } + Update: { + account_id?: string + age_class?: Database["public"]["Enums"]["fish_age_class"] + cost_euros?: number | null + created_at?: string + created_by?: string | null + id?: string + quantity?: number + remarks?: string | null + species_id?: string + stocking_date?: string + supplier_id?: string | null + updated_at?: string + updated_by?: string | null + water_id?: string + weight_kg?: number | null + } + Relationships: [ + { + foreignKeyName: "fish_stocking_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_species_id_fkey" + columns: ["species_id"] + isOneToOne: false + referencedRelation: "fish_species" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_supplier_id_fkey" + columns: ["supplier_id"] + isOneToOne: false + referencedRelation: "fish_suppliers" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_stocking_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + fish_suppliers: { + Row: { + account_id: string + address: string | null + contact_person: string | null + created_at: string + email: string | null + id: string + is_active: boolean + name: string + notes: string | null + phone: string | null + updated_at: string + } + Insert: { + account_id: string + address?: string | null + contact_person?: string | null + created_at?: string + email?: string | null + id?: string + is_active?: boolean + name: string + notes?: string | null + phone?: string | null + updated_at?: string + } + Update: { + account_id?: string + address?: string | null + contact_person?: string | null + created_at?: string + email?: string | null + id?: string + is_active?: boolean + name?: string + notes?: string | null + phone?: string | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "fish_suppliers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_suppliers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fish_suppliers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } + fishing_leases: { + Row: { + account_holder: string | null + account_id: string + bic: string | null + created_at: string + created_by: string | null + duration_years: number | null + end_date: string | null + fixed_annual_increase: number | null + iban: string | null + id: string + initial_amount: number + is_archived: boolean + lessor_address: string | null + lessor_email: string | null + lessor_name: string + lessor_phone: string | null + location_details: string | null + payment_method: + | Database["public"]["Enums"]["lease_payment_method"] + | null + percentage_annual_increase: number | null + special_agreements: string | null + start_date: string + updated_at: string + updated_by: string | null + water_id: string + } + Insert: { + account_holder?: string | null + account_id: string + bic?: string | null + created_at?: string + created_by?: string | null + duration_years?: number | null + end_date?: string | null + fixed_annual_increase?: number | null + iban?: string | null + id?: string + initial_amount?: number + is_archived?: boolean + lessor_address?: string | null + lessor_email?: string | null + lessor_name: string + lessor_phone?: string | null + location_details?: string | null + payment_method?: + | Database["public"]["Enums"]["lease_payment_method"] + | null + percentage_annual_increase?: number | null + special_agreements?: string | null + start_date: string + updated_at?: string + updated_by?: string | null + water_id: string + } + Update: { + account_holder?: string | null + account_id?: string + bic?: string | null + created_at?: string + created_by?: string | null + duration_years?: number | null + end_date?: string | null + fixed_annual_increase?: number | null + iban?: string | null + id?: string + initial_amount?: number + is_archived?: boolean + lessor_address?: string | null + lessor_email?: string | null + lessor_name?: string + lessor_phone?: string | null + location_details?: string | null + payment_method?: + | Database["public"]["Enums"]["lease_payment_method"] + | null + percentage_annual_increase?: number | null + special_agreements?: string | null + start_date?: string + updated_at?: string + updated_by?: string | null + water_id?: string + } + Relationships: [ + { + foreignKeyName: "fishing_leases_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_leases_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_leases_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_leases_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + fishing_permits: { + Row: { + account_id: string + cost_center_id: string | null + created_at: string + hejfish_id: string | null + id: string + is_archived: boolean + is_for_sale: boolean + name: string + primary_water_id: string | null + short_code: string | null + total_quantity: number | null + updated_at: string + } + Insert: { + account_id: string + cost_center_id?: string | null + created_at?: string + hejfish_id?: string | null + id?: string + is_archived?: boolean + is_for_sale?: boolean + name: string + primary_water_id?: string | null + short_code?: string | null + total_quantity?: number | null + updated_at?: string + } + Update: { + account_id?: string + cost_center_id?: string | null + created_at?: string + hejfish_id?: string | null + id?: string + is_archived?: boolean + is_for_sale?: boolean + name?: string + primary_water_id?: string | null + short_code?: string | null + total_quantity?: number | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "fishing_permits_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_cost_center_id_fkey" + columns: ["cost_center_id"] + isOneToOne: false + referencedRelation: "cost_centers" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fishing_permits_primary_water_id_fkey" + columns: ["primary_water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } gdpr_processing_register: { Row: { account_id: string @@ -1372,6 +2858,58 @@ export type Database = { }, ] } + hejfish_sync: { + Row: { + account_id: string + entity_type: string + hejfish_id: string + id: string + last_synced_at: string + local_id: string + sync_data: Json | null + } + Insert: { + account_id: string + entity_type: string + hejfish_id: string + id?: string + last_synced_at?: string + local_id: string + sync_data?: Json | null + } + Update: { + account_id?: string + entity_type?: string + hejfish_id?: string + id?: string + last_synced_at?: string + local_id?: string + sync_data?: Json | null + } + Relationships: [ + { + foreignKeyName: "hejfish_sync_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "hejfish_sync_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "hejfish_sync_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } holiday_pass_activities: { Row: { activity_date: string | null @@ -1682,6 +3220,211 @@ export type Database = { }, ] } + meeting_protocol_attachments: { + Row: { + content_type: string | null + created_at: string + created_by: string | null + file_name: string + file_path: string + file_size: number | null + id: string + item_id: string | null + protocol_id: string + } + Insert: { + content_type?: string | null + created_at?: string + created_by?: string | null + file_name: string + file_path: string + file_size?: number | null + id?: string + item_id?: string | null + protocol_id: string + } + Update: { + content_type?: string | null + created_at?: string + created_by?: string | null + file_name?: string + file_path?: string + file_size?: number | null + id?: string + item_id?: string | null + protocol_id?: string + } + Relationships: [ + { + foreignKeyName: "meeting_protocol_attachments_item_id_fkey" + columns: ["item_id"] + isOneToOne: false + referencedRelation: "meeting_protocol_items" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocol_attachments_item_id_fkey" + columns: ["item_id"] + isOneToOne: false + referencedRelation: "open_meeting_tasks" + referencedColumns: ["item_id"] + }, + { + foreignKeyName: "meeting_protocol_attachments_protocol_id_fkey" + columns: ["protocol_id"] + isOneToOne: false + referencedRelation: "meeting_protocols" + referencedColumns: ["id"] + }, + ] + } + meeting_protocol_items: { + Row: { + content: string | null + created_at: string + decision_text: string | null + due_date: string | null + id: string + item_number: number + item_type: string + protocol_id: string + responsible_person: string | null + sort_order: number + status: Database["public"]["Enums"]["meeting_item_status"] + title: string + updated_at: string + } + Insert: { + content?: string | null + created_at?: string + decision_text?: string | null + due_date?: string | null + id?: string + item_number?: number + item_type?: string + protocol_id: string + responsible_person?: string | null + sort_order?: number + status?: Database["public"]["Enums"]["meeting_item_status"] + title: string + updated_at?: string + } + Update: { + content?: string | null + created_at?: string + decision_text?: string | null + due_date?: string | null + id?: string + item_number?: number + item_type?: string + protocol_id?: string + responsible_person?: string | null + sort_order?: number + status?: Database["public"]["Enums"]["meeting_item_status"] + title?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "meeting_protocol_items_protocol_id_fkey" + columns: ["protocol_id"] + isOneToOne: false + referencedRelation: "meeting_protocols" + referencedColumns: ["id"] + }, + ] + } + meeting_protocols: { + Row: { + absent: Json + account_id: string + attendees: Json + chair: string | null + created_at: string + created_by: string | null + end_time: string | null + id: string + is_archived: boolean + location: string | null + meeting_date: string + meeting_time: string | null + next_meeting_date: string | null + protocol_number: string | null + recorder: string | null + status: string + summary: string | null + title: string + updated_at: string + updated_by: string | null + } + Insert: { + absent?: Json + account_id: string + attendees?: Json + chair?: string | null + created_at?: string + created_by?: string | null + end_time?: string | null + id?: string + is_archived?: boolean + location?: string | null + meeting_date: string + meeting_time?: string | null + next_meeting_date?: string | null + protocol_number?: string | null + recorder?: string | null + status?: string + summary?: string | null + title: string + updated_at?: string + updated_by?: string | null + } + Update: { + absent?: Json + account_id?: string + attendees?: Json + chair?: string | null + created_at?: string + created_by?: string | null + end_time?: string | null + id?: string + is_archived?: boolean + location?: string | null + meeting_date?: string + meeting_time?: string | null + next_meeting_date?: string | null + protocol_number?: string | null + recorder?: string | null + status?: string + summary?: string | null + title?: string + updated_at?: string + updated_by?: string | null + } + Relationships: [ + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } member_cards: { Row: { account_id: string @@ -1744,6 +3487,122 @@ export type Database = { }, ] } + member_clubs: { + Row: { + account_holder: string | null + account_id: string + address_city: string | null + address_street: string | null + address_zip: string | null + association_type_id: string | null + bic: string | null + club_number: string | null + created_at: string + created_by: string | null + custom_data: Json + email: string | null + founding_year: number | null + iban: string | null + id: string + is_active: boolean + is_archived: boolean + member_count: number + name: string + notes: string | null + phone: string | null + short_name: string | null + updated_at: string + updated_by: string | null + website: string | null + youth_count: number + } + Insert: { + account_holder?: string | null + account_id: string + address_city?: string | null + address_street?: string | null + address_zip?: string | null + association_type_id?: string | null + bic?: string | null + club_number?: string | null + created_at?: string + created_by?: string | null + custom_data?: Json + email?: string | null + founding_year?: number | null + iban?: string | null + id?: string + is_active?: boolean + is_archived?: boolean + member_count?: number + name: string + notes?: string | null + phone?: string | null + short_name?: string | null + updated_at?: string + updated_by?: string | null + website?: string | null + youth_count?: number + } + Update: { + account_holder?: string | null + account_id?: string + address_city?: string | null + address_street?: string | null + address_zip?: string | null + association_type_id?: string | null + bic?: string | null + club_number?: string | null + created_at?: string + created_by?: string | null + custom_data?: Json + email?: string | null + founding_year?: number | null + iban?: string | null + id?: string + is_active?: boolean + is_archived?: boolean + member_count?: number + name?: string + notes?: string | null + phone?: string | null + short_name?: string | null + updated_at?: string + updated_by?: string | null + website?: string | null + youth_count?: number + } + Relationships: [ + { + foreignKeyName: "member_clubs_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_clubs_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_clubs_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_clubs_association_type_id_fkey" + columns: ["association_type_id"] + isOneToOne: false + referencedRelation: "association_types" + referencedColumns: ["id"] + }, + ] + } member_department_assignments: { Row: { department_id: string @@ -2012,6 +3871,89 @@ export type Database = { }, ] } + member_transfers: { + Row: { + cleared_data: Json + id: string + member_id: string + reason: string | null + source_account_id: string + target_account_id: string + transferred_at: string + transferred_by: string + } + Insert: { + cleared_data?: Json + id?: string + member_id: string + reason?: string | null + source_account_id: string + target_account_id: string + transferred_at?: string + transferred_by: string + } + Update: { + cleared_data?: Json + id?: string + member_id?: string + reason?: string | null + source_account_id?: string + target_account_id?: string + transferred_at?: string + transferred_by?: string + } + Relationships: [ + { + foreignKeyName: "member_transfers_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_source_account_id_fkey" + columns: ["source_account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_source_account_id_fkey" + columns: ["source_account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_source_account_id_fkey" + columns: ["source_account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_target_account_id_fkey" + columns: ["target_account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_target_account_id_fkey" + columns: ["target_account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "member_transfers_target_account_id_fkey" + columns: ["target_account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } members: { Row: { account_holder: string | null @@ -2924,6 +4866,7 @@ export type Database = { created_at: string id: string name: string + shared_with_hierarchy: boolean subject: string updated_at: string variables: Json @@ -2935,6 +4878,7 @@ export type Database = { created_at?: string id?: string name: string + shared_with_hierarchy?: boolean subject: string updated_at?: string variables?: Json @@ -2946,6 +4890,7 @@ export type Database = { created_at?: string id?: string name?: string + shared_with_hierarchy?: boolean subject?: string updated_at?: string variables?: Json @@ -3278,6 +5223,44 @@ export type Database = { }, ] } + permit_quotas: { + Row: { + business_year: number + category_name: string | null + conversion_factor: number | null + created_at: string + id: string + permit_id: string + quota_quantity: number + } + Insert: { + business_year: number + category_name?: string | null + conversion_factor?: number | null + created_at?: string + id?: string + permit_id: string + quota_quantity?: number + } + Update: { + business_year?: number + category_name?: string | null + conversion_factor?: number | null + created_at?: string + id?: string + permit_id?: string + quota_quantity?: number + } + Relationships: [ + { + foreignKeyName: "permit_quotas_permit_id_fkey" + columns: ["permit_id"] + isOneToOne: false + referencedRelation: "fishing_permits" + referencedColumns: ["id"] + }, + ] + } role_permissions: { Row: { id: number @@ -3395,6 +5378,7 @@ export type Database = { description: string | null execution_date: string id: string + is_consolidated: boolean item_count: number pain_format: string status: Database["public"]["Enums"]["sepa_batch_status"] @@ -3410,6 +5394,7 @@ export type Database = { description?: string | null execution_date: string id?: string + is_consolidated?: boolean item_count?: number pain_format?: string status?: Database["public"]["Enums"]["sepa_batch_status"] @@ -3425,6 +5410,7 @@ export type Database = { description?: string | null execution_date?: string id?: string + is_consolidated?: boolean item_count?: number pain_format?: string status?: Database["public"]["Enums"]["sepa_batch_status"] @@ -3902,8 +5888,296 @@ export type Database = { }, ] } + water_inspectors: { + Row: { + account_id: string + assignment_end: string | null + assignment_start: string + created_at: string + id: string + member_id: string + water_id: string + } + Insert: { + account_id: string + assignment_end?: string | null + assignment_start?: string + created_at?: string + id?: string + member_id: string + water_id: string + } + Update: { + account_id?: string + assignment_end?: string | null + assignment_start?: string + created_at?: string + id?: string + member_id?: string + water_id?: string + } + Relationships: [ + { + foreignKeyName: "water_inspectors_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_member_id_fkey" + columns: ["member_id"] + isOneToOne: false + referencedRelation: "members" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_inspectors_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + water_species_rules: { + Row: { + created_at: string + id: string + max_catch_per_day: number | null + max_catch_per_year: number | null + min_size_cm: number | null + protection_period_end: string | null + protection_period_start: string | null + species_id: string + water_id: string + } + Insert: { + created_at?: string + id?: string + max_catch_per_day?: number | null + max_catch_per_year?: number | null + min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + species_id: string + water_id: string + } + Update: { + created_at?: string + id?: string + max_catch_per_day?: number | null + max_catch_per_year?: number | null + min_size_cm?: number | null + protection_period_end?: string | null + protection_period_start?: string | null + species_id?: string + water_id?: string + } + Relationships: [ + { + foreignKeyName: "water_species_rules_species_id_fkey" + columns: ["species_id"] + isOneToOne: false + referencedRelation: "fish_species" + referencedColumns: ["id"] + }, + { + foreignKeyName: "water_species_rules_water_id_fkey" + columns: ["water_id"] + isOneToOne: false + referencedRelation: "waters" + referencedColumns: ["id"] + }, + ] + } + waters: { + Row: { + account_id: string + avg_depth_m: number | null + classification_order: number | null + cost_center_id: string | null + cost_share_ds: number | null + cost_share_kalk: number | null + county: string | null + created_at: string + created_by: string | null + description: string | null + electrofishing_permit_requested: boolean + geo_lat: number | null + geo_lng: number | null + hejfish_id: string | null + id: string + is_archived: boolean + length_m: number | null + lfv_name: string | null + lfv_number: string | null + location: string | null + max_depth_m: number | null + name: string + outflow: string | null + short_name: string | null + surface_area_ha: number | null + updated_at: string + updated_by: string | null + water_type: Database["public"]["Enums"]["water_type"] + width_m: number | null + } + Insert: { + account_id: string + avg_depth_m?: number | null + classification_order?: number | null + cost_center_id?: string | null + cost_share_ds?: number | null + cost_share_kalk?: number | null + county?: string | null + created_at?: string + created_by?: string | null + description?: string | null + electrofishing_permit_requested?: boolean + geo_lat?: number | null + geo_lng?: number | null + hejfish_id?: string | null + id?: string + is_archived?: boolean + length_m?: number | null + lfv_name?: string | null + lfv_number?: string | null + location?: string | null + max_depth_m?: number | null + name: string + outflow?: string | null + short_name?: string | null + surface_area_ha?: number | null + updated_at?: string + updated_by?: string | null + water_type?: Database["public"]["Enums"]["water_type"] + width_m?: number | null + } + Update: { + account_id?: string + avg_depth_m?: number | null + classification_order?: number | null + cost_center_id?: string | null + cost_share_ds?: number | null + cost_share_kalk?: number | null + county?: string | null + created_at?: string + created_by?: string | null + description?: string | null + electrofishing_permit_requested?: boolean + geo_lat?: number | null + geo_lng?: number | null + hejfish_id?: string | null + id?: string + is_archived?: boolean + length_m?: number | null + lfv_name?: string | null + lfv_number?: string | null + location?: string | null + max_depth_m?: number | null + name?: string + outflow?: string | null + short_name?: string | null + surface_area_ha?: number | null + updated_at?: string + updated_by?: string | null + water_type?: Database["public"]["Enums"]["water_type"] + width_m?: number | null + } + Relationships: [ + { + foreignKeyName: "waters_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "waters_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "waters_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "waters_cost_center_id_fkey" + columns: ["cost_center_id"] + isOneToOne: false + referencedRelation: "cost_centers" + referencedColumns: ["id"] + }, + ] + } } Views: { + open_meeting_tasks: { + Row: { + account_id: string | null + due_date: string | null + is_overdue: boolean | null + item_id: string | null + item_number: number | null + meeting_date: string | null + protocol_id: string | null + protocol_title: string | null + responsible_person: string | null + status: Database["public"]["Enums"]["meeting_item_status"] | null + task_description: string | null + task_title: string | null + } + Relationships: [ + { + foreignKeyName: "meeting_protocol_items_protocol_id_fkey" + columns: ["protocol_id"] + isOneToOne: false + referencedRelation: "meeting_protocols" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "meeting_protocols_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } user_account_workspace: { Row: { id: string | null @@ -3967,6 +6241,25 @@ export type Database = { status: Database["public"]["Enums"]["membership_status"] }[] } + clone_template: { + Args: { + p_new_name?: string + p_target_account_id: string + p_template_id: string + p_template_type: string + } + Returns: string + } + compute_lease_amount: { + Args: { + p_fixed_increase: number + p_initial_amount: number + p_percentage_increase: number + p_start_year: number + p_target_year: number + } + Returns: number + } create_invitation: { Args: { account_id: string; email: string; role: string } Returns: { @@ -4007,6 +6300,7 @@ export type Database = { id: string is_personal_account: boolean name: string + parent_account_id: string | null picture_url: string | null primary_owner_user_id: string public_data: Json @@ -4021,6 +6315,9 @@ export type Database = { isSetofReturn: false } } + get_account_ancestors: { Args: { child_id: string }; Returns: string[] } + get_account_depth: { Args: { account_id: string }; Returns: number } + get_account_descendants: { Args: { root_id: string }; Returns: string[] } get_account_invitations: { Args: { account_slug: string } Returns: { @@ -4052,9 +6349,75 @@ export type Database = { user_id: string }[] } + get_catch_statistics: { + Args: { p_account_id: string; p_water_id?: string; p_year?: number } + Returns: { + avg_k_factor: number + avg_length_cm: number + avg_weight_g: number + species_id: string + species_name: string + total_count: number + total_weight_kg: number + }[] + } + get_child_accounts: { + Args: { parent_id: string } + Returns: { + created_at: string + id: string + name: string + parent_account_id: string + slug: string + }[] + } get_config: { Args: never; Returns: Json } + get_hierarchy_report: { + Args: { root_account_id: string } + Returns: { + active_courses: number + active_members: number + depth: number + inactive_members: number + new_members_this_year: number + open_invoice_amount: number + open_invoices: number + org_id: string + org_name: string + org_slug: string + sepa_batches_this_year: number + total_members: number + upcoming_events: number + }[] + } + get_hierarchy_summary: { + Args: { root_account_id: string } + Returns: { + new_members_this_year: number + total_active_courses: number + total_active_members: number + total_members: number + total_open_invoice_amount: number + total_open_invoices: number + total_orgs: number + total_sepa_batches_this_year: number + total_upcoming_events: number + }[] + } get_nonce_status: { Args: { p_id: string }; Returns: Json } get_upper_system_role: { Args: never; Returns: string } + get_user_visible_accounts: { Args: never; Returns: string[] } + get_verband_dashboard_stats: { + Args: { p_account_id: string } + Returns: { + active_clubs: number + open_fees: number + open_fees_amount: number + total_clubs: number + total_members: number + total_youth: number + }[] + } has_active_subscription: { Args: { target_account_id: string } Returns: boolean @@ -4075,10 +6438,22 @@ export type Database = { } Returns: boolean } + has_permission_or_ancestor: { + Args: { + permission_name: Database["public"]["Enums"]["app_permissions"] + target_account_id: string + user_id: string + } + Returns: boolean + } has_role_on_account: { Args: { account_id: string; account_role?: string } Returns: boolean } + has_role_on_account_or_ancestor: { + Args: { account_role?: string; target_account_id: string } + Returns: boolean + } has_same_role_hierarchy_level: { Args: { role_name: string @@ -4104,6 +6479,64 @@ export type Database = { Args: { p_invite_token: string; p_user_id: string } Returns: string } + list_hierarchy_events: { + Args: { + p_from_date?: string + p_page?: number + p_page_size?: number + p_shared_only?: boolean + p_status?: string + root_account_id: string + } + Returns: { + account_id: string + account_name: string + capacity: number + description: string + end_date: string + event_date: string + event_time: string + fee: number + id: string + location: string + name: string + registration_count: number + registration_deadline: string + shared_with_hierarchy: boolean + status: string + total_count: number + }[] + } + list_hierarchy_sepa_eligible_members: { + Args: { p_account_filter?: string; root_account_id: string } + Returns: { + account_holder: string + account_id: string + account_name: string + bic: string + dues_amount: number + first_name: string + iban: string + last_name: string + mandate_date: string + mandate_id: string + member_id: string + }[] + } + list_hierarchy_shared_templates: { + Args: { p_template_type?: string; root_account_id: string } + Returns: { + account_id: string + account_name: string + created_at: string + description: string + id: string + name: string + shared_with_hierarchy: boolean + template_source: string + template_type: string + }[] + } module_query: { Args: { p_filters?: Json @@ -4120,6 +6553,33 @@ export type Database = { Args: { p_id: string; p_reason?: string } Returns: boolean } + search_members_across_hierarchy: { + Args: { + account_filter?: string + page_number?: number + page_size?: number + root_account_id: string + search_term?: string + status_filter?: string + } + Returns: { + account_id: string + account_name: string + account_slug: string + city: string + email: string + entry_date: string + first_name: string + id: string + last_name: string + member_number: string + phone: string + status: Database["public"]["Enums"]["membership_status"] + total_count: number + }[] + } + show_limit: { Args: never; Returns: number } + show_trgm: { Args: { "": string }; Returns: string[] } team_account_workspace: { Args: { account_slug: string } Returns: { @@ -4134,6 +6594,15 @@ export type Database = { subscription_status: Database["public"]["Enums"]["subscription_status"] }[] } + transfer_member: { + Args: { + p_keep_sepa?: boolean + p_member_id: string + p_reason?: string + p_target_account_id: string + } + Returns: string + } transfer_team_account_ownership: { Args: { new_owner_id: string; target_account_id: string } Returns: undefined @@ -4246,9 +6715,28 @@ export type Database = { | "finance.sepa" | "documents.generate" | "newsletter.send" + | "fischerei.read" + | "fischerei.write" + | "meetings.read" + | "meetings.write" + | "verband.read" + | "verband.write" application_status: "submitted" | "review" | "approved" | "rejected" audit_action: "insert" | "update" | "delete" | "lock" billing_provider: "stripe" | "lemon-squeezy" | "paddle" + catch_book_status: + | "offen" + | "eingereicht" + | "geprueft" + | "akzeptiert" + | "abgelehnt" + catch_book_verification: + | "sehrgut" + | "gut" + | "ok" + | "schlecht" + | "falsch" + | "leer" cms_account_type: "verein" | "vhs" | "hotel" | "kommune" | "generic" cms_field_type: | "text" @@ -4274,6 +6762,18 @@ export type Database = { cms_module_status: "active" | "inactive" | "archived" cms_record_status: "active" | "locked" | "deleted" | "archived" enrollment_status: "enrolled" | "waitlisted" | "cancelled" | "completed" + fish_age_class: + | "brut" + | "soemmerlinge" + | "einsoemmerig" + | "zweisoemmerig" + | "dreisoemmerig" + | "vorgestreckt" + | "setzlinge" + | "laichfische" + | "sonstige" + fish_gender: "maennlich" | "weiblich" | "unbekannt" + fish_size_category: "gross" | "mittel" | "klein" gdpr_legal_basis: | "consent" | "contract" @@ -4288,6 +6788,8 @@ export type Database = { | "overdue" | "cancelled" | "credited" + lease_payment_method: "bar" | "lastschrift" | "ueberweisung" + meeting_item_status: "offen" | "in_bearbeitung" | "erledigt" | "vertagt" membership_status: | "active" | "inactive" @@ -4319,6 +6821,16 @@ export type Database = { | "incomplete" | "incomplete_expired" | "paused" + water_type: + | "fluss" + | "bach" + | "see" + | "teich" + | "weiher" + | "kanal" + | "stausee" + | "baggersee" + | "sonstige" } CompositeTypes: { invitation: { @@ -5012,10 +7524,31 @@ export const Constants = { "finance.sepa", "documents.generate", "newsletter.send", + "fischerei.read", + "fischerei.write", + "meetings.read", + "meetings.write", + "verband.read", + "verband.write", ], application_status: ["submitted", "review", "approved", "rejected"], audit_action: ["insert", "update", "delete", "lock"], billing_provider: ["stripe", "lemon-squeezy", "paddle"], + catch_book_status: [ + "offen", + "eingereicht", + "geprueft", + "akzeptiert", + "abgelehnt", + ], + catch_book_verification: [ + "sehrgut", + "gut", + "ok", + "schlecht", + "falsch", + "leer", + ], cms_account_type: ["verein", "vhs", "hotel", "kommune", "generic"], cms_field_type: [ "text", @@ -5042,6 +7575,19 @@ export const Constants = { cms_module_status: ["active", "inactive", "archived"], cms_record_status: ["active", "locked", "deleted", "archived"], enrollment_status: ["enrolled", "waitlisted", "cancelled", "completed"], + fish_age_class: [ + "brut", + "soemmerlinge", + "einsoemmerig", + "zweisoemmerig", + "dreisoemmerig", + "vorgestreckt", + "setzlinge", + "laichfische", + "sonstige", + ], + fish_gender: ["maennlich", "weiblich", "unbekannt"], + fish_size_category: ["gross", "mittel", "klein"], gdpr_legal_basis: [ "consent", "contract", @@ -5058,6 +7604,8 @@ export const Constants = { "cancelled", "credited", ], + lease_payment_method: ["bar", "lastschrift", "ueberweisung"], + meeting_item_status: ["offen", "in_bearbeitung", "erledigt", "vertagt"], membership_status: [ "active", "inactive", @@ -5092,6 +7640,17 @@ export const Constants = { "incomplete_expired", "paused", ], + water_type: [ + "fluss", + "bach", + "see", + "teich", + "weiher", + "kanal", + "stausee", + "baggersee", + "sonstige", + ], }, }, storage: { diff --git a/packages/supabase/src/get-supabase-client-keys.ts b/packages/supabase/src/get-supabase-client-keys.ts index 3afab122e..99a3e668d 100644 --- a/packages/supabase/src/get-supabase-client-keys.ts +++ b/packages/supabase/src/get-supabase-client-keys.ts @@ -11,7 +11,7 @@ export function getSupabaseClientKeys() { const isServer = typeof window === 'undefined'; const url = isServer - ? (process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL) + ? process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL : process.env.NEXT_PUBLIC_SUPABASE_URL; return z
+ {currentSearch + ? 'Versuchen Sie einen anderen Suchbegriff.' + : 'In den verknüpften Organisationen sind noch keine Mitglieder vorhanden.'} +
+ Seite {page} von {totalPages} ({total} Einträge) +
+ {preview.courses.length} aktive Kurseinschreibung(en) +
+ {preview.invoices.length} offene Rechnung(en) +
+ {preview.mandates.length} aktive(s) SEPA-Mandat(e) +
+ {preview.newsletters.length} Newsletter-Abonnement(s) +
Wird zurückgesetzt:
+ Keine aktiven Verknüpfungen gefunden +
+ Transfer kann ohne Seiteneffekte durchgeführt werden. +
+ Bankverbindung wird übernommen, Mandat muss im Zielverein neu + bestätigt werden. +
+ {currentStatus || currentSharedOnly || currentFromDate + ? 'Versuchen Sie andere Filterkriterien.' + : 'In den verknüpften Organisationen sind noch keine Veranstaltungen vorhanden.'} +
+ Organisationen +
+ {formatNumber(summary.total_orgs)} +
+ Aktive Mitglieder +
+ {formatNumber(summary.total_active_members)} +
+ von {formatNumber(summary.total_members)} gesamt +
+ Neue Mitglieder (Jahr) +
+ {formatNumber(summary.new_members_this_year)} +
+ Anstehende Termine +
+ {formatNumber(summary.total_upcoming_events)} +
+ Aktive Kurse +
+ {formatNumber(summary.total_active_courses)} +
+ Offene Rechnungen +
+ {formatCurrencyAmount(summary.total_open_invoice_amount)} +
+ {formatNumber(summary.total_open_invoices)} Rechnungen +
+ Die Hierarchie enthält noch keine Organisationen. +
+ Keine geteilten Vorlagen vorhanden +
+ Vorlagen, die von anderen Organisationen in Ihrer Hierarchie + geteilt werden, erscheinen hier. +
+ {template.description} +