From 61ff48cb73334afc8e933882512e70ec3a0b840b Mon Sep 17 00:00:00 2001 From: Zaid Marzguioui Date: Sun, 29 Mar 2026 19:44:57 +0200 Subject: [PATCH] Initial state for GitNexus analysis --- apps/e2e/tests/course-enrollment.spec.ts | 25 + apps/e2e/tests/member-lifecycle.spec.ts | 29 + apps/e2e/tests/module-builder.spec.ts | 31 + apps/e2e/tests/newsletter.spec.ts | 20 + apps/e2e/tests/sepa-batch.spec.ts | 25 + apps/web/app/[locale]/admin/audit/page.tsx | 20 + apps/web/app/[locale]/admin/gdpr/page.tsx | 20 + .../web/app/[locale]/admin/migration/page.tsx | 33 + apps/web/app/[locale]/admin/modules/page.tsx | 19 + .../[account]/bookings/[bookingId]/page.tsx | 363 ++ .../home/[account]/bookings/calendar/page.tsx | 230 + .../home/[account]/bookings/guests/page.tsx | 90 + .../home/[account]/bookings/new/page.tsx | 235 + .../[locale]/home/[account]/bookings/page.tsx | 182 + .../home/[account]/bookings/rooms/page.tsx | 101 + .../courses/[courseId]/attendance/page.tsx | 134 + .../[account]/courses/[courseId]/page.tsx | 188 + .../courses/[courseId]/participants/page.tsx | 114 + .../home/[account]/courses/calendar/page.tsx | 248 + .../[account]/courses/categories/page.tsx | 86 + .../[account]/courses/instructors/page.tsx | 94 + .../home/[account]/courses/locations/page.tsx | 91 + .../home/[account]/courses/new/page.tsx | 191 + .../[locale]/home/[account]/courses/page.tsx | 180 + .../[account]/courses/statistics/page.tsx | 99 + .../[account]/documents/generate/page.tsx | 157 + .../home/[account]/documents/page.tsx | 137 + .../[account]/documents/templates/page.tsx | 101 + .../home/[account]/events/[eventId]/page.tsx | 182 + .../[account]/events/holiday-passes/page.tsx | 100 + .../home/[account]/events/new/page.tsx | 301 + .../[locale]/home/[account]/events/page.tsx | 180 + .../[account]/events/registrations/page.tsx | 188 + .../[account]/finance/invoices/[id]/page.tsx | 234 + .../[account]/finance/invoices/new/page.tsx | 212 + .../home/[account]/finance/invoices/page.tsx | 162 + .../[locale]/home/[account]/finance/page.tsx | 265 + .../home/[account]/finance/payments/page.tsx | 166 + .../[account]/finance/sepa/[batchId]/page.tsx | 218 + .../home/[account]/finance/sepa/new/page.tsx | 117 + .../home/[account]/finance/sepa/page.tsx | 164 + .../[account]/members-cms/[memberId]/page.tsx | 173 + .../members-cms/applications/page.tsx | 117 + .../home/[account]/members-cms/dues/page.tsx | 95 + .../home/[account]/members-cms/new/page.tsx | 183 + .../home/[account]/members-cms/page.tsx | 66 + .../[account]/members-cms/statistics/page.tsx | 92 + .../modules/[moduleId]/[recordId]/page.tsx | 92 + .../modules/[moduleId]/import/page.tsx | 83 + .../[account]/modules/[moduleId]/new/page.tsx | 40 + .../[account]/modules/[moduleId]/page.tsx | 58 + .../modules/[moduleId]/settings/page.tsx | 152 + .../[locale]/home/[account]/modules/page.tsx | 66 + .../newsletter/[campaignId]/page.tsx | 196 + .../home/[account]/newsletter/new/page.tsx | 125 + .../home/[account]/newsletter/page.tsx | 178 + .../[account]/newsletter/templates/page.tsx | 118 + apps/web/app/[locale]/home/[account]/page.tsx | 392 +- apps/web/app/layout.tsx | 4 + apps/web/components.json | 25 + apps/web/components/cms-page-shell.tsx | 33 + apps/web/components/empty-state.tsx | 39 + apps/web/components/stats-card.tsx | 44 + apps/web/config/billing-plans.config.ts | 97 + apps/web/config/feature-flags.config.ts | 42 + apps/web/config/paths.config.ts | 16 + .../config/team-account-navigation.config.tsx | 62 +- apps/web/i18n/messages/de/account.json | 149 + apps/web/i18n/messages/de/auth.json | 119 + apps/web/i18n/messages/de/billing.json | 127 + apps/web/i18n/messages/de/cms.json | 271 + apps/web/i18n/messages/de/common.json | 123 + apps/web/i18n/messages/de/marketing.json | 46 + apps/web/i18n/messages/de/teams.json | 184 + apps/web/i18n/messages/en/cms.json | 271 + apps/web/i18n/messages/en/common.json | 10 +- apps/web/i18n/request.ts | 1 + apps/web/lib/database.types.ts | 2842 ++++++++- apps/web/lib/resolve-account.ts | 24 + apps/web/package.json | 12 + apps/web/styles/globals.css | 141 +- apps/web/styles/theme.css | 93 +- .../20260401000001_cms_foundation.sql | 448 ++ .../20260401000002_cms_permission_seeds.sql | 40 + .../20260402000001_module_engine.sql | 482 ++ .../20260403000001_member_management.sql | 211 + .../20260404000001_course_management.sql | 168 + .../20260405000001_booking_management.sql | 85 + .../20260406000001_event_management.sql | 98 + .../20260407000001_finance_sepa.sql | 118 + .../migrations/20260408000001_newsletter.sql | 70 + package.json | 6 + .../services/legacy-migration.service.ts | 250 + .../features/booking-management/package.json | 34 + .../src/components/index.ts | 1 + .../src/schema/booking.schema.ts | 40 + .../booking-management/src/server/api.ts | 88 + .../features/booking-management/tsconfig.json | 6 + .../features/course-management/package.json | 34 + .../course-management/src/components/index.ts | 1 + .../src/schema/course.schema.ts | 74 + .../course-management/src/server/api.ts | 145 + .../features/course-management/tsconfig.json | 6 + .../features/document-generator/package.json | 34 + .../src/components/index.ts | 1 + .../src/schema/document.schema.ts | 31 + .../document-generator/src/server/api.ts | 96 + .../features/document-generator/tsconfig.json | 6 + .../features/event-management/package.json | 34 + .../event-management/src/components/index.ts | 1 + .../src/schema/event.schema.ts | 44 + .../event-management/src/server/api.ts | 83 + .../features/event-management/tsconfig.json | 6 + packages/features/finance/package.json | 34 + .../features/finance/src/components/index.ts | 1 + .../finance/src/schema/finance.schema.ts | 45 + packages/features/finance/src/server/api.ts | 154 + .../services/sepa-xml-generator.service.ts | 221 + packages/features/finance/tsconfig.json | 6 + .../features/member-management/package.json | 34 + .../member-management/src/components/index.ts | 3 + .../src/schema/member.schema.ts | 55 + .../member-management/src/server/api.ts | 190 + .../features/member-management/tsconfig.json | 6 + packages/features/module-builder/package.json | 37 + .../src/components/field-renderer.tsx | 273 + .../module-builder/src/components/index.ts | 5 + .../src/components/module-form.tsx | 123 + .../src/components/module-search.tsx | 167 + .../src/components/module-table.tsx | 193 + .../src/components/module-toolbar.tsx | 104 + .../src/schema/module-field.schema.ts | 76 + .../src/schema/module-import.schema.ts | 23 + .../src/schema/module-query.schema.ts | 33 + .../src/schema/module-record.schema.ts | 35 + .../src/schema/module.schema.ts | 56 + .../src/server/actions/module-actions.ts | 56 + .../src/server/actions/record-actions.ts | 132 + .../features/module-builder/src/server/api.ts | 24 + .../src/server/services/audit.service.ts | 33 + .../services/module-definition.service.ts | 120 + .../server/services/module-query.service.ts | 39 + .../server/services/record-crud.service.ts | 105 + .../services/record-validation.service.ts | 178 + .../features/module-builder/tsconfig.json | 8 + packages/features/newsletter/package.json | 34 + .../newsletter/src/components/index.ts | 1 + .../src/schema/newsletter.schema.ts | 32 + .../features/newsletter/src/server/api.ts | 158 + packages/features/newsletter/tsconfig.json | 6 + packages/i18n/src/locales.tsx | 6 +- packages/supabase/src/database.types.ts | 5404 ++++++++++++----- pnpm-lock.yaml | 1509 ++++- pnpm-workspace.yaml | 8 + run-mcp.sh | 4 + 155 files changed, 23483 insertions(+), 1722 deletions(-) create mode 100644 apps/e2e/tests/course-enrollment.spec.ts create mode 100644 apps/e2e/tests/member-lifecycle.spec.ts create mode 100644 apps/e2e/tests/module-builder.spec.ts create mode 100644 apps/e2e/tests/newsletter.spec.ts create mode 100644 apps/e2e/tests/sepa-batch.spec.ts create mode 100644 apps/web/app/[locale]/admin/audit/page.tsx create mode 100644 apps/web/app/[locale]/admin/gdpr/page.tsx create mode 100644 apps/web/app/[locale]/admin/migration/page.tsx create mode 100644 apps/web/app/[locale]/admin/modules/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/bookings/calendar/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/bookings/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/bookings/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/bookings/rooms/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/[courseId]/attendance/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/[courseId]/participants/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/calendar/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/categories/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/instructors/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/locations/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/courses/statistics/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/documents/generate/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/documents/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/documents/templates/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/events/[eventId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/events/holiday-passes/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/events/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/events/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/events/registrations/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/invoices/[id]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/invoices/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/invoices/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/payments/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/sepa/[batchId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/sepa/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/finance/sepa/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/members-cms/[memberId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/members-cms/applications/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/members-cms/dues/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/members-cms/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/members-cms/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/members-cms/statistics/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/modules/[moduleId]/[recordId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/modules/[moduleId]/import/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/modules/[moduleId]/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/modules/[moduleId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/modules/[moduleId]/settings/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/modules/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/newsletter/[campaignId]/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/newsletter/new/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/newsletter/page.tsx create mode 100644 apps/web/app/[locale]/home/[account]/newsletter/templates/page.tsx create mode 100644 apps/web/components.json create mode 100644 apps/web/components/cms-page-shell.tsx create mode 100644 apps/web/components/empty-state.tsx create mode 100644 apps/web/components/stats-card.tsx create mode 100644 apps/web/config/billing-plans.config.ts create mode 100644 apps/web/i18n/messages/de/account.json create mode 100644 apps/web/i18n/messages/de/auth.json create mode 100644 apps/web/i18n/messages/de/billing.json create mode 100644 apps/web/i18n/messages/de/cms.json create mode 100644 apps/web/i18n/messages/de/common.json create mode 100644 apps/web/i18n/messages/de/marketing.json create mode 100644 apps/web/i18n/messages/de/teams.json create mode 100644 apps/web/i18n/messages/en/cms.json create mode 100644 apps/web/lib/resolve-account.ts create mode 100644 apps/web/supabase/migrations/20260401000001_cms_foundation.sql create mode 100644 apps/web/supabase/migrations/20260401000002_cms_permission_seeds.sql create mode 100644 apps/web/supabase/migrations/20260402000001_module_engine.sql create mode 100644 apps/web/supabase/migrations/20260403000001_member_management.sql create mode 100644 apps/web/supabase/migrations/20260404000001_course_management.sql create mode 100644 apps/web/supabase/migrations/20260405000001_booking_management.sql create mode 100644 apps/web/supabase/migrations/20260406000001_event_management.sql create mode 100644 apps/web/supabase/migrations/20260407000001_finance_sepa.sql create mode 100644 apps/web/supabase/migrations/20260408000001_newsletter.sql create mode 100644 packages/features/admin/src/server/services/legacy-migration.service.ts create mode 100644 packages/features/booking-management/package.json create mode 100644 packages/features/booking-management/src/components/index.ts create mode 100644 packages/features/booking-management/src/schema/booking.schema.ts create mode 100644 packages/features/booking-management/src/server/api.ts create mode 100644 packages/features/booking-management/tsconfig.json create mode 100644 packages/features/course-management/package.json create mode 100644 packages/features/course-management/src/components/index.ts create mode 100644 packages/features/course-management/src/schema/course.schema.ts create mode 100644 packages/features/course-management/src/server/api.ts create mode 100644 packages/features/course-management/tsconfig.json create mode 100644 packages/features/document-generator/package.json create mode 100644 packages/features/document-generator/src/components/index.ts create mode 100644 packages/features/document-generator/src/schema/document.schema.ts create mode 100644 packages/features/document-generator/src/server/api.ts create mode 100644 packages/features/document-generator/tsconfig.json create mode 100644 packages/features/event-management/package.json create mode 100644 packages/features/event-management/src/components/index.ts create mode 100644 packages/features/event-management/src/schema/event.schema.ts create mode 100644 packages/features/event-management/src/server/api.ts create mode 100644 packages/features/event-management/tsconfig.json create mode 100644 packages/features/finance/package.json create mode 100644 packages/features/finance/src/components/index.ts create mode 100644 packages/features/finance/src/schema/finance.schema.ts create mode 100644 packages/features/finance/src/server/api.ts create mode 100644 packages/features/finance/src/server/services/sepa-xml-generator.service.ts create mode 100644 packages/features/finance/tsconfig.json create mode 100644 packages/features/member-management/package.json create mode 100644 packages/features/member-management/src/components/index.ts create mode 100644 packages/features/member-management/src/schema/member.schema.ts create mode 100644 packages/features/member-management/src/server/api.ts create mode 100644 packages/features/member-management/tsconfig.json create mode 100644 packages/features/module-builder/package.json create mode 100644 packages/features/module-builder/src/components/field-renderer.tsx create mode 100644 packages/features/module-builder/src/components/index.ts create mode 100644 packages/features/module-builder/src/components/module-form.tsx create mode 100644 packages/features/module-builder/src/components/module-search.tsx create mode 100644 packages/features/module-builder/src/components/module-table.tsx create mode 100644 packages/features/module-builder/src/components/module-toolbar.tsx create mode 100644 packages/features/module-builder/src/schema/module-field.schema.ts create mode 100644 packages/features/module-builder/src/schema/module-import.schema.ts create mode 100644 packages/features/module-builder/src/schema/module-query.schema.ts create mode 100644 packages/features/module-builder/src/schema/module-record.schema.ts create mode 100644 packages/features/module-builder/src/schema/module.schema.ts create mode 100644 packages/features/module-builder/src/server/actions/module-actions.ts create mode 100644 packages/features/module-builder/src/server/actions/record-actions.ts create mode 100644 packages/features/module-builder/src/server/api.ts create mode 100644 packages/features/module-builder/src/server/services/audit.service.ts create mode 100644 packages/features/module-builder/src/server/services/module-definition.service.ts create mode 100644 packages/features/module-builder/src/server/services/module-query.service.ts create mode 100644 packages/features/module-builder/src/server/services/record-crud.service.ts create mode 100644 packages/features/module-builder/src/server/services/record-validation.service.ts create mode 100644 packages/features/module-builder/tsconfig.json create mode 100644 packages/features/newsletter/package.json create mode 100644 packages/features/newsletter/src/components/index.ts create mode 100644 packages/features/newsletter/src/schema/newsletter.schema.ts create mode 100644 packages/features/newsletter/src/server/api.ts create mode 100644 packages/features/newsletter/tsconfig.json create mode 100755 run-mcp.sh diff --git a/apps/e2e/tests/course-enrollment.spec.ts b/apps/e2e/tests/course-enrollment.spec.ts new file mode 100644 index 000000000..5192ce081 --- /dev/null +++ b/apps/e2e/tests/course-enrollment.spec.ts @@ -0,0 +1,25 @@ +/** + * E2E Test: Course Enrollment + */ +import { test, expect } from '@playwright/test'; + +test.describe('Course Management', () => { + test('create course, enroll participant, check capacity, waitlist', async ({ page }) => { + // Create course with capacity 2 + // Enroll participant 1 → status: enrolled + // Enroll participant 2 → status: enrolled + // Enroll participant 3 → status: waitlisted (capacity full) + }); + + test('course calendar view shows sessions', async ({ page }) => { + // Create course with sessions + // Navigate to calendar + // Verify sessions visible + }); + + test('attendance tracking', async ({ page }) => { + // Create course + session + participants + // Mark attendance + // Verify attendance persists + }); +}); diff --git a/apps/e2e/tests/member-lifecycle.spec.ts b/apps/e2e/tests/member-lifecycle.spec.ts new file mode 100644 index 000000000..9f1f3d8bd --- /dev/null +++ b/apps/e2e/tests/member-lifecycle.spec.ts @@ -0,0 +1,29 @@ +/** + * E2E Test: Member Management — Full lifecycle + */ +import { test, expect } from '@playwright/test'; + +test.describe('Member Management', () => { + test('create member, edit, search, filter by status', async ({ page }) => { + await page.goto('/auth/sign-in'); + await page.fill('input[name="email"]', 'test@example.com'); + await page.fill('input[name="password"]', 'testpassword123'); + await page.click('button[type="submit"]'); + await page.waitForURL('**/home/**'); + + await page.click('text=Mitglieder'); + await expect(page.locator('h1')).toContainText('Mitglieder'); + }); + + test('application workflow: submit → review → approve → member created', async ({ page }) => { + // Submit application + // Review application + // Approve → verify member auto-created + }); + + test('SEPA mandate management', async ({ page }) => { + // Create member with IBAN + // Verify IBAN validation + // Create SEPA batch from dues + }); +}); diff --git a/apps/e2e/tests/module-builder.spec.ts b/apps/e2e/tests/module-builder.spec.ts new file mode 100644 index 000000000..30363fcf2 --- /dev/null +++ b/apps/e2e/tests/module-builder.spec.ts @@ -0,0 +1,31 @@ +/** + * E2E Test: Module Builder — Full CRUD lifecycle + */ +import { test, expect } from '@playwright/test'; + +test.describe('Module Builder', () => { + test('create module, add fields, insert record, query, update, soft-delete', async ({ page }) => { + // Login + await page.goto('/auth/sign-in'); + await page.fill('input[name="email"]', 'test@example.com'); + await page.fill('input[name="password"]', 'testpassword123'); + await page.click('button[type="submit"]'); + await page.waitForURL('**/home/**'); + + // Navigate to modules + await page.click('text=Module'); + await expect(page.locator('h1')).toContainText('Module'); + + // Create module via API or UI + // ... test continues with full CRUD cycle + }); +}); + +test.describe('Cross-tenant isolation', () => { + test('tenant A cannot see tenant B data', async ({ page }) => { + // Login as tenant A user + // Verify can see own modules + // Verify cannot access tenant B module URL + // Verify API returns only own data + }); +}); diff --git a/apps/e2e/tests/newsletter.spec.ts b/apps/e2e/tests/newsletter.spec.ts new file mode 100644 index 000000000..ef4f679f0 --- /dev/null +++ b/apps/e2e/tests/newsletter.spec.ts @@ -0,0 +1,20 @@ +/** + * E2E Test: Newsletter + */ +import { test, expect } from '@playwright/test'; + +test.describe('Newsletter', () => { + test('create campaign, select recipients from members, preview, send', async ({ page }) => { + // Create newsletter + // Add recipients from member filter (status=active, hasEmail=true) + // Preview with variable substitution + // Dispatch + // Verify sent_count + }); + + test('template variable substitution works', async ({ page }) => { + // Create template with {{first_name}} {{member_number}} + // Create newsletter from template + // Preview — verify variables replaced + }); +}); diff --git a/apps/e2e/tests/sepa-batch.spec.ts b/apps/e2e/tests/sepa-batch.spec.ts new file mode 100644 index 000000000..44ffe5349 --- /dev/null +++ b/apps/e2e/tests/sepa-batch.spec.ts @@ -0,0 +1,25 @@ +/** + * E2E Test: SEPA Batch Processing + */ +import { test, expect } from '@playwright/test'; + +test.describe('SEPA / Finance', () => { + test('create SEPA direct debit batch, add items, generate XML', async ({ page }) => { + // Create batch + // Add items with valid IBANs + // Generate XML + // Verify pain.008.003.02 format + // Verify amounts sum correctly + }); + + test('IBAN validation rejects invalid IBANs', async ({ page }) => { + // Try to add item with invalid IBAN + // Verify rejection + }); + + test('invoice creation with line items', async ({ page }) => { + // Create invoice + // Add 3 line items + // Verify subtotal, tax, total calculations + }); +}); diff --git a/apps/web/app/[locale]/admin/audit/page.tsx b/apps/web/app/[locale]/admin/audit/page.tsx new file mode 100644 index 000000000..4f3642a16 --- /dev/null +++ b/apps/web/app/[locale]/admin/audit/page.tsx @@ -0,0 +1,20 @@ +export default async function AdminAuditPage() { + return ( +
+
+

Protokoll

+

+ Mandantenübergreifendes Änderungsprotokoll +

+
+ +
+

+ Alle Datenänderungen (Erstellen, Ändern, Löschen, Sperren) + über alle Mandanten hinweg. Filtert nach Zeitraum, Benutzer, + Tabelle und Aktion. +

+
+
+ ); +} diff --git a/apps/web/app/[locale]/admin/gdpr/page.tsx b/apps/web/app/[locale]/admin/gdpr/page.tsx new file mode 100644 index 000000000..d4de28230 --- /dev/null +++ b/apps/web/app/[locale]/admin/gdpr/page.tsx @@ -0,0 +1,20 @@ +export default async function AdminGdprPage() { + return ( +
+
+

DSGVO-Verarbeitungsverzeichnis

+

+ Art. 30 DSGVO — Überblick über alle Verarbeitungstätigkeiten +

+
+ +
+

+ Mandantenübergreifende Übersicht aller registrierten Verarbeitungstätigkeiten + gemäß Art. 30 DSGVO. Umfasst Zweck, Rechtsgrundlage, Datenkategorien, + Aufbewahrungsfristen und technisch-organisatorische Maßnahmen. +

+
+
+ ); +} diff --git a/apps/web/app/[locale]/admin/migration/page.tsx b/apps/web/app/[locale]/admin/migration/page.tsx new file mode 100644 index 000000000..b721e8ec9 --- /dev/null +++ b/apps/web/app/[locale]/admin/migration/page.tsx @@ -0,0 +1,33 @@ +export default async function AdminMigrationPage() { + return ( +
+
+

Datenmigration

+

+ Legacy-Daten aus MyEasyCMS (MySQL) in die neue Plattform migrieren +

+
+ +
+

Migrationsschritte

+
    +
  1. MySQL-Verbindung konfigurieren
  2. +
  3. Mandanten (user_profile → team accounts) zuordnen
  4. +
  5. Benutzer (cms_user → auth.users) migrieren
  6. +
  7. Module (m_module/m_modulfeld → modules/module_fields) übertragen
  8. +
  9. Mitglieder (ve_mitglieder → members) importieren
  10. +
  11. Kurse (ve_kurse → courses) importieren
  12. +
  13. Dateien (cms_files → Supabase Storage) hochladen
  14. +
  15. Daten verifizieren und bereinigen
  16. +
+ +
+

+ Hinweis: Die Migration erfordert eine MySQL-Verbindung zum Legacy-System. + Stellen Sie sicher, dass mysql2 installiert ist und die Verbindungsdaten korrekt konfiguriert sind. +

+
+
+
+ ); +} diff --git a/apps/web/app/[locale]/admin/modules/page.tsx b/apps/web/app/[locale]/admin/modules/page.tsx new file mode 100644 index 000000000..44db15c31 --- /dev/null +++ b/apps/web/app/[locale]/admin/modules/page.tsx @@ -0,0 +1,19 @@ +export default async function AdminModulesPage() { + return ( +
+
+

Module-Übersicht

+

+ Mandantenübergreifende Modulverwaltung +

+
+ +
+

+ Hier werden alle Module über alle Mandanten hinweg angezeigt. + Ermöglicht die zentrale Verwaltung von Modulvorlagen und -konfigurationen. +

+
+
+ ); +} diff --git a/apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx new file mode 100644 index 000000000..f156b848f --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx @@ -0,0 +1,363 @@ +import Link from 'next/link'; + +import { + ArrowLeft, + BedDouble, + CalendarDays, + LogIn, + LogOut, + XCircle, + User, +} from 'lucide-react'; + +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { Badge } from '@kit/ui/badge'; +import { Button } from '@kit/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@kit/ui/card'; + +import { createBookingManagementApi } from '@kit/booking-management/api'; + +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface PageProps { + params: Promise<{ account: string; bookingId: string }>; +} + +const STATUS_BADGE_VARIANT: Record< + string, + 'secondary' | 'default' | 'info' | 'outline' | 'destructive' +> = { + pending: 'secondary', + confirmed: 'default', + checked_in: 'info', + checked_out: 'outline', + cancelled: 'destructive', + no_show: 'destructive', +}; + +const STATUS_LABEL: Record = { + pending: 'Ausstehend', + confirmed: 'Bestätigt', + checked_in: 'Eingecheckt', + checked_out: 'Ausgecheckt', + cancelled: 'Storniert', + no_show: 'Nicht erschienen', +}; + +export default async function BookingDetailPage({ params }: PageProps) { + const { account, bookingId } = await params; + const client = getSupabaseServerClient(); + + const { data: acct } = await client + .from('accounts') + .select('id') + .eq('slug', account) + .single(); + + if (!acct) return
Konto nicht gefunden
; + + const api = createBookingManagementApi(client); + + // Load booking directly + const { data: booking } = await client + .from('bookings') + .select('*') + .eq('id', bookingId) + .eq('account_id', acct.id) + .single(); + + if (!booking) { + return ( + +
+

+ Buchung mit ID "{bookingId}" wurde nicht gefunden. +

+ + + +
+
+ ); + } + + // Load related room and guest data + const [roomResult, guestResult] = await Promise.all([ + booking.room_id + ? client.from('rooms').select('*').eq('id', booking.room_id).single() + : Promise.resolve({ data: null }), + booking.guest_id + ? client.from('guests').select('*').eq('id', booking.guest_id).single() + : Promise.resolve({ data: null }), + ]); + + const room = roomResult.data; + const guest = guestResult.data; + const status = String(booking.status ?? 'pending'); + + return ( + +
+ {/* Header */} +
+
+ + + +
+
+

Buchungsdetails

+ + {STATUS_LABEL[status] ?? status} + +
+

+ ID: {bookingId} +

+
+
+
+ +
+ {/* Zimmer */} + + + + + Zimmer + + + + {room ? ( +
+
+ + Zimmernummer + + + {String(room.room_number)} + +
+ {room.name && ( +
+ + Name + + {String(room.name)} +
+ )} +
+ Typ + + {String(room.room_type ?? '—')} + +
+
+ ) : ( +

+ Kein Zimmer zugewiesen +

+ )} +
+
+ + {/* Gast */} + + + + + Gast + + + + {guest ? ( +
+
+ Name + + {String(guest.first_name)} {String(guest.last_name)} + +
+ {guest.email && ( +
+ + E-Mail + + + {String(guest.email)} + +
+ )} + {guest.phone && ( +
+ + Telefon + + + {String(guest.phone)} + +
+ )} +
+ ) : ( +

+ Kein Gast zugewiesen +

+ )} +
+
+ + {/* Aufenthalt */} + + + + + Aufenthalt + + + +
+
+ + Check-in + + + {booking.check_in + ? new Date(String(booking.check_in)).toLocaleDateString( + 'de-DE', + { + weekday: 'short', + day: '2-digit', + month: '2-digit', + year: 'numeric', + }, + ) + : '—'} + +
+
+ + Check-out + + + {booking.check_out + ? new Date(String(booking.check_out)).toLocaleDateString( + 'de-DE', + { + weekday: 'short', + day: '2-digit', + month: '2-digit', + year: 'numeric', + }, + ) + : '—'} + +
+
+ + Erwachsene + + + {booking.adults ?? '—'} + +
+
+ + Kinder + + + {booking.children ?? 0} + +
+
+
+
+ + {/* Betrag */} + + + Betrag + + +
+
+ + Gesamtpreis + + + {booking.total_price != null + ? `${Number(booking.total_price).toFixed(2)} €` + : '—'} + +
+ {booking.notes && ( +
+ + Notizen + +

{String(booking.notes)}

+
+ )} +
+
+
+
+ + {/* Status Workflow */} + + + Aktionen + + Status der Buchung ändern + + + +
+ {(status === 'pending' || status === 'confirmed') && ( + + )} + + {status === 'checked_in' && ( + + )} + + {status !== 'cancelled' && + status !== 'checked_out' && + status !== 'no_show' && ( + + )} + + {status === 'cancelled' || status === 'checked_out' ? ( +

+ Diese Buchung ist{' '} + {status === 'cancelled' ? 'storniert' : 'abgeschlossen'} — keine + weiteren Aktionen verfügbar. +

+ ) : null} +
+
+
+
+
+ ); +} diff --git a/apps/web/app/[locale]/home/[account]/bookings/calendar/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/calendar/page.tsx new file mode 100644 index 000000000..b7d278dbb --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/bookings/calendar/page.tsx @@ -0,0 +1,230 @@ +import Link from 'next/link'; + +import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react'; + +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { Badge } from '@kit/ui/badge'; +import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; + +import { createBookingManagementApi } from '@kit/booking-management/api'; + +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface PageProps { + params: Promise<{ account: string }>; +} + +const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + +const MONTH_NAMES = [ + 'Januar', + 'Februar', + 'März', + 'April', + 'Mai', + 'Juni', + 'Juli', + 'August', + 'September', + 'Oktober', + 'November', + 'Dezember', +]; + +function getDaysInMonth(year: number, month: number): number { + return new Date(year, month + 1, 0).getDate(); +} + +function getFirstWeekday(year: number, month: number): number { + const day = new Date(year, month, 1).getDay(); + // Convert Sunday=0 to Monday-based (Mo=0, Di=1 … So=6) + return day === 0 ? 6 : day - 1; +} + +function isDateInRange(date: string, checkIn: string, checkOut: string): boolean { + return date >= checkIn && date < checkOut; +} + +export default async function BookingCalendarPage({ 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
Konto nicht gefunden
; + + const api = createBookingManagementApi(client); + + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth(); + const daysInMonth = getDaysInMonth(year, month); + const firstWeekday = getFirstWeekday(year, month); + + // Load bookings for this month + const monthStart = `${year}-${String(month + 1).padStart(2, '0')}-01`; + const monthEnd = `${year}-${String(month + 1).padStart(2, '0')}-${String(daysInMonth).padStart(2, '0')}`; + + const bookings = await api.listBookings(acct.id, { + from: monthStart, + to: monthEnd, + page: 1, + }); + + // Build set of occupied dates + const occupiedDates = new Set(); + for (const booking of bookings.data) { + const b = booking as Record; + if (b.status === 'cancelled' || b.status === 'no_show') continue; + const checkIn = String(b.check_in ?? ''); + const checkOut = String(b.check_out ?? ''); + if (!checkIn || !checkOut) continue; + + for (let d = 1; d <= daysInMonth; d++) { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; + if (isDateInRange(dateStr, checkIn, checkOut)) { + occupiedDates.add(dateStr); + } + } + } + + // Build calendar grid cells + const cells: Array<{ day: number | null; occupied: boolean; isToday: boolean }> = []; + + // Empty cells before first day + for (let i = 0; i < firstWeekday; i++) { + cells.push({ day: null, occupied: false, isToday: false }); + } + + const todayStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + + for (let d = 1; d <= daysInMonth; d++) { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; + cells.push({ + day: d, + occupied: occupiedDates.has(dateStr), + isToday: dateStr === todayStr, + }); + } + + // Fill remaining cells to complete the grid + while (cells.length % 7 !== 0) { + cells.push({ day: null, occupied: false, isToday: false }); + } + + return ( + +
+ {/* Header */} +
+
+ + + +
+

Belegungskalender

+

+ Zimmerauslastung im Überblick +

+
+
+
+ + {/* Month Navigation */} + + +
+ + + {MONTH_NAMES[month]} {year} + + +
+
+ + {/* Weekday Header */} +
+ {WEEKDAYS.map((day) => ( +
+ {day} +
+ ))} +
+ + {/* Calendar Grid */} +
+ {cells.map((cell, idx) => ( +
+ {cell.day !== null && ( + <> + {cell.day} + {cell.occupied && ( + + )} + + )} +
+ ))} +
+ + {/* Legend */} +
+
+ + Belegt +
+
+ + Frei +
+
+ + Heute +
+
+
+
+ + {/* Summary */} + + +
+
+

+ Buchungen in diesem Monat +

+

{bookings.data.length}

+
+ + {occupiedDates.size} von {daysInMonth} Tagen belegt + +
+
+
+
+
+ ); +} diff --git a/apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx new file mode 100644 index 000000000..cb8abc52d --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx @@ -0,0 +1,90 @@ +import { UserCircle, Plus } from 'lucide-react'; + +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; + +import { createBookingManagementApi } from '@kit/booking-management/api'; + +import { CmsPageShell } from '~/components/cms-page-shell'; +import { EmptyState } from '~/components/empty-state'; + +interface PageProps { + params: Promise<{ account: string }>; +} + +export default async function GuestsPage({ 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
Konto nicht gefunden
; + + const api = createBookingManagementApi(client); + const guests = await api.listGuests(acct.id); + + return ( + +
+
+
+

Gäste

+

Gästeverwaltung

+
+ +
+ + {guests.length === 0 ? ( + } + title="Keine Gäste vorhanden" + description="Legen Sie Ihren ersten Gast an." + actionLabel="Neuer Gast" + /> + ) : ( + + + Alle Gäste ({guests.length}) + + +
+ + + + + + + + + + + + {guests.map((guest: Record) => ( + + + + + + + + ))} + +
NameE-MailTelefonStadtLand
+ {String(guest.last_name ?? '')}, {String(guest.first_name ?? '')} + {String(guest.email ?? '—')}{String(guest.phone ?? '—')}{String(guest.city ?? '—')}{String(guest.country ?? '—')}
+
+
+
+ )} +
+
+ ); +} diff --git a/apps/web/app/[locale]/home/[account]/bookings/new/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/new/page.tsx new file mode 100644 index 000000000..890e225f6 --- /dev/null +++ b/apps/web/app/[locale]/home/[account]/bookings/new/page.tsx @@ -0,0 +1,235 @@ +import Link from 'next/link'; + +import { ArrowLeft, BedDouble } from 'lucide-react'; + +import { getSupabaseServerClient } from '@kit/supabase/server-client'; +import { Button } from '@kit/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@kit/ui/card'; + +import { createBookingManagementApi } from '@kit/booking-management/api'; + +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface PageProps { + params: Promise<{ account: string }>; +} + +export default async function NewBookingPage({ 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
Konto nicht gefunden
; + + const api = createBookingManagementApi(client); + + const [rooms, guests] = await Promise.all([ + api.listRooms(acct.id), + api.listGuests(acct.id), + ]); + + return ( + +
+ {/* Header */} +
+ + + +
+

Neue Buchung

+

+ Buchung für ein Zimmer erstellen +

+
+
+ +
+ {/* Zimmer & Gast */} + + + + + Zimmer & Gast + + + Wählen Sie Zimmer und Gast für die Buchung aus + + + +
+ + +
+ +
+ + +
+
+
+ + {/* Aufenthalt */} + + + Aufenthalt + + Reisedaten und Personenanzahl + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Preis & Notizen */} + + + Preis & Notizen + + +
+ + +
+ +
+ +