fix: add missing newlines at the end of JSON files; clean up formatting in page components
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 17m4s
Workflow / ⚫️ Test (push) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-02 11:02:58 +02:00
parent b26e5aaafa
commit c6d564836f
56 changed files with 471 additions and 381 deletions

View File

@@ -1 +1 @@
[]
[]

View File

@@ -54,10 +54,7 @@ async function AuditPage(props: AdminAuditPageProps) {
return (
<PageBody>
<PageHeader
title={t('title')}
description={t('description')}
/>
<PageHeader title={t('title')} description={t('description')} />
<div className="space-y-4">
{/* Filters */}

View File

@@ -75,7 +75,9 @@ export default async function PortalInvitePage({
<Shield className="mx-auto mb-4 h-10 w-10 text-amber-500" />
<h2 className="text-lg font-bold">{t('invite.expiredTitle')}</h2>
<p className="text-muted-foreground mt-2 text-sm">
{t('invite.expiredDesc', { date: formatDate(invitation.expires_at) })}
{t('invite.expiredDesc', {
date: formatDate(invitation.expires_at),
})}
</p>
</CardContent>
</Card>

View File

@@ -179,13 +179,17 @@ export function PortalLinkedAccounts({ slug }: { slug: string }) {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('linkedAccounts.title')}</AlertDialogTitle>
<AlertDialogTitle>
{t('linkedAccounts.title')}
</AlertDialogTitle>
<AlertDialogDescription>
{t('linkedAccounts.disconnectDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('linkedAccounts.cancel')}</AlertDialogCancel>
<AlertDialogCancel>
{t('linkedAccounts.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleUnlink(identity)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"

View File

@@ -3,13 +3,7 @@ import { redirect } from 'next/navigation';
import { createClient } from '@supabase/supabase-js';
import {
UserCircle,
Mail,
MapPin,
Shield,
Link2,
} from 'lucide-react';
import { UserCircle, Mail, MapPin, Shield, Link2 } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { formatDate } from '@kit/shared/dates';

View File

@@ -99,7 +99,12 @@ export default async function BookingDetailPage({ params }: PageProps) {
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild aria-label={t('detail.backToBookings')}>
<Button
variant="ghost"
size="icon"
asChild
aria-label={t('detail.backToBookings')}
>
<Link href={`/home/${account}/bookings`}>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
</Link>

View File

@@ -1,13 +1,13 @@
import Link from 'next/link';
import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
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 { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -139,14 +139,17 @@ export default async function BookingCalendarPage({ params }: PageProps) {
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.backToBookings')}>
<Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.backToBookings')}
>
<Link href={`/home/${account}/bookings`}>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
</Link>
</Button>
<p className="text-muted-foreground">
{t('calendar.subtitle')}
</p>
<p className="text-muted-foreground">{t('calendar.subtitle')}</p>
</div>
</div>
@@ -154,13 +157,23 @@ export default async function BookingCalendarPage({ params }: PageProps) {
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Button variant="ghost" size="icon" disabled aria-label={t('calendar.previousMonth')}>
<Button
variant="ghost"
size="icon"
disabled
aria-label={t('calendar.previousMonth')}
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
</Button>
<CardTitle>
{MONTH_NAMES[month]} {year}
</CardTitle>
<Button variant="ghost" size="icon" disabled aria-label={t('calendar.nextMonth')}>
<Button
variant="ghost"
size="icon"
disabled
aria-label={t('calendar.nextMonth')}
>
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
@@ -232,7 +245,10 @@ export default async function BookingCalendarPage({ params }: PageProps) {
<p className="text-2xl font-bold">{bookings.data.length}</p>
</div>
<Badge variant="outline">
{t('calendar.daysOccupied', { occupied: occupiedDates.size, total: daysInMonth })}
{t('calendar.daysOccupied', {
occupied: occupiedDates.size,
total: daysInMonth,
})}
</Badge>
</div>
</CardContent>

View File

@@ -250,7 +250,10 @@ export default async function BookingsPage({
'secondary'
}
>
{t(STATUS_LABEL_KEYS[String(booking.status)] ?? String(booking.status))}
{t(
STATUS_LABEL_KEYS[String(booking.status)] ??
String(booking.status),
)}
</Badge>
</td>
<td className="p-3 text-right">

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api';
import { CreateCourseForm } from '@kit/course-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -88,8 +88,10 @@ export default async function CourseDetailPage({ params }: PageProps) {
'secondary'
}
>
{t(COURSE_STATUS_LABEL_KEYS[String(courseData.status)] ??
String(courseData.status))}
{t(
COURSE_STATUS_LABEL_KEYS[String(courseData.status)] ??
String(courseData.status),
)}
</Badge>
</div>
</CardContent>

View File

@@ -129,7 +129,12 @@ export default async function CourseCalendarPage({
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.backToCourses')}>
<Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.backToCourses')}
>
<Link href={`/home/${account}/courses`}>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
</Link>
@@ -142,7 +147,12 @@ export default async function CourseCalendarPage({
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.previousMonth')}>
<Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.previousMonth')}
>
<Link
href={`/home/${account}/courses/calendar?month=${
month === 0
@@ -156,7 +166,12 @@ export default async function CourseCalendarPage({
<CardTitle>
{MONTH_NAMES[month]} {year}
</CardTitle>
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.nextMonth')}>
<Button
variant="ghost"
size="icon"
asChild
aria-label={t('calendar.nextMonth')}
>
<Link
href={`/home/${account}/courses/calendar?month=${
month === 11

View File

@@ -196,7 +196,10 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
'secondary'
}
>
{t(COURSE_STATUS_LABEL_KEYS[String(course.status)] ?? String(course.status))}
{t(
COURSE_STATUS_LABEL_KEYS[String(course.status)] ??
String(course.status),
)}
</Badge>
</td>
<td className="p-3 text-right">

View File

@@ -18,7 +18,10 @@ import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EVENT_STATUS_LABEL_KEYS, EVENT_STATUS_VARIANT } from '~/lib/status-badges';
import {
EVENT_STATUS_LABEL_KEYS,
EVENT_STATUS_VARIANT,
} from '~/lib/status-badges';
import { DeleteEventButton } from './delete-event-button';
@@ -64,8 +67,10 @@ export default async function EventDetailPage({ params }: PageProps) {
}
className="mt-1"
>
{t(EVENT_STATUS_LABEL_KEYS[String(eventData.status)] ??
String(eventData.status))}
{t(
EVENT_STATUS_LABEL_KEYS[String(eventData.status)] ??
String(eventData.status),
)}
</Badge>
</div>
<Button>

View File

@@ -21,7 +21,10 @@ 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 { EVENT_STATUS_VARIANT, EVENT_STATUS_LABEL_KEYS } from '~/lib/status-badges';
import {
EVENT_STATUS_VARIANT,
EVENT_STATUS_LABEL_KEYS,
} from '~/lib/status-badges';
interface PageProps {
params: Promise<{ account: string }>;
@@ -178,7 +181,10 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
'secondary'
}
>
{t(EVENT_STATUS_LABEL_KEYS[String(event.status)] ?? String(event.status))}
{t(
EVENT_STATUS_LABEL_KEYS[String(event.status)] ??
String(event.status),
)}
</Badge>
</td>
<td className="p-3 text-right font-medium">

View File

@@ -13,7 +13,10 @@ 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 { EVENT_STATUS_VARIANT, EVENT_STATUS_LABEL_KEYS } from '~/lib/status-badges';
import {
EVENT_STATUS_VARIANT,
EVENT_STATUS_LABEL_KEYS,
} from '~/lib/status-badges';
interface PageProps {
params: Promise<{ account: string }>;
@@ -156,7 +159,10 @@ export default async function EventRegistrationsPage({ params }: PageProps) {
'secondary'
}
>
{t(EVENT_STATUS_LABEL_KEYS[event.status] ?? event.status)}
{t(
EVENT_STATUS_LABEL_KEYS[event.status] ??
event.status,
)}
</Badge>
</td>
<td className="p-3 text-right">

View File

@@ -1,13 +1,13 @@
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFinanceApi } from '@kit/finance/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 { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -66,7 +66,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>
{t('invoices.invoiceLabel', { number: String(invoice.invoice_number ?? '') })}
{t('invoices.invoiceLabel', {
number: String(invoice.invoice_number ?? ''),
})}
</CardTitle>
<Badge variant={INVOICE_STATUS_VARIANT[status] ?? 'secondary'}>
{t(INVOICE_STATUS_LABEL_KEYS[status] ?? status)}
@@ -125,7 +127,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
{/* Line Items */}
<Card>
<CardHeader>
<CardTitle>{t('invoiceForm.lineItems')} ({items.length})</CardTitle>
<CardTitle>
{t('invoiceForm.lineItems')} ({items.length})
</CardTitle>
</CardHeader>
<CardContent>
{items.length === 0 ? (
@@ -140,11 +144,15 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
<th scope="col" className="p-3 text-left font-medium">
{t('invoiceForm.itemDescription')}
</th>
<th scope="col" className="p-3 text-right font-medium">{t('invoiceForm.quantity')}</th>
<th scope="col" className="p-3 text-right font-medium">
{t('invoiceForm.quantity')}
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('invoices.unitPriceCol')}
</th>
<th scope="col" className="p-3 text-right font-medium">{t('invoices.totalCol')}</th>
<th scope="col" className="p-3 text-right font-medium">
{t('invoices.totalCol')}
</th>
</tr>
</thead>
<tbody>
@@ -183,7 +191,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
</tr>
<tr>
<td colSpan={3} className="p-3 text-right font-medium">
{t('invoiceForm.tax', { rate: Number(invoice.tax_rate ?? 19) })}
{t('invoiceForm.tax', {
rate: Number(invoice.tax_rate ?? 19),
})}
</td>
<td className="p-3 text-right">
{formatCurrency(invoice.tax_amount ?? 0)}

View File

@@ -216,8 +216,10 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
'secondary'
}
>
{t(BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
String(batch.status))}
{t(
BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
String(batch.status),
)}
</Badge>
</td>
<td className="p-3">
@@ -318,7 +320,11 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
'secondary'
}
>
{t(INVOICE_STATUS_LABEL_KEYS[String(invoice.status)] ?? String(invoice.status))}
{t(
INVOICE_STATUS_LABEL_KEYS[
String(invoice.status)
] ?? String(invoice.status),
)}
</Badge>
</td>
</tr>
@@ -347,7 +353,12 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
</Link>
</Button>
) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.previous')}>
<Button
variant="outline"
size="sm"
disabled
aria-label={t('common.previous')}
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
</Button>
)}
@@ -366,7 +377,12 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
</Link>
</Button>
) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.next')}>
<Button
variant="outline"
size="sm"
disabled
aria-label={t('common.next')}
>
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Button>
)}

View File

@@ -1,13 +1,13 @@
import Link from 'next/link';
import { Euro, CreditCard, TrendingUp, ArrowRight } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFinanceApi } from '@kit/finance/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 { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -84,9 +84,7 @@ export default async function PaymentsPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6">
{/* Header */}
<div>
<p className="text-muted-foreground">
{t('payments.subtitle')}
</p>
<p className="text-muted-foreground">{t('payments.subtitle')}</p>
</div>
{/* Stats */}
@@ -121,7 +119,9 @@ export default async function PaymentsPage({ params }: PageProps) {
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">{t('payments.openInvoices')}</CardTitle>
<CardTitle className="text-base">
{t('payments.openInvoices')}
</CardTitle>
<Badge
variant={openInvoices.length > 0 ? 'default' : 'secondary'}
>
@@ -131,7 +131,10 @@ export default async function PaymentsPage({ params }: PageProps) {
<CardContent>
<p className="text-muted-foreground mb-4 text-sm">
{openInvoices.length > 0
? t('payments.invoicesOpenSummary', { count: openInvoices.length, total: formatCurrency(openTotal) })
? t('payments.invoicesOpenSummary', {
count: openInvoices.length,
total: formatCurrency(openTotal),
})
: t('payments.noOpenInvoices')}
</p>
<Button variant="outline" size="sm" asChild>
@@ -145,7 +148,9 @@ export default async function PaymentsPage({ params }: PageProps) {
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">{t('payments.sepaBatches')}</CardTitle>
<CardTitle className="text-base">
{t('payments.sepaBatches')}
</CardTitle>
<Badge variant={batches.length > 0 ? 'default' : 'secondary'}>
{batches.length}
</Badge>
@@ -153,7 +158,10 @@ export default async function PaymentsPage({ params }: PageProps) {
<CardContent>
<p className="text-muted-foreground mb-4 text-sm">
{batches.length > 0
? t('payments.batchSummary', { count: batches.length, total: formatCurrency(sepaTotal) })
? t('payments.batchSummary', {
count: batches.length,
total: formatCurrency(sepaTotal),
})
: t('payments.noBatchesFound')}
</p>
<Button variant="outline" size="sm" asChild>

View File

@@ -1,6 +1,7 @@
import Link from 'next/link';
import { ArrowLeft, Download } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFinanceApi } from '@kit/finance/api';
import { formatDate } from '@kit/shared/dates';
@@ -8,7 +9,6 @@ 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 { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -76,7 +76,9 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
{/* Summary Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>{String(batch.description ?? t('sepa.batchFallbackName'))}</CardTitle>
<CardTitle>
{String(batch.description ?? t('sepa.batchFallbackName'))}
</CardTitle>
<Badge variant={BATCH_STATUS_VARIANT[status] ?? 'secondary'}>
{t(BATCH_STATUS_LABEL_KEYS[status] ?? status)}
</Badge>
@@ -133,7 +135,9 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
{/* Items Table */}
<Card>
<CardHeader>
<CardTitle>{t('sepa.itemCount')} ({items.length})</CardTitle>
<CardTitle>
{t('sepa.itemCount')} ({items.length})
</CardTitle>
</CardHeader>
<CardContent>
{items.length === 0 ? (
@@ -145,10 +149,18 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th scope="col" className="p-3 text-left font-medium">Name</th>
<th scope="col" className="p-3 text-left font-medium">IBAN</th>
<th scope="col" className="p-3 text-right font-medium">{t('common.amount')}</th>
<th scope="col" className="p-3 text-left font-medium">{t('common.status')}</th>
<th scope="col" className="p-3 text-left font-medium">
Name
</th>
<th scope="col" className="p-3 text-left font-medium">
IBAN
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('common.amount')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('common.status')}
</th>
</tr>
</thead>
<tbody>
@@ -176,7 +188,11 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
ITEM_STATUS_VARIANT[itemStatus] ?? 'secondary'
}
>
{t(`sepaItemStatus.${itemStatus}` as Parameters<typeof t>[0]) ?? itemStatus}
{t(
`sepaItemStatus.${itemStatus}` as Parameters<
typeof t
>[0],
) ?? itemStatus}
</Badge>
</td>
</tr>

View File

@@ -13,7 +13,10 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
import { BATCH_STATUS_VARIANT, BATCH_STATUS_LABEL_KEYS } from '~/lib/status-badges';
import {
BATCH_STATUS_VARIANT,
BATCH_STATUS_LABEL_KEYS,
} from '~/lib/status-badges';
interface PageProps {
params: Promise<{ account: string }>;
@@ -114,7 +117,10 @@ export default async function SepaPage({ params }: PageProps) {
'secondary'
}
>
{t(BATCH_STATUS_LABEL_KEYS[String(batch.status)] ?? String(batch.status))}
{t(
BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
String(batch.status),
)}
</Badge>
</td>
<td className="p-3">

View File

@@ -4,7 +4,24 @@ import { use } from 'react';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { Fish, FileSignature, Building2 } from 'lucide-react';
import {
Fish,
Waves,
Anchor,
BookOpen,
ShieldCheck,
Trophy,
FileSignature,
ScrollText,
ListChecks,
BookMarked,
Building2,
Network,
SearchCheck,
Share2,
PieChart,
LayoutTemplate,
} from 'lucide-react';
import * as z from 'zod';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -61,65 +78,132 @@ const getAccountFeatures = cache(async (accountSlug: string) => {
return (settings?.features as Record<string, boolean>) ?? {};
});
const iconClasses = 'w-4';
/**
* Inject per-account feature routes (e.g. Fischerei) into the parsed
* navigation config. The entry is inserted right after "Veranstaltungen".
* Inject per-account feature module route groups (Fischerei, Meetings, Verband)
* into the navigation config. These are added as separate collapsible sections
* before the Administration section (last group).
*
* Only modules enabled in the account's settings are shown.
*/
function injectAccountFeatureRoutes(
config: z.output<typeof NavigationConfigSchema>,
account: string,
features: Record<string, boolean>,
): z.output<typeof NavigationConfigSchema> {
if (!features.fischerei && !features.meetings && !features.verband)
return config;
const featureEntries: Array<{
label: string;
path: string;
Icon: React.ReactNode;
}> = [];
const featureGroups: z.output<typeof NavigationConfigSchema>['routes'] = [];
if (features.fischerei) {
featureEntries.push({
label: 'common.routes.fischerei',
path: `/home/${account}/fischerei`,
Icon: <Fish className="w-4" />,
featureGroups.push({
label: 'common:routes.fisheriesManagement',
collapsible: true,
children: [
{
label: 'common:routes.fisheriesOverview',
path: `/home/${account}/fischerei`,
Icon: <Fish className={iconClasses} />,
},
{
label: 'common:routes.fisheriesWaters',
path: `/home/${account}/fischerei/waters`,
Icon: <Waves className={iconClasses} />,
},
{
label: 'common:routes.fisheriesLeases',
path: `/home/${account}/fischerei/leases`,
Icon: <Anchor className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCatchBooks',
path: `/home/${account}/fischerei/catch-books`,
Icon: <BookOpen className={iconClasses} />,
},
{
label: 'common:routes.fisheriesPermits',
path: `/home/${account}/fischerei/permits`,
Icon: <ShieldCheck className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCompetitions',
path: `/home/${account}/fischerei/competitions`,
Icon: <Trophy className={iconClasses} />,
},
],
});
}
if (features.meetings) {
featureEntries.push({
label: 'common.routes.meetings',
path: `/home/${account}/meetings`,
Icon: <FileSignature className="w-4" />,
featureGroups.push({
label: 'common:routes.meetingProtocols',
collapsible: true,
children: [
{
label: 'common:routes.meetingsOverview',
path: `/home/${account}/meetings`,
Icon: <BookMarked className={iconClasses} />,
},
{
label: 'common:routes.meetingsProtocols',
path: `/home/${account}/meetings/protocols`,
Icon: <ScrollText className={iconClasses} />,
},
{
label: 'common:routes.meetingsTasks',
path: `/home/${account}/meetings/tasks`,
Icon: <ListChecks className={iconClasses} />,
},
],
});
}
if (features.verband) {
featureEntries.push({
label: 'common.routes.verband',
path: `/home/${account}/verband`,
Icon: <Building2 className="w-4" />,
featureGroups.push({
label: 'common:routes.associationManagement',
collapsible: true,
children: [
{
label: 'common:routes.associationOverview',
path: `/home/${account}/verband`,
Icon: <Building2 className={iconClasses} />,
},
{
label: 'common:routes.associationHierarchy',
path: `/home/${account}/verband/hierarchy`,
Icon: <Network className={iconClasses} />,
},
{
label: 'common:routes.associationMemberSearch',
path: `/home/${account}/verband/members`,
Icon: <SearchCheck className={iconClasses} />,
},
{
label: 'common:routes.associationEvents',
path: `/home/${account}/verband/events`,
Icon: <Share2 className={iconClasses} />,
},
{
label: 'common:routes.associationReporting',
path: `/home/${account}/verband/reporting`,
Icon: <PieChart className={iconClasses} />,
},
{
label: 'common:routes.associationTemplates',
path: `/home/${account}/verband/templates`,
Icon: <LayoutTemplate className={iconClasses} />,
},
],
});
}
return {
...config,
routes: config.routes.map((group) => {
if (!('children' in group)) return group;
if (featureGroups.length === 0) return config;
const eventsIndex = group.children.findIndex(
(child) => child.label === 'common.routes.events',
);
// Insert before the last group (Administration)
const routes = [...config.routes];
const adminIndex = routes.length - 1;
routes.splice(adminIndex, 0, ...featureGroups);
if (eventsIndex === -1) return group;
const newChildren = [...group.children];
newChildren.splice(eventsIndex + 1, 0, ...featureEntries);
return { ...group, children: newChildren };
}),
};
return { ...config, routes };
}
async function SidebarLayout({

View File

@@ -3,16 +3,16 @@ import { PageBody } from '@kit/ui/page';
export default function AccountLoading() {
return (
<PageBody>
<div className="flex flex-col gap-6 animate-pulse">
<div className="h-8 w-48 rounded-md bg-muted" />
<div className="flex animate-pulse flex-col gap-6">
<div className="bg-muted h-8 w-48 rounded-md" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-24 rounded-lg bg-muted" />
<div key={i} className="bg-muted h-24 rounded-lg" />
))}
</div>
<div className="space-y-2">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-12 w-full rounded bg-muted" />
<div key={i} className="bg-muted h-12 w-full rounded" />
))}
</div>
</div>

View File

@@ -86,7 +86,9 @@ export default async function ProtocolDetailPage({ params }: PageProps) {
{MEETING_TYPE_LABELS[protocol.status] ?? protocol.status}
</Badge>
{protocol.status === 'final' ? (
<Badge variant="default">{t('pages.statusPublished')}</Badge>
<Badge variant="default">
{t('pages.statusPublished')}
</Badge>
) : (
<Badge variant="outline">{t('pages.statusDraft')}</Badge>
)}

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api';
import { EditMemberForm } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createModuleBuilderApi } from '@kit/module-builder/api';
import { ModuleForm } from '@kit/module-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -1,6 +1,7 @@
import { getTranslations } from 'next-intl/server';
import { createModuleBuilderApi } from '@kit/module-builder/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -1,7 +1,8 @@
import { getTranslations } from 'next-intl/server';
import { createModuleBuilderApi } from '@kit/module-builder/api';
import { ModuleForm } from '@kit/module-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -171,7 +171,11 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
'secondary'
}
>
{t(NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS[rStatus] ?? rStatus)}
{t(
NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS[
rStatus
] ?? rStatus,
)}
</Badge>
</td>
</tr>

View File

@@ -204,7 +204,10 @@ export default async function NewsletterPage({
'secondary'
}
>
{t(NEWSLETTER_STATUS_LABEL_KEYS[String(nl.status)] ?? String(nl.status))}
{t(
NEWSLETTER_STATUS_LABEL_KEYS[String(nl.status)] ??
String(nl.status),
)}
</Badge>
</td>
<td className="p-3 text-right">
@@ -241,7 +244,12 @@ export default async function NewsletterPage({
</Link>
</Button>
) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.previous')}>
<Button
variant="outline"
size="sm"
disabled
aria-label={t('common.previous')}
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
</Button>
)}
@@ -256,11 +264,19 @@ export default async function NewsletterPage({
href={`/home/${account}/newsletter${buildQuery(queryBase, { page: safePage + 1 })}`}
aria-label={t('common.next')}
>
<ChevronRight className="h-4 w-4" aria-hidden="true" />
<ChevronRight
className="h-4 w-4"
aria-hidden="true"
/>
</Link>
</Button>
) : (
<Button variant="outline" size="sm" disabled aria-label={t('common.next')}>
<Button
variant="outline"
size="sm"
disabled
aria-label={t('common.next')}
>
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</Button>
)}

View File

@@ -1,6 +1,7 @@
import { getTranslations } from 'next-intl/server';
import { CreatePageForm } from '@kit/site-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -166,10 +166,7 @@ export default async function SiteBuilderDashboard({ params }: Props) {
</thead>
<tbody>
{(pages as SitePage[]).map((page) => (
<tr
key={page.id}
className="hover:bg-muted/30 border-b"
>
<tr key={page.id} className="hover:bg-muted/30 border-b">
<td className="p-3 font-medium">{page.title}</td>
<td className="text-muted-foreground p-3 font-mono text-xs">
/{page.slug}

View File

@@ -1,6 +1,7 @@
import { getTranslations } from 'next-intl/server';
import { CreatePostForm } from '@kit/site-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';

View File

@@ -77,10 +77,7 @@ export default async function PostsManagerPage({ params }: Props) {
</thead>
<tbody>
{(posts as SitePost[]).map((post) => (
<tr
key={post.id}
className="hover:bg-muted/30 border-b"
>
<tr key={post.id} className="hover:bg-muted/30 border-b">
<td className="p-3 font-medium">{post.title}</td>
<td className="p-3">
<Badge

View File

@@ -5,7 +5,6 @@ import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { toast } from '@kit/ui/sonner';
import {
AlertDialog,
@@ -19,6 +18,7 @@ import {
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import { toast } from '@kit/ui/sonner';
interface PublishToggleButtonProps {
pageId: string;
@@ -38,11 +38,14 @@ export function PublishToggleButton({
const handleToggle = () => {
startTransition(async () => {
try {
const response = await fetch(`/api/site-builder/pages/${pageId}/publish`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId, isPublished: !isPublished }),
});
const response = await fetch(
`/api/site-builder/pages/${pageId}/publish`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId, isPublished: !isPublished }),
},
);
if (!response.ok) {
toast.error(t('pages.toggleError'));

View File

@@ -230,9 +230,7 @@ export default function SettingsContent({
return (
<div className="space-y-6">
<div>
<p className="text-muted-foreground">
{t('settings.subtitle')}
</p>
<p className="text-muted-foreground">{t('settings.subtitle')}</p>
</div>
<SettingsSection

View File

@@ -19,7 +19,8 @@ export async function AccountNotFound({
const t = await getTranslations('common');
const resolvedTitle = title ?? t('accountNotFoundCard.title');
const resolvedDescription = description ?? t('accountNotFoundCard.description');
const resolvedDescription =
description ?? t('accountNotFoundCard.description');
const resolvedButtonLabel = buttonLabel ?? t('accountNotFoundCard.action');
return (

View File

@@ -27,7 +27,9 @@ export function CmsPageShell({
<TeamAccountLayoutPageHeader
account={account}
title={title}
description={description !== undefined ? description : <AppBreadcrumbs />}
description={
description !== undefined ? description : <AppBreadcrumbs />
}
/>
<PageBody>{children}</PageBody>

View File

@@ -8,7 +8,6 @@ import {
UserPlus,
IdCard,
ClipboardList,
KeyRound,
// Courses
GraduationCap,
CalendarDays,
@@ -41,24 +40,6 @@ import {
PanelTop,
Newspaper,
Palette,
// Fisheries
Fish,
Waves,
Anchor,
BookOpen,
ShieldCheck,
Trophy,
// Meetings
BookMarked,
ListChecks,
ScrollText,
// Association (Verband)
Building2,
Network,
SearchCheck,
Share2,
PieChart,
LayoutTemplate,
// Modules
Database,
} from 'lucide-react';
@@ -396,176 +377,51 @@ const getRoutes = (account: string) => {
});
}
// ── Custom Modules ──
if (featureFlagsConfig.enableModuleBuilder) {
routes.push({
label: 'common:routes.customModules',
collapsible: true,
collapsed: true,
children: [
{
label: 'common:routes.moduleList',
path: createPath(pathsConfig.app.accountModules, account),
Icon: <Database className={iconClasses} />,
},
],
});
}
// ── Fisheries ──
if (featureFlagsConfig.enableFischerei) {
routes.push({
label: 'common:routes.fisheriesManagement',
collapsible: true,
children: [
{
label: 'common:routes.fisheriesOverview',
path: createPath(pathsConfig.app.accountFischerei, account),
Icon: <Fish className={iconClasses} />,
},
{
label: 'common:routes.fisheriesWaters',
path: createPath(
pathsConfig.app.accountFischerei + '/waters',
account,
),
Icon: <Waves className={iconClasses} />,
},
{
label: 'common:routes.fisheriesLeases',
path: createPath(
pathsConfig.app.accountFischerei + '/leases',
account,
),
Icon: <Anchor className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCatchBooks',
path: createPath(
pathsConfig.app.accountFischerei + '/catch-books',
account,
),
Icon: <BookOpen className={iconClasses} />,
},
{
label: 'common:routes.fisheriesPermits',
path: createPath(
pathsConfig.app.accountFischerei + '/permits',
account,
),
Icon: <ShieldCheck className={iconClasses} />,
},
{
label: 'common:routes.fisheriesCompetitions',
path: createPath(
pathsConfig.app.accountFischerei + '/competitions',
account,
),
Icon: <Trophy className={iconClasses} />,
},
],
});
}
// ── Meeting Protocols ──
if (featureFlagsConfig.enableMeetingProtocols) {
routes.push({
label: 'common:routes.meetingProtocols',
collapsible: true,
children: [
{
label: 'common:routes.meetingsOverview',
path: createPath(pathsConfig.app.accountMeetings, account),
Icon: <BookMarked className={iconClasses} />,
},
{
label: 'common:routes.meetingsProtocols',
path: createPath(
pathsConfig.app.accountMeetings + '/protocols',
account,
),
Icon: <ScrollText className={iconClasses} />,
},
{
label: 'common:routes.meetingsTasks',
path: createPath(pathsConfig.app.accountMeetings + '/tasks', account),
Icon: <ListChecks className={iconClasses} />,
},
],
});
}
// ── Association Management (Verband) ──
if (featureFlagsConfig.enableVerbandsverwaltung) {
routes.push({
label: 'common:routes.associationManagement',
collapsible: true,
children: [
{
label: 'common:routes.associationOverview',
path: createPath(pathsConfig.app.accountVerband, account),
Icon: <Building2 className={iconClasses} />,
},
{
label: 'common:routes.associationHierarchy',
path: createPath(
pathsConfig.app.accountVerband + '/hierarchy',
account,
),
Icon: <Network className={iconClasses} />,
},
{
label: 'common:routes.associationMemberSearch',
path: createPath(
pathsConfig.app.accountVerband + '/members',
account,
),
Icon: <SearchCheck className={iconClasses} />,
},
{
label: 'common:routes.associationEvents',
path: createPath(pathsConfig.app.accountVerband + '/events', account),
Icon: <Share2 className={iconClasses} />,
},
{
label: 'common:routes.associationReporting',
path: createPath(
pathsConfig.app.accountVerband + '/reporting',
account,
),
Icon: <PieChart className={iconClasses} />,
},
{
label: 'common:routes.associationTemplates',
path: createPath(
pathsConfig.app.accountVerband + '/templates',
account,
),
Icon: <LayoutTemplate className={iconClasses} />,
},
],
});
}
// Note: Fischerei, Meetings, and Verband sections are injected at runtime
// via injectAccountFeatureRoutes() in the layout, based on per-account
// settings (account_settings.features). They are NOT added here to avoid
// duplicate entries when both the global feature flag and per-account
// setting are enabled.
// ── Administration ──
routes.push({
label: 'common:routes.administration',
collapsible: false,
children: [
{
const adminChildren: Array<
| {
label: string;
path: string;
Icon: React.ReactNode;
}
| undefined
> = [
{
label: 'common:routes.accountSettings',
path: createPath(pathsConfig.app.accountSettings, account),
Icon: <Settings className={iconClasses} />,
},
featureFlagsConfig.enableTeamAccountBilling
? {
label: 'common:routes.billing',
path: createPath(pathsConfig.app.accountBilling, account),
Icon: <CreditCard className={iconClasses} />,
}
: undefined,
],
});
];
if (featureFlagsConfig.enableModuleBuilder) {
adminChildren.push({
label: 'common:routes.moduleList',
path: createPath(pathsConfig.app.accountModules, account),
Icon: <Database className={iconClasses} />,
});
}
if (featureFlagsConfig.enableTeamAccountBilling) {
adminChildren.push({
label: 'common:routes.billing',
path: createPath(pathsConfig.app.accountBilling, account),
Icon: <CreditCard className={iconClasses} />,
});
}
routes.push({
label: 'common:routes.administration',
collapsible: false,
children: adminChildren,
});
}
return routes;
};

View File

@@ -141,4 +141,4 @@
"cancel": "Abbrechen",
"cancelling": "Wird storniert..."
}
}
}

View File

@@ -812,4 +812,4 @@
"formatExcel": "Excel"
}
}
}
}

View File

@@ -220,4 +220,4 @@
"description": "Das angeforderte Konto existiert nicht oder Sie haben keine Berechtigung darauf zuzugreifen.",
"action": "Zum Dashboard"
}
}
}

View File

@@ -150,15 +150,7 @@
"overview": "Kurstermine im Überblick",
"activeCourses": "Aktive Kurse ({count})",
"noActiveCourses": "Keine aktiven Kurse in diesem Monat.",
"weekdays": [
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa",
"So"
],
"weekdays": ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
"months": [
"Januar",
"Februar",
@@ -210,4 +202,4 @@
"addressPlaceholder": "Musterstr. 1, 12345 Musterstadt",
"roomPlaceholder": "z. B. Raum 101"
}
}
}

View File

@@ -84,4 +84,4 @@
"committee": "Ausschusssitzung",
"other": "Sonstige"
}
}
}

View File

@@ -200,4 +200,4 @@
"bic": "BIC",
"accountHolder": "Kontoinhaber"
}
}
}

View File

@@ -80,4 +80,4 @@
"previous": "Zurück",
"next": "Weiter"
}
}
}

View File

@@ -141,4 +141,4 @@
"cancel": "Dismiss",
"cancelling": "Cancelling..."
}
}
}

View File

@@ -338,4 +338,4 @@
}
}
}
}
}

View File

@@ -221,4 +221,4 @@
"description": "The requested account does not exist or you do not have permission to access it.",
"action": "Go to Dashboard"
}
}
}

View File

@@ -150,15 +150,7 @@
"overview": "Overview of course dates",
"activeCourses": "Active Courses ({count})",
"noActiveCourses": "No active courses this month.",
"weekdays": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
],
"weekdays": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"months": [
"January",
"February",
@@ -210,4 +202,4 @@
"addressPlaceholder": "123 Main St, Springfield",
"roomPlaceholder": "e.g. Room 101"
}
}
}

View File

@@ -84,4 +84,4 @@
"committee": "Committee Meeting",
"other": "Other"
}
}
}

View File

@@ -200,4 +200,4 @@
"bic": "BIC",
"accountHolder": "Account Holder"
}
}
}

View File

@@ -80,4 +80,4 @@
"previous": "Previous",
"next": "Next"
}
}
}

View File

@@ -223,7 +223,8 @@ export const COURSE_STATUS_LABEL = COURSE_STATUS_LABEL_KEYS;
/** @deprecated Use APPLICATION_STATUS_LABEL_KEYS + t() */
export const APPLICATION_STATUS_LABEL = APPLICATION_STATUS_LABEL_KEYS;
/** @deprecated Use NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS + t() */
export const NEWSLETTER_RECIPIENT_STATUS_LABEL = NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS;
export const NEWSLETTER_RECIPIENT_STATUS_LABEL =
NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS;
/** @deprecated Use BOOKING_STATUS_LABEL_KEYS + t() */
export const BOOKING_STATUS_LABEL = BOOKING_STATUS_LABEL_KEYS;
/** @deprecated Use MODULE_STATUS_LABEL_KEYS + t() */