diff --git a/.env.production.example b/.env.production.example index a79d41842..33239e97f 100644 --- a/.env.production.example +++ b/.env.production.example @@ -44,6 +44,12 @@ ADDITIONAL_REDIRECT_URLS= # --- Webhooks --- DB_WEBHOOK_SECRET=your-webhook-secret +# --- Monitoring (Sentry) --- +NEXT_PUBLIC_MONITORING_PROVIDER=sentry +NEXT_PUBLIC_SENTRY_DSN=https://your-dsn@o123456.ingest.sentry.io/123456 +# NEXT_PUBLIC_SENTRY_ENVIRONMENT=production +# SENTRY_AUTH_TOKEN=your-auth-token-for-source-maps + # --- Feature Flags --- # All default to true, set to false to disable # NEXT_PUBLIC_ENABLE_MODULE_BUILDER=true diff --git a/apps/e2e/tests/course-enrollment.spec.ts b/apps/e2e/tests/course-enrollment.spec.ts index 5192ce081..5d874d7c2 100644 --- a/apps/e2e/tests/course-enrollment.spec.ts +++ b/apps/e2e/tests/course-enrollment.spec.ts @@ -4,7 +4,9 @@ import { test, expect } from '@playwright/test'; test.describe('Course Management', () => { - test('create course, enroll participant, check capacity, waitlist', async ({ page }) => { + 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 diff --git a/apps/e2e/tests/member-lifecycle.spec.ts b/apps/e2e/tests/member-lifecycle.spec.ts index 9f1f3d8bd..bff3e6ebe 100644 --- a/apps/e2e/tests/member-lifecycle.spec.ts +++ b/apps/e2e/tests/member-lifecycle.spec.ts @@ -15,7 +15,9 @@ test.describe('Member Management', () => { await expect(page.locator('h1')).toContainText('Mitglieder'); }); - test('application workflow: submit → review → approve → member created', async ({ page }) => { + test('application workflow: submit → review → approve → member created', async ({ + page, + }) => { // Submit application // Review application // Approve → verify member auto-created diff --git a/apps/e2e/tests/module-builder.spec.ts b/apps/e2e/tests/module-builder.spec.ts index 30363fcf2..831923127 100644 --- a/apps/e2e/tests/module-builder.spec.ts +++ b/apps/e2e/tests/module-builder.spec.ts @@ -4,7 +4,9 @@ import { test, expect } from '@playwright/test'; test.describe('Module Builder', () => { - test('create module, add fields, insert record, query, update, soft-delete', async ({ page }) => { + 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'); diff --git a/apps/e2e/tests/newsletter.spec.ts b/apps/e2e/tests/newsletter.spec.ts index ef4f679f0..2a0fef5c2 100644 --- a/apps/e2e/tests/newsletter.spec.ts +++ b/apps/e2e/tests/newsletter.spec.ts @@ -4,7 +4,9 @@ import { test, expect } from '@playwright/test'; test.describe('Newsletter', () => { - test('create campaign, select recipients from members, preview, send', async ({ page }) => { + 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 diff --git a/apps/e2e/tests/sepa-batch.spec.ts b/apps/e2e/tests/sepa-batch.spec.ts index 44ffe5349..3519b6c21 100644 --- a/apps/e2e/tests/sepa-batch.spec.ts +++ b/apps/e2e/tests/sepa-batch.spec.ts @@ -4,7 +4,9 @@ import { test, expect } from '@playwright/test'; test.describe('SEPA / Finance', () => { - test('create SEPA direct debit batch, add items, generate XML', async ({ page }) => { + test('create SEPA direct debit batch, add items, generate XML', async ({ + page, + }) => { // Create batch // Add items with valid IBANs // Generate XML diff --git a/apps/web/app/[locale]/(marketing)/page.tsx b/apps/web/app/[locale]/(marketing)/page.tsx index 7d7f36de9..a79736f12 100644 --- a/apps/web/app/[locale]/(marketing)/page.tsx +++ b/apps/web/app/[locale]/(marketing)/page.tsx @@ -78,7 +78,7 @@ function Home() { {/* Trust Indicators */}
-

+

@@ -89,10 +89,7 @@ function Home() { label="marketing.trustSchools" /> - +
@@ -184,9 +181,7 @@ function Home() { .{' '} - + } @@ -256,7 +251,7 @@ function Home() {
-

+

@@ -316,7 +311,7 @@ function Home() { {/* Final CTA */}

-

+

diff --git a/apps/web/app/[locale]/admin/audit/page.tsx b/apps/web/app/[locale]/admin/audit/page.tsx index 4f3642a16..4c4ec5a98 100644 --- a/apps/web/app/[locale]/admin/audit/page.tsx +++ b/apps/web/app/[locale]/admin/audit/page.tsx @@ -9,10 +9,9 @@ export default async function AdminAuditPage() {

-

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

+ 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 index d4de28230..a38d6217a 100644 --- a/apps/web/app/[locale]/admin/gdpr/page.tsx +++ b/apps/web/app/[locale]/admin/gdpr/page.tsx @@ -9,10 +9,11 @@ export default async function AdminGdprPage() {
-

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

+ 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 index b721e8ec9..966333aa4 100644 --- a/apps/web/app/[locale]/admin/migration/page.tsx +++ b/apps/web/app/[locale]/admin/migration/page.tsx @@ -8,23 +8,27 @@ export default async function AdminMigrationPage() {

-
+

Migrationsschritte

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

    - Hinweis: Die Migration erfordert eine MySQL-Verbindung zum Legacy-System. - Stellen Sie sicher, dass mysql2 installiert ist und die Verbindungsdaten korrekt konfiguriert sind. + 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 index 44db15c31..ad801f327 100644 --- a/apps/web/app/[locale]/admin/modules/page.tsx +++ b/apps/web/app/[locale]/admin/modules/page.tsx @@ -9,9 +9,10 @@ export default async function AdminModulesPage() {
-

+

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

diff --git a/apps/web/app/[locale]/club/[slug]/[...page]/page.tsx b/apps/web/app/[locale]/club/[slug]/[...page]/page.tsx index a750b2d94..ca9a4db69 100644 --- a/apps/web/app/[locale]/club/[slug]/[...page]/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/[...page]/page.tsx @@ -1,9 +1,13 @@ -import { createClient } from '@supabase/supabase-js'; import { notFound } from 'next/navigation'; + +import { createClient } from '@supabase/supabase-js'; + import { SiteRenderer } from '@kit/site-builder/components'; import type { SiteData } from '@kit/site-builder/context'; -interface Props { params: Promise<{ slug: string; page: string[] }> } +interface Props { + params: Promise<{ slug: string; page: string[] }>; +} export default async function ClubSubPage({ params }: Props) { const { slug, page: pagePath } = await params; @@ -14,36 +18,73 @@ export default async function ClubSubPage({ params }: Props) { process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!, ); - const { data: account } = await supabase.from('accounts').select('id').eq('slug', slug).single(); + const { data: account } = await supabase + .from('accounts') + .select('id') + .eq('slug', slug) + .single(); if (!account) notFound(); - const { data: settings } = await supabase.from('site_settings').select('*').eq('account_id', account.id).eq('is_public', true).maybeSingle(); + const { data: settings } = await supabase + .from('site_settings') + .select('*') + .eq('account_id', account.id) + .eq('is_public', true) + .maybeSingle(); if (!settings) notFound(); - const { data: sitePageData } = await supabase.from('site_pages').select('*') - .eq('account_id', account.id).eq('slug', pageSlug).eq('is_published', true).maybeSingle(); + const { data: sitePageData } = await supabase + .from('site_pages') + .select('*') + .eq('account_id', account.id) + .eq('slug', pageSlug) + .eq('is_published', true) + .maybeSingle(); if (!sitePageData) notFound(); // Pre-fetch CMS data for Puck components const [eventsRes, coursesRes, postsRes] = await Promise.all([ - supabase.from('events').select('id, name, event_date, event_time, location, fee, status') - .eq('account_id', account.id).order('event_date', { ascending: true }).limit(20), - supabase.from('courses').select('id, name, start_date, end_date, fee, capacity, status') - .eq('account_id', account.id).order('start_date', { ascending: true }).limit(20), - supabase.from('cms_posts').select('id, title, excerpt, cover_image, published_at, slug') - .eq('account_id', account.id).eq('status', 'published').order('published_at', { ascending: false }).limit(20), + supabase + .from('events') + .select('id, name, event_date, event_time, location, fee, status') + .eq('account_id', account.id) + .order('event_date', { ascending: true }) + .limit(20), + supabase + .from('courses') + .select('id, name, start_date, end_date, fee, capacity, status') + .eq('account_id', account.id) + .order('start_date', { ascending: true }) + .limit(20), + supabase + .from('cms_posts') + .select('id, title, excerpt, cover_image, published_at, slug') + .eq('account_id', account.id) + .eq('status', 'published') + .order('published_at', { ascending: false }) + .limit(20), ]); const siteData: SiteData = { accountId: account.id, events: eventsRes.data ?? [], - courses: (coursesRes.data ?? []).map(c => ({ ...c, enrolled_count: 0 })), + courses: (coursesRes.data ?? []).map((c) => ({ ...c, enrolled_count: 0 })), posts: postsRes.data ?? [], }; return ( -
- } siteData={siteData} /> +
+ } + siteData={siteData} + />
); } diff --git a/apps/web/app/[locale]/club/[slug]/newsletter/subscribe/page.tsx b/apps/web/app/[locale]/club/[slug]/newsletter/subscribe/page.tsx index 312880318..567d32132 100644 --- a/apps/web/app/[locale]/club/[slug]/newsletter/subscribe/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/newsletter/subscribe/page.tsx @@ -1,23 +1,28 @@ +import { Mail } from 'lucide-react'; + +import { Button } from '@kit/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Input } from '@kit/ui/input'; import { Label } from '@kit/ui/label'; -import { Button } from '@kit/ui/button'; -import { Mail } from 'lucide-react'; -interface Props { params: Promise<{ slug: string }> } +interface Props { + params: Promise<{ slug: string }>; +} export default async function NewsletterSubscribePage({ params }: Props) { const { slug } = await params; return ( -
+
-
- +
+
Newsletter abonnieren -

Bleiben Sie über Neuigkeiten informiert.

+

+ Bleiben Sie über Neuigkeiten informiert. +

@@ -27,11 +32,19 @@ export default async function NewsletterSubscribePage({ params }: Props) {
- +
- -

- Sie können sich jederzeit abmelden. Wir senden Ihnen eine Bestätigungs-E-Mail. + +

+ Sie können sich jederzeit abmelden. Wir senden Ihnen eine + Bestätigungs-E-Mail.

diff --git a/apps/web/app/[locale]/club/[slug]/newsletter/unsubscribe/page.tsx b/apps/web/app/[locale]/club/[slug]/newsletter/unsubscribe/page.tsx index deb842f65..e53e13a5f 100644 --- a/apps/web/app/[locale]/club/[slug]/newsletter/unsubscribe/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/newsletter/unsubscribe/page.tsx @@ -1,34 +1,51 @@ -import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; -import { Button } from '@kit/ui/button'; -import { MailX } from 'lucide-react'; import Link from 'next/link'; -interface Props { params: Promise<{ slug: string }>; searchParams: Promise<{ token?: string }> } +import { MailX } from 'lucide-react'; -export default async function NewsletterUnsubscribePage({ params, searchParams }: Props) { +import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; + +interface Props { + params: Promise<{ slug: string }>; + searchParams: Promise<{ token?: string }>; +} + +export default async function NewsletterUnsubscribePage({ + params, + searchParams, +}: Props) { const { slug } = await params; const { token } = await searchParams; return ( -
+
-
- +
+
Newsletter abbestellen {token ? ( <> -

Möchten Sie den Newsletter wirklich abbestellen?

- +

+ Möchten Sie den Newsletter wirklich abbestellen? +

+ ) : ( -

Kein gültiger Abmeldelink. Bitte verwenden Sie den Link aus der Newsletter-E-Mail.

+

+ Kein gültiger Abmeldelink. Bitte verwenden Sie den Link aus der + Newsletter-E-Mail. +

)} - +
diff --git a/apps/web/app/[locale]/club/[slug]/page.tsx b/apps/web/app/[locale]/club/[slug]/page.tsx index eea99aa17..47c9ee351 100644 --- a/apps/web/app/[locale]/club/[slug]/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/page.tsx @@ -1,9 +1,13 @@ -import { createClient } from '@supabase/supabase-js'; import { notFound } from 'next/navigation'; + +import { createClient } from '@supabase/supabase-js'; + import { SiteRenderer } from '@kit/site-builder/components'; import type { SiteData } from '@kit/site-builder/context'; -interface Props { params: Promise<{ slug: string }> } +interface Props { + params: Promise<{ slug: string }>; +} export default async function ClubHomePage({ params }: Props) { const { slug } = await params; @@ -13,36 +17,74 @@ export default async function ClubHomePage({ params }: Props) { process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!, ); - const { data: account } = await supabase.from('accounts').select('id, name').eq('slug', slug).single(); + const { data: account } = await supabase + .from('accounts') + .select('id, name') + .eq('slug', slug) + .single(); if (!account) notFound(); - const { data: settings } = await supabase.from('site_settings').select('*').eq('account_id', account.id).eq('is_public', true).maybeSingle(); + const { data: settings } = await supabase + .from('site_settings') + .select('*') + .eq('account_id', account.id) + .eq('is_public', true) + .maybeSingle(); if (!settings) notFound(); - const { data: page } = await supabase.from('site_pages').select('*') - .eq('account_id', account.id).eq('is_homepage', true).eq('is_published', true).maybeSingle(); + const { data: page } = await supabase + .from('site_pages') + .select('*') + .eq('account_id', account.id) + .eq('is_homepage', true) + .eq('is_published', true) + .maybeSingle(); if (!page) notFound(); // Pre-fetch CMS data for Puck components const [eventsRes, coursesRes, postsRes] = await Promise.all([ - supabase.from('events').select('id, name, event_date, event_time, location, fee, status') - .eq('account_id', account.id).order('event_date', { ascending: true }).limit(20), - supabase.from('courses').select('id, name, start_date, end_date, fee, capacity, status') - .eq('account_id', account.id).order('start_date', { ascending: true }).limit(20), - supabase.from('cms_posts').select('id, title, excerpt, cover_image, published_at, slug') - .eq('account_id', account.id).eq('status', 'published').order('published_at', { ascending: false }).limit(20), + supabase + .from('events') + .select('id, name, event_date, event_time, location, fee, status') + .eq('account_id', account.id) + .order('event_date', { ascending: true }) + .limit(20), + supabase + .from('courses') + .select('id, name, start_date, end_date, fee, capacity, status') + .eq('account_id', account.id) + .order('start_date', { ascending: true }) + .limit(20), + supabase + .from('cms_posts') + .select('id, title, excerpt, cover_image, published_at, slug') + .eq('account_id', account.id) + .eq('status', 'published') + .order('published_at', { ascending: false }) + .limit(20), ]); const siteData: SiteData = { accountId: account.id, events: eventsRes.data ?? [], - courses: (coursesRes.data ?? []).map(c => ({ ...c, enrolled_count: 0 })), + courses: (coursesRes.data ?? []).map((c) => ({ ...c, enrolled_count: 0 })), posts: postsRes.data ?? [], }; return ( -
- } siteData={siteData} /> +
+ } + siteData={siteData} + />
); } diff --git a/apps/web/app/[locale]/club/[slug]/portal/documents/page.tsx b/apps/web/app/[locale]/club/[slug]/portal/documents/page.tsx index d9553a275..6676ff638 100644 --- a/apps/web/app/[locale]/club/[slug]/portal/documents/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/portal/documents/page.tsx @@ -1,10 +1,14 @@ -import { createClient } from '@supabase/supabase-js'; -import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; -import { Button } from '@kit/ui/button'; -import { Badge } from '@kit/ui/badge'; -import { FileText, Download, Shield, Receipt, FileCheck } from 'lucide-react'; import Link from 'next/link'; +import { createClient } from '@supabase/supabase-js'; + +import { FileText, Download, Shield, Receipt, FileCheck } from 'lucide-react'; + +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'; + interface Props { params: Promise<{ slug: string }>; } @@ -14,77 +18,117 @@ export default async function PortalDocumentsPage({ params }: Props) { const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!, + process.env.SUPABASE_SERVICE_ROLE_KEY || + process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!, ); - const { data: account } = await supabase.from('accounts').select('id, name').eq('slug', slug).single(); - if (!account) return
Organisation nicht gefunden
; + const { data: account } = await supabase + .from('accounts') + .select('id, name') + .eq('slug', slug) + .single(); + if (!account) + return
Organisation nicht gefunden
; // Demo documents (in production: query invoices + cms_files for this member) const documents = [ - { id: '1', title: 'Mitgliedsbeitrag 2026', type: 'Rechnung', date: '2026-01-15', status: 'paid' }, - { id: '2', title: 'Mitgliedsbeitrag 2025', type: 'Rechnung', date: '2025-01-10', status: 'paid' }, - { id: '3', title: 'Beitrittserklärung', type: 'Dokument', date: '2020-01-15', status: 'signed' }, + { + id: '1', + title: 'Mitgliedsbeitrag 2026', + type: 'Rechnung', + date: '2026-01-15', + status: 'paid', + }, + { + id: '2', + title: 'Mitgliedsbeitrag 2025', + type: 'Rechnung', + date: '2025-01-10', + status: 'paid', + }, + { + id: '3', + title: 'Beitrittserklärung', + type: 'Dokument', + date: '2020-01-15', + status: 'signed', + }, ]; const getStatusBadge = (status: string) => { switch (status) { - case 'paid': return Bezahlt; - case 'open': return Offen; - case 'signed': return Unterschrieben; - default: return {status}; + case 'paid': + return Bezahlt; + case 'open': + return Offen; + case 'signed': + return Unterschrieben; + default: + return {status}; } }; const getIcon = (type: string) => { switch (type) { - case 'Rechnung': return ; - case 'Dokument': return ; - default: return ; + case 'Rechnung': + return ; + case 'Dokument': + return ; + default: + return ; } }; return ( -
-
-
+
+
+
- +

Meine Dokumente

- +
-
+
Verfügbare Dokumente -

{String(account.name)} — Dokumente und Rechnungen

+

+ {String(account.name)} — Dokumente und Rechnungen +

{documents.length === 0 ? ( -
- +
+

Keine Dokumente vorhanden

) : (
{documents.map((doc) => ( -
+
{getIcon(doc.type)}
-

{doc.title}

-

{doc.type} — {new Date(doc.date).toLocaleDateString('de-DE')}

+

{doc.title}

+

+ {doc.type} — {formatDate(doc.date)} +

{getStatusBadge(doc.status)}
diff --git a/apps/web/app/[locale]/club/[slug]/portal/invite/page.tsx b/apps/web/app/[locale]/club/[slug]/portal/invite/page.tsx index 4343d5812..164f46867 100644 --- a/apps/web/app/[locale]/club/[slug]/portal/invite/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/portal/invite/page.tsx @@ -1,18 +1,25 @@ -import { createClient } from '@supabase/supabase-js'; +import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; + +import { createClient } from '@supabase/supabase-js'; + +import { UserPlus, Shield, CheckCircle } from 'lucide-react'; + +import { formatDate } from '@kit/shared/dates'; import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Input } from '@kit/ui/input'; import { Label } from '@kit/ui/label'; -import { UserPlus, Shield, CheckCircle } from 'lucide-react'; -import Link from 'next/link'; interface Props { params: Promise<{ slug: string }>; searchParams: Promise<{ token?: string }>; } -export default async function PortalInvitePage({ params, searchParams }: Props) { +export default async function PortalInvitePage({ + params, + searchParams, +}: Props) { const { slug } = await params; const { token } = await searchParams; @@ -24,28 +31,35 @@ export default async function PortalInvitePage({ params, searchParams }: Props) ); // Resolve account - const { data: account } = await supabase.from('accounts').select('id, name').eq('slug', slug).single(); + const { data: account } = await supabase + .from('accounts') + .select('id, name') + .eq('slug', slug) + .single(); if (!account) notFound(); // Look up invitation - const { data: invitation } = await supabase.from('member_portal_invitations') + const { data: invitation } = await supabase + .from('member_portal_invitations') .select('id, email, status, expires_at, member_id') .eq('invite_token', token) .maybeSingle(); if (!invitation || invitation.status !== 'pending') { return ( -
+
- +

Einladung ungültig

-

- Diese Einladung ist abgelaufen, wurde bereits verwendet oder ist ungültig. - Bitte wenden Sie sich an Ihren Vereinsadministrator. +

+ Diese Einladung ist abgelaufen, wurde bereits verwendet oder ist + ungültig. Bitte wenden Sie sich an Ihren Vereinsadministrator.

- +
@@ -56,14 +70,14 @@ export default async function PortalInvitePage({ params, searchParams }: Props) const expired = new Date(invitation.expires_at) < new Date(); if (expired) { return ( -
+
- +

Einladung abgelaufen

-

- Diese Einladung ist am {new Date(invitation.expires_at).toLocaleDateString('de-DE')} abgelaufen. - Bitte fordern Sie eine neue Einladung an. +

+ Diese Einladung ist am {formatDate(invitation.expires_at)}{' '} + abgelaufen. Bitte fordern Sie eine neue Einladung an.

@@ -72,41 +86,67 @@ export default async function PortalInvitePage({ params, searchParams }: Props) } return ( -
+
-
- +
+
Einladung zum Mitgliederbereich -

{String(account.name)}

+

+ {String(account.name)} +

-
+

- Sie wurden eingeladen, ein Konto für den Mitgliederbereich zu erstellen. - Damit können Sie Ihr Profil einsehen, Dokumente herunterladen und Ihre Datenschutz-Einstellungen verwalten. + Sie wurden eingeladen, ein Konto für den Mitgliederbereich zu + erstellen. Damit können Sie Ihr Profil einsehen, Dokumente + herunterladen und Ihre Datenschutz-Einstellungen verwalten.

-
+
- -

Ihre E-Mail-Adresse wurde vom Verein vorgegeben.

+ +

+ Ihre E-Mail-Adresse wurde vom Verein vorgegeben. +

- +
- +
-

- Bereits ein Konto? Anmelden +

+ Bereits ein Konto?{' '} + + Anmelden +

diff --git a/apps/web/app/[locale]/club/[slug]/portal/page.tsx b/apps/web/app/[locale]/club/[slug]/portal/page.tsx index cedce27ec..f54a9b211 100644 --- a/apps/web/app/[locale]/club/[slug]/portal/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/portal/page.tsx @@ -1,10 +1,12 @@ -import { createClient } from '@supabase/supabase-js'; -import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; -import { Button } from '@kit/ui/button'; -import { UserCircle, FileText, CreditCard, Shield } from 'lucide-react'; import Link from 'next/link'; +import { createClient } from '@supabase/supabase-js'; + +import { UserCircle, FileText, CreditCard, Shield } from 'lucide-react'; + import { PortalLoginForm } from '@kit/site-builder/components'; +import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; interface Props { params: Promise<{ slug: string }>; @@ -18,15 +20,23 @@ export default async function MemberPortalPage({ params }: Props) { process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!, ); - const { data: account } = await supabase.from('accounts').select('id, name').eq('slug', slug).single(); - if (!account) return
Organisation nicht gefunden
; + const { data: account } = await supabase + .from('accounts') + .select('id, name') + .eq('slug', slug) + .single(); + if (!account) + return
Organisation nicht gefunden
; // Check if user is already logged in - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); if (user) { // Check if this user is a member of this club - const { data: member } = await supabase.from('members') + const { data: member } = await supabase + .from('members') .select('id, first_name, last_name, status') .eq('account_id', account.id) .eq('user_id', user.id) @@ -35,45 +45,61 @@ export default async function MemberPortalPage({ params }: Props) { if (member) { // Logged in member — show portal dashboard return ( -
-
-
+
+
+
- -

Mitgliederbereich — {String(account.name)}

+ +

+ Mitgliederbereich — {String(account.name)} +

- {String(member.first_name)} {String(member.last_name)} - + + {String(member.first_name)} {String(member.last_name)} + + + +
-
-

Willkommen, {String(member.first_name)}!

+
+

+ Willkommen, {String(member.first_name)}! +

- + - +

Mein Profil

-

Kontaktdaten und Datenschutz

+

+ Kontaktdaten und Datenschutz +

- + - +

Dokumente

-

Rechnungen und Bescheinigungen

+

+ Rechnungen und Bescheinigungen +

- +

Mitgliedsausweis

-

Digital anzeigen

+

+ Digital anzeigen +

@@ -85,14 +111,18 @@ export default async function MemberPortalPage({ params }: Props) { // Not logged in or not a member — show login form return ( -
-
-
+
+
+

Mitgliederbereich

- + + +
-
+
diff --git a/apps/web/app/[locale]/club/[slug]/portal/profile/_components/portal-linked-accounts.tsx b/apps/web/app/[locale]/club/[slug]/portal/profile/_components/portal-linked-accounts.tsx new file mode 100644 index 000000000..761e8c0a2 --- /dev/null +++ b/apps/web/app/[locale]/club/[slug]/portal/profile/_components/portal-linked-accounts.tsx @@ -0,0 +1,244 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; + +import type { Provider, UserIdentity } from '@supabase/supabase-js'; +import { createClient } from '@supabase/supabase-js'; + +import { Link2, Link2Off, Loader2 } from 'lucide-react'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@kit/ui/alert-dialog'; +import { Button } from '@kit/ui/button'; +import { OauthProviderLogoImage } from '@kit/ui/oauth-provider-logo-image'; +import { toast } from '@kit/ui/sonner'; + +const PROVIDERS: Provider[] = ['google', 'apple', 'azure', 'github']; + +const PROVIDER_LABELS: Record = { + google: 'Google', + apple: 'Apple', + azure: 'Microsoft', + github: 'GitHub', +}; + +function getSupabaseClient() { + return createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!, + ); +} + +export function PortalLinkedAccounts({ slug }: { slug: string }) { + const [identities, setIdentities] = useState([]); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(null); + + const loadIdentities = useCallback(async () => { + const supabase = getSupabaseClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (user?.identities) { + setIdentities(user.identities); + } + + setLoading(false); + }, []); + + useEffect(() => { + void loadIdentities(); + }, [loadIdentities]); + + const handleLink = async (provider: Provider) => { + setActionLoading(provider); + + try { + const supabase = getSupabaseClient(); + const redirectTo = `${window.location.origin}/club/${slug}/portal/profile`; + + const { error } = await supabase.auth.linkIdentity({ + provider, + options: { redirectTo }, + }); + + if (error) { + toast.error(`Verknüpfung fehlgeschlagen: ${error.message}`); + setActionLoading(null); + } + } catch { + toast.error('Verbindungsfehler. Bitte versuchen Sie es erneut.'); + setActionLoading(null); + } + }; + + const handleUnlink = async (identity: UserIdentity) => { + if (identities.length <= 1) { + toast.error('Sie benötigen mindestens eine Anmeldemethode.'); + return; + } + + setActionLoading(identity.id); + + try { + const supabase = getSupabaseClient(); + const { error } = await supabase.auth.unlinkIdentity(identity); + + if (error) { + toast.error(`Trennung fehlgeschlagen: ${error.message}`); + } else { + toast.success( + `${PROVIDER_LABELS[identity.provider] ?? identity.provider} wurde getrennt.`, + ); + await loadIdentities(); + } + } catch { + toast.error('Verbindungsfehler. Bitte versuchen Sie es erneut.'); + } finally { + setActionLoading(null); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + const connectedProviders = identities + .filter((i) => i.provider !== 'email') + .map((i) => i.provider); + + const availableProviders = PROVIDERS.filter( + (p) => !connectedProviders.includes(p), + ); + + return ( +
+ {/* Connected accounts */} + {identities.filter((i) => i.provider !== 'email').length > 0 && ( +
+

+ Verknüpfte Konten +

+ + {identities + .filter((i) => i.provider !== 'email') + .map((identity) => ( +
+
+
+ +
+
+

+ {PROVIDER_LABELS[identity.provider] ?? identity.provider} +

+ {identity.identity_data?.email && ( +

+ {identity.identity_data.email as string} +

+ )} +
+
+ + {identities.length > 1 && ( + + + {actionLoading === identity.id ? ( + + ) : ( + + )} + + } + /> + + + + Konto trennen? + + Möchten Sie die Verknüpfung mit{' '} + {PROVIDER_LABELS[identity.provider] ?? + identity.provider}{' '} + wirklich aufheben? Sie können sich dann nicht mehr + darüber anmelden. + + + + Abbrechen + handleUnlink(identity)} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Trennen + + + + + )} +
+ ))} +
+ )} + + {/* Available providers to link */} + {availableProviders.length > 0 && ( +
+

+ Konto verknüpfen für schnellere Anmeldung +

+ +
+ {availableProviders.map((provider) => ( + + ))} +
+
+ )} + + {/* Info text when email-only */} + {identities.length <= 1 && availableProviders.length > 0 && ( +

+ Verknüpfen Sie ein Konto, um sich zukünftig schneller und ohne + Passwort anmelden zu können. +

+ )} +
+ ); +} diff --git a/apps/web/app/[locale]/club/[slug]/portal/profile/page.tsx b/apps/web/app/[locale]/club/[slug]/portal/profile/page.tsx index 0ba3b4ee2..f809a1c59 100644 --- a/apps/web/app/[locale]/club/[slug]/portal/profile/page.tsx +++ b/apps/web/app/[locale]/club/[slug]/portal/profile/page.tsx @@ -1,11 +1,25 @@ -import { createClient } from '@supabase/supabase-js'; +import Link from 'next/link'; import { redirect } from 'next/navigation'; -import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; + +import { createClient } from '@supabase/supabase-js'; + +import { + UserCircle, + Mail, + MapPin, + Phone, + Shield, + Calendar, + Link2, +} from 'lucide-react'; + +import { formatDate } from '@kit/shared/dates'; import { Button } from '@kit/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Input } from '@kit/ui/input'; import { Label } from '@kit/ui/label'; -import { UserCircle, Mail, MapPin, Phone, Shield, Calendar } from 'lucide-react'; -import Link from 'next/link'; + +import { PortalLinkedAccounts } from './_components/portal-linked-accounts'; interface Props { params: Promise<{ slug: string }>; @@ -19,15 +33,23 @@ export default async function PortalProfilePage({ params }: Props) { process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!, ); - const { data: account } = await supabase.from('accounts').select('id, name').eq('slug', slug).single(); - if (!account) return
Organisation nicht gefunden
; + const { data: account } = await supabase + .from('accounts') + .select('id, name') + .eq('slug', slug) + .single(); + if (!account) + return
Organisation nicht gefunden
; // Get current user - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); if (!user) redirect(`/club/${slug}/portal`); // Find member linked to this user - const { data: member } = await supabase.from('members') + const { data: member } = await supabase + .from('members') .select('*') .eq('account_id', account.id) .eq('user_id', user.id) @@ -35,17 +57,20 @@ export default async function PortalProfilePage({ params }: Props) { if (!member) { return ( -
+
- +

Kein Mitglied

-

- Ihr Benutzerkonto ist nicht mit einem Mitgliedsprofil in diesem Verein verknüpft. - Bitte wenden Sie sich an Ihren Vereinsadministrator. +

+ Ihr Benutzerkonto ist nicht mit einem Mitgliedsprofil in diesem + Verein verknüpft. Bitte wenden Sie sich an Ihren + Vereinsadministrator.

- +
@@ -56,28 +81,35 @@ export default async function PortalProfilePage({ params }: Props) { const m = member; return ( -
-
-
+
+
+
- +

Mein Profil

- + + +
-
+
-
+
-

{String(m.first_name)} {String(m.last_name)}

-

- Nr. {String(m.member_number ?? '—')} — Mitglied seit {m.entry_date ? new Date(String(m.entry_date)).toLocaleDateString('de-DE') : '—'} +

+ {String(m.first_name)} {String(m.last_name)} +

+

+ Nr. {String(m.member_number ?? '—')} — Mitglied seit{' '} + {formatDate(m.entry_date)}

@@ -85,37 +117,111 @@ export default async function PortalProfilePage({ params }: Props) { - Kontaktdaten + + + + Kontaktdaten + + -
-
-
-
-
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
- Adresse + + + + Adresse + + -
-
-
-
+
+ + +
+
+ + +
+
+ + +
+
+ + +
- Datenschutz-Einwilligungen + + + + Anmeldemethoden + + + + + + + + + + + + Datenschutz-Einwilligungen + + {[ - { key: 'gdpr_newsletter', label: 'Newsletter per E-Mail', value: m.gdpr_newsletter }, - { key: 'gdpr_internet', label: 'Veröffentlichung auf der Homepage', value: m.gdpr_internet }, - { key: 'gdpr_print', label: 'Veröffentlichung in der Vereinszeitung', value: m.gdpr_print }, - { key: 'gdpr_birthday_info', label: 'Geburtstagsinfo an Mitglieder', value: m.gdpr_birthday_info }, + { + key: 'gdpr_newsletter', + label: 'Newsletter per E-Mail', + value: m.gdpr_newsletter, + }, + { + key: 'gdpr_internet', + label: 'Veröffentlichung auf der Homepage', + value: m.gdpr_internet, + }, + { + key: 'gdpr_print', + label: 'Veröffentlichung in der Vereinszeitung', + value: m.gdpr_print, + }, + { + key: 'gdpr_birthday_info', + label: 'Geburtstagsinfo an Mitglieder', + value: m.gdpr_birthday_info, + }, ].map(({ key, label, value }) => ( ))} diff --git a/apps/web/app/[locale]/home/[account]/_components/team-account-layout-mobile-navigation.tsx b/apps/web/app/[locale]/home/[account]/_components/team-account-layout-mobile-navigation.tsx index 70d208c25..477c3892f 100644 --- a/apps/web/app/[locale]/home/[account]/_components/team-account-layout-mobile-navigation.tsx +++ b/apps/web/app/[locale]/home/[account]/_components/team-account-layout-mobile-navigation.tsx @@ -49,26 +49,24 @@ export const TeamAccountLayoutMobileNavigation = ( ) => { const signOut = useSignOut(); - const Links = props.config.routes.map( - (item, index) => { - if ('children' in item) { - return item.children.map((child) => { - return ( - - ); - }); - } + const Links = props.config.routes.map((item, index) => { + if ('children' in item) { + return item.children.map((child) => { + return ( + + ); + }); + } - if ('divider' in item) { - return ; - } - }, - ); + if ('divider' in item) { + return ; + } + }); return ( diff --git a/apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx index 7fcb11e44..d2bc4a83f 100644 --- a/apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx +++ b/apps/web/app/[locale]/home/[account]/bookings/[bookingId]/page.tsx @@ -10,6 +10,7 @@ import { User, } from 'lucide-react'; +import { formatDate } from '@kit/shared/dates'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { Badge } from '@kit/ui/badge'; import { Button } from '@kit/ui/button'; @@ -21,8 +22,8 @@ import { CardTitle, } from '@kit/ui/card'; -import { CmsPageShell } from '~/components/cms-page-shell'; import { AccountNotFound } from '~/components/account-not-found'; +import { CmsPageShell } from '~/components/cms-page-shell'; interface PageProps { params: Promise<{ account: string; bookingId: string }>; @@ -124,9 +125,7 @@ export default async function BookingDetailPage({ params }: PageProps) { {STATUS_LABEL[status] ?? status}
-

- ID: {bookingId} -

+

ID: {bookingId}

@@ -144,7 +143,7 @@ export default async function BookingDetailPage({ params }: PageProps) { {room ? (
- + Zimmernummer @@ -153,14 +152,14 @@ export default async function BookingDetailPage({ params }: PageProps) {
{room.name && (
- + Name {String(room.name)}
)}
- Typ + Typ {String(room.room_type ?? '—')} @@ -186,29 +185,25 @@ export default async function BookingDetailPage({ params }: PageProps) { {guest ? (
- Name + Name {String(guest.first_name)} {String(guest.last_name)}
{guest.email && (
- + E-Mail - - {String(guest.email)} - + {String(guest.email)}
)} {guest.phone && (
- + Telefon - - {String(guest.phone)} - + {String(guest.phone)}
)}
@@ -231,56 +226,30 @@ export default async function BookingDetailPage({ params }: PageProps) {
- + Check-in - {booking.check_in - ? new Date(String(booking.check_in)).toLocaleDateString( - 'de-DE', - { - weekday: 'short', - day: '2-digit', - month: '2-digit', - year: 'numeric', - }, - ) - : '—'} + {formatDate(booking.check_in)}
- + Check-out - {booking.check_out - ? new Date(String(booking.check_out)).toLocaleDateString( - 'de-DE', - { - weekday: 'short', - day: '2-digit', - month: '2-digit', - year: 'numeric', - }, - ) - : '—'} + {formatDate(booking.check_out)}
- + Erwachsene - - {booking.adults ?? '—'} - + {booking.adults ?? '—'}
- - Kinder - - - {booking.children ?? 0} - + Kinder + {booking.children ?? 0}
@@ -294,7 +263,7 @@ export default async function BookingDetailPage({ params }: PageProps) {
- + Gesamtpreis @@ -305,7 +274,7 @@ export default async function BookingDetailPage({ params }: PageProps) {
{booking.notes && (
- + Notizen

{String(booking.notes)}

@@ -320,9 +289,7 @@ export default async function BookingDetailPage({ params }: PageProps) { Aktionen - - Status der Buchung ändern - + Status der Buchung ändern
@@ -350,10 +317,10 @@ export default async function BookingDetailPage({ params }: PageProps) { )} {status === 'cancelled' || status === 'checked_out' ? ( -

+

Diese Buchung ist{' '} - {status === 'cancelled' ? 'storniert' : 'abgeschlossen'} — keine - weiteren Aktionen verfügbar. + {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 index 82221898d..df3e7e21b 100644 --- a/apps/web/app/[locale]/home/[account]/bookings/calendar/page.tsx +++ b/apps/web/app/[locale]/home/[account]/bookings/calendar/page.tsx @@ -2,15 +2,14 @@ import Link from 'next/link'; import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react'; +import { createBookingManagementApi } from '@kit/booking-management/api'; 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'; import { AccountNotFound } from '~/components/account-not-found'; +import { CmsPageShell } from '~/components/cms-page-shell'; interface PageProps { params: Promise<{ account: string }>; @@ -43,7 +42,11 @@ function getFirstWeekday(year: number, month: number): number { return day === 0 ? 6 : day - 1; } -function isDateInRange(date: string, checkIn: string, checkOut: string): boolean { +function isDateInRange( + date: string, + checkIn: string, + checkOut: string, +): boolean { return date >= checkIn && date < checkOut; } @@ -101,7 +104,11 @@ export default async function BookingCalendarPage({ params }: PageProps) { } // Build calendar grid cells - const cells: Array<{ day: number | null; occupied: boolean; isToday: boolean }> = []; + const cells: Array<{ + day: number | null; + occupied: boolean; + isToday: boolean; + }> = []; // Empty cells before first day for (let i = 0; i < firstWeekday; i++) { @@ -158,11 +165,11 @@ export default async function BookingCalendarPage({ params }: PageProps) { {/* Weekday Header */} -
+
{WEEKDAYS.map((day) => (
{day}
@@ -180,13 +187,13 @@ export default async function BookingCalendarPage({ params }: PageProps) { : cell.occupied ? 'bg-primary/15 text-primary font-semibold' : 'bg-muted/30 hover:bg-muted/50' - } ${cell.isToday ? 'ring-2 ring-primary ring-offset-1' : ''}`} + } ${cell.isToday ? 'ring-primary ring-2 ring-offset-1' : ''}`} > {cell.day !== null && ( <> {cell.day} {cell.occupied && ( - + )} )} @@ -195,17 +202,17 @@ export default async function BookingCalendarPage({ params }: PageProps) {
{/* Legend */} -
+
- + Belegt
- + Frei
- + Heute
@@ -217,7 +224,7 @@ export default async function BookingCalendarPage({ params }: PageProps) {
-

+

Buchungen in diesem Monat

{bookings.data.length}

diff --git a/apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx index 653a3a699..1a112fa48 100644 --- a/apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx +++ b/apps/web/app/[locale]/home/[account]/bookings/guests/page.tsx @@ -1,14 +1,13 @@ import { UserCircle, Plus } from 'lucide-react'; +import { createBookingManagementApi } from '@kit/booking-management/api'; 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 { AccountNotFound } from '~/components/account-not-found'; import { CmsPageShell } from '~/components/cms-page-shell'; import { EmptyState } from '~/components/empty-state'; -import { AccountNotFound } from '~/components/account-not-found'; interface PageProps { params: Promise<{ account: string }>; @@ -62,7 +61,7 @@ export default async function GuestsPage({ params }: PageProps) {
- + @@ -72,9 +71,13 @@ export default async function GuestsPage({ params }: PageProps) { {guests.map((guest: Record) => ( - + diff --git a/apps/web/app/[locale]/home/[account]/bookings/new/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/new/page.tsx index 6cb7c4908..653bcd912 100644 --- a/apps/web/app/[locale]/home/[account]/bookings/new/page.tsx +++ b/apps/web/app/[locale]/home/[account]/bookings/new/page.tsx @@ -1,15 +1,22 @@ -import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { createBookingManagementApi } from '@kit/booking-management/api'; import { CreateBookingForm } from '@kit/booking-management/components'; -import { CmsPageShell } from '~/components/cms-page-shell'; -import { AccountNotFound } from '~/components/account-not-found'; +import { getSupabaseServerClient } from '@kit/supabase/server-client'; -interface Props { params: Promise<{ account: string }> } +import { AccountNotFound } from '~/components/account-not-found'; +import { CmsPageShell } from '~/components/cms-page-shell'; + +interface Props { + params: Promise<{ account: string }>; +} export default async function NewBookingPage({ params }: Props) { const { account } = await params; const client = getSupabaseServerClient(); - const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single(); + const { data: acct } = await client + .from('accounts') + .select('id') + .eq('slug', account) + .single(); if (!acct) { return ( @@ -22,13 +29,20 @@ export default async function NewBookingPage({ params }: Props) { const rooms = await api.listRooms(acct.id); return ( - - + ) => ({ - id: String(r.id), roomNumber: String(r.room_number), name: String(r.name ?? ''), pricePerNight: Number(r.price_per_night ?? 0) - }))} + id: String(r.id), + roomNumber: String(r.room_number), + name: String(r.name ?? ''), + pricePerNight: Number(r.price_per_night ?? 0), + }))} /> ); diff --git a/apps/web/app/[locale]/home/[account]/bookings/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/page.tsx index f5582a6e8..ac2381305 100644 --- a/apps/web/app/[locale]/home/[account]/bookings/page.tsx +++ b/apps/web/app/[locale]/home/[account]/bookings/page.tsx @@ -2,18 +2,18 @@ import Link from 'next/link'; import { BedDouble, CalendarCheck, Plus, Euro, Search } from 'lucide-react'; +import { createBookingManagementApi } from '@kit/booking-management/api'; +import { formatDate } from '@kit/shared/dates'; 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 { Input } from '@kit/ui/input'; -import { createBookingManagementApi } from '@kit/booking-management/api'; - +import { AccountNotFound } from '~/components/account-not-found'; import { CmsPageShell } from '~/components/cms-page-shell'; import { EmptyState } from '~/components/empty-state'; import { StatsCard } from '~/components/stats-card'; -import { AccountNotFound } from '~/components/account-not-found'; interface PageProps { params: Promise<{ account: string }>; @@ -42,7 +42,10 @@ const STATUS_LABEL: Record = { no_show: 'Nicht erschienen', }; -export default async function BookingsPage({ params, searchParams }: PageProps) { +export default async function BookingsPage({ + params, + searchParams, +}: PageProps) { const { account } = await params; const search = await searchParams; const client = getSupabaseServerClient(); @@ -148,7 +151,7 @@ export default async function BookingsPage({ params, searchParams }: PageProps) {/* Search */}
- +
Name E-Mail Telefon
- {String(guest.last_name ?? '')}, {String(guest.first_name ?? '')} + {String(guest.last_name ?? '')},{' '} + {String(guest.first_name ?? '')} {String(guest.email ?? '—')} {String(guest.phone ?? '—')}
- + @@ -211,13 +214,19 @@ export default async function BookingsPage({ params, searchParams }: PageProps) {bookingsData.map((booking) => { - const room = booking.room as Record | null; - const guest = booking.guest as Record | null; + const room = booking.room as Record< + string, + string + > | null; + const guest = booking.guest as Record< + string, + string + > | null; return (
Zimmer Gast Anreise
- {booking.check_in - ? new Date( - String(booking.check_in), - ).toLocaleDateString('de-DE') - : '—'} + {formatDate(booking.check_in)} - {booking.check_out - ? new Date( - String(booking.check_out), - ).toLocaleDateString('de-DE') - : '—'} + {formatDate(booking.check_out)} 1 && !searchQuery && (
-

+

Seite {page} von {totalPages} ({total} Einträge)

{page > 1 ? ( - + @@ -293,9 +292,7 @@ export default async function BookingsPage({ params, searchParams }: PageProps) )} {page < totalPages ? ( - + diff --git a/apps/web/app/[locale]/home/[account]/bookings/rooms/page.tsx b/apps/web/app/[locale]/home/[account]/bookings/rooms/page.tsx index ab05ccb88..853cc6f62 100644 --- a/apps/web/app/[locale]/home/[account]/bookings/rooms/page.tsx +++ b/apps/web/app/[locale]/home/[account]/bookings/rooms/page.tsx @@ -1,15 +1,14 @@ import { BedDouble, Plus } from 'lucide-react'; +import { createBookingManagementApi } from '@kit/booking-management/api'; 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 { AccountNotFound } from '~/components/account-not-found'; import { CmsPageShell } from '~/components/cms-page-shell'; import { EmptyState } from '~/components/empty-state'; -import { AccountNotFound } from '~/components/account-not-found'; interface PageProps { params: Promise<{ account: string }>; @@ -63,26 +62,37 @@ export default async function RoomsPage({ params }: PageProps) {
- + - + {rooms.map((room: Record) => ( - + - - + + -
Zimmernr. Name Typ KapazitätPreis/Nacht + Preis/Nacht + Aktiv
{String(room.room_number ?? '—')} {String(room.name ?? '—')} - {String(room.room_type ?? '—')} + + {String(room.name ?? '—')} + + + {String(room.room_type ?? '—')} + + + {String(room.capacity ?? '—')} {String(room.capacity ?? '—')} {room.price_per_night != null ? `${Number(room.price_per_night).toFixed(2)} €` diff --git a/apps/web/app/[locale]/home/[account]/courses/[courseId]/attendance/page.tsx b/apps/web/app/[locale]/home/[account]/courses/[courseId]/attendance/page.tsx index 09e4db878..e0668428b 100644 --- a/apps/web/app/[locale]/home/[account]/courses/[courseId]/attendance/page.tsx +++ b/apps/web/app/[locale]/home/[account]/courses/[courseId]/attendance/page.tsx @@ -1,11 +1,11 @@ import { ClipboardCheck, Calendar } from 'lucide-react'; +import { createCourseManagementApi } from '@kit/course-management/api'; +import { formatDate } from '@kit/shared/dates'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { Badge } from '@kit/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; -import { createCourseManagementApi } from '@kit/course-management/api'; - import { CmsPageShell } from '~/components/cms-page-shell'; import { EmptyState } from '~/components/empty-state'; @@ -14,7 +14,10 @@ interface PageProps { searchParams: Promise>; } -export default async function AttendancePage({ params, searchParams }: PageProps) { +export default async function AttendancePage({ + params, + searchParams, +}: PageProps) { const { account, courseId } = await params; const search = await searchParams; const client = getSupabaseServerClient(); @@ -28,14 +31,21 @@ export default async function AttendancePage({ params, searchParams }: PageProps if (!course) return
Kurs nicht gefunden
; - const selectedSessionId = (search.session as string) ?? (sessions.length > 0 ? String((sessions[0] as Record).id) : null); + const selectedSessionId = + (search.session as string) ?? + (sessions.length > 0 + ? String((sessions[0] as Record).id) + : null); const attendance = selectedSessionId ? await api.getAttendance(selectedSessionId) : []; const attendanceMap = new Map( - attendance.map((a: Record) => [String(a.participant_id), Boolean(a.present)]), + attendance.map((a: Record) => [ + String(a.participant_id), + Boolean(a.present), + ]), ); return ( @@ -70,9 +80,12 @@ export default async function AttendancePage({ params, searchParams }: PageProps key={String(s.id)} href={`/home/${account}/courses/${courseId}/attendance?session=${String(s.id)}`} > - + {s.session_date - ? new Date(String(s.session_date)).toLocaleDateString('de-DE') + ? formatDate(s.session_date as string) : String(s.id)} @@ -92,28 +105,38 @@ export default async function AttendancePage({ params, searchParams }: PageProps {participants.length === 0 ? ( -

+

Keine Teilnehmer in diesem Kurs

) : (
- - - + + + {participants.map((p: Record) => ( - +
TeilnehmerAnwesend
+ Teilnehmer + + Anwesend +
- {String(p.last_name ?? '')}, {String(p.first_name ?? '')} + {String(p.last_name ?? '')},{' '} + {String(p.first_name ?? '')} diff --git a/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx b/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx index ae98008b0..79e20c0d2 100644 --- a/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx +++ b/apps/web/app/[locale]/home/[account]/courses/[courseId]/page.tsx @@ -1,14 +1,21 @@ import Link from 'next/link'; -import { GraduationCap, Users, Calendar, Euro, User, Clock } from 'lucide-react'; +import { + GraduationCap, + Users, + Calendar, + Euro, + User, + Clock, +} from 'lucide-react'; +import { createCourseManagementApi } from '@kit/course-management/api'; +import { formatDate } from '@kit/shared/dates'; 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 { createCourseManagementApi } from '@kit/course-management/api'; - import { CmsPageShell } from '~/components/cms-page-shell'; interface PageProps { @@ -16,13 +23,22 @@ interface PageProps { } const STATUS_LABEL: Record = { - planned: 'Geplant', open: 'Offen', running: 'Laufend', - completed: 'Abgeschlossen', cancelled: 'Abgesagt', + planned: 'Geplant', + open: 'Offen', + running: 'Laufend', + completed: 'Abgeschlossen', + cancelled: 'Abgesagt', }; -const STATUS_VARIANT: Record = { - planned: 'secondary', open: 'default', running: 'info', - completed: 'outline', cancelled: 'destructive', +const STATUS_VARIANT: Record< + string, + 'secondary' | 'default' | 'info' | 'outline' | 'destructive' +> = { + planned: 'secondary', + open: 'default', + running: 'info', + completed: 'outline', + cancelled: 'destructive', }; export default async function CourseDetailPage({ params }: PageProps) { @@ -47,19 +63,21 @@ export default async function CourseDetailPage({ params }: PageProps) {
- +
-

Name

+

Name

{String(c.name)}

- +
-

Status

- +

Status

+ {STATUS_LABEL[String(c.status)] ?? String(c.status)}
@@ -67,31 +85,33 @@ export default async function CourseDetailPage({ params }: PageProps) {
- +
-

Dozent

-

{String(c.instructor_id ?? '—')}

-
-
-
- - - -
-

Beginn – Ende

+

Dozent

- {c.start_date ? new Date(String(c.start_date)).toLocaleDateString('de-DE') : '—'} - {' – '} - {c.end_date ? new Date(String(c.end_date)).toLocaleDateString('de-DE') : '—'} + {String(c.instructor_id ?? '—')}

- +
-

Gebühr

+

Beginn – Ende

+

+ {formatDate(c.start_date as string)} + {' – '} + {formatDate(c.end_date as string)} +

+
+
+
+ + + +
+

Gebühr

{c.fee != null ? `${Number(c.fee).toFixed(2)} €` : '—'}

@@ -100,9 +120,9 @@ export default async function CourseDetailPage({ params }: PageProps) { - +
-

Teilnehmer

+

Teilnehmer

{participants.length} / {String(c.capacity ?? '∞')}

@@ -116,14 +136,16 @@ export default async function CourseDetailPage({ params }: PageProps) { Teilnehmer - +
- + @@ -132,15 +154,36 @@ export default async function CourseDetailPage({ params }: PageProps) { {participants.length === 0 ? ( - - ) : participants.map((p: Record) => ( - - - - - + + - ))} + ) : ( + participants.map((p: Record) => ( + + + + + + + )) + )}
Name E-Mail Status
Keine Teilnehmer
{String(p.last_name ?? '')}, {String(p.first_name ?? '')}{String(p.email ?? '—')}{String(p.status ?? '—')}{p.enrolled_at ? new Date(String(p.enrolled_at)).toLocaleDateString('de-DE') : '—'}
+ Keine Teilnehmer +
+ {String(p.last_name ?? '')},{' '} + {String(p.first_name ?? '')} + {String(p.email ?? '—')} + + {String(p.status ?? '—')} + + + {formatDate(p.enrolled_at as string)} +
@@ -152,14 +195,16 @@ export default async function CourseDetailPage({ params }: PageProps) { Termine - +
- + @@ -168,15 +213,35 @@ export default async function CourseDetailPage({ params }: PageProps) { {sessions.length === 0 ? ( - - ) : sessions.map((s: Record) => ( - - - - - + + - ))} + ) : ( + sessions.map((s: Record) => ( + + + + + + + )) + )}
Datum Beginn Ende
Keine Termine
{s.session_date ? new Date(String(s.session_date)).toLocaleDateString('de-DE') : '—'}{String(s.start_time ?? '—')}{String(s.end_time ?? '—')}{s.cancelled ? Ja : '—'}
+ Keine Termine +
+ {formatDate(s.session_date as string)} + {String(s.start_time ?? '—')}{String(s.end_time ?? '—')} + {s.cancelled ? ( + Ja + ) : ( + '—' + )} +
diff --git a/apps/web/app/[locale]/home/[account]/courses/[courseId]/participants/page.tsx b/apps/web/app/[locale]/home/[account]/courses/[courseId]/participants/page.tsx index 8810bb981..00770f520 100644 --- a/apps/web/app/[locale]/home/[account]/courses/[courseId]/participants/page.tsx +++ b/apps/web/app/[locale]/home/[account]/courses/[courseId]/participants/page.tsx @@ -2,13 +2,13 @@ import Link from 'next/link'; import { Plus, Users } from 'lucide-react'; +import { createCourseManagementApi } from '@kit/course-management/api'; +import { formatDate } from '@kit/shared/dates'; 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 { createCourseManagementApi } from '@kit/course-management/api'; - import { CmsPageShell } from '~/components/cms-page-shell'; import { EmptyState } from '~/components/empty-state'; @@ -16,7 +16,10 @@ interface PageProps { params: Promise<{ account: string; courseId: string }>; } -const STATUS_VARIANT: Record = { +const STATUS_VARIANT: Record< + string, + 'secondary' | 'default' | 'info' | 'outline' | 'destructive' +> = { enrolled: 'default', waitlisted: 'secondary', cancelled: 'destructive', @@ -49,7 +52,8 @@ export default async function ParticipantsPage({ params }: PageProps) {

Teilnehmer

- {String((course as Record).name)} — {participants.length} Teilnehmer + {String((course as Record).name)} —{' '} + {participants.length} Teilnehmer