fix: add missing newlines at the end of JSON files; clean up formatting in page components
This commit is contained in:
@@ -1 +1 @@
|
||||
[]
|
||||
[]
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -141,4 +141,4 @@
|
||||
"cancel": "Abbrechen",
|
||||
"cancelling": "Wird storniert..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,4 +812,4 @@
|
||||
"formatExcel": "Excel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,4 +220,4 @@
|
||||
"description": "Das angeforderte Konto existiert nicht oder Sie haben keine Berechtigung darauf zuzugreifen.",
|
||||
"action": "Zum Dashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,4 +84,4 @@
|
||||
"committee": "Ausschusssitzung",
|
||||
"other": "Sonstige"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,4 +200,4 @@
|
||||
"bic": "BIC",
|
||||
"accountHolder": "Kontoinhaber"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +80,4 @@
|
||||
"previous": "Zurück",
|
||||
"next": "Weiter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,4 +141,4 @@
|
||||
"cancel": "Dismiss",
|
||||
"cancelling": "Cancelling..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,4 +338,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,4 +221,4 @@
|
||||
"description": "The requested account does not exist or you do not have permission to access it.",
|
||||
"action": "Go to Dashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,4 +84,4 @@
|
||||
"committee": "Committee Meeting",
|
||||
"other": "Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,4 +200,4 @@
|
||||
"bic": "BIC",
|
||||
"accountHolder": "Account Holder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +80,4 @@
|
||||
"previous": "Previous",
|
||||
"next": "Next"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() */
|
||||
|
||||
@@ -40,7 +40,7 @@ const RouteGroup = z.object({
|
||||
});
|
||||
|
||||
export const NavigationConfigSchema = z.object({
|
||||
sidebarCollapsed: z.stringbool().optional().default(false),
|
||||
sidebarCollapsed: z.coerce.boolean().optional().default(false),
|
||||
sidebarCollapsedStyle: z.enum(['icon', 'offcanvas', 'none']).default('icon'),
|
||||
routes: z.array(z.union([RouteGroup, Divider])),
|
||||
style: z.enum(['sidebar', 'header', 'custom']).default('sidebar'),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import * as z from 'zod';
|
||||
|
||||
@@ -68,7 +68,7 @@ function MaybeCollapsible({
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible defaultOpen={defaultOpen} className={'group/collapsible'}>
|
||||
<Collapsible defaultOpen={defaultOpen} className="group/collapsible">
|
||||
{children}
|
||||
</Collapsible>
|
||||
);
|
||||
@@ -129,23 +129,23 @@ function SidebarNavigationRouteGroupLabel({
|
||||
}) {
|
||||
const className = cn({ hidden: !open });
|
||||
|
||||
return (
|
||||
<If
|
||||
condition={collapsible}
|
||||
fallback={
|
||||
<SidebarGroupLabel className={className}>
|
||||
<Trans i18nKey={label} defaults={label} />
|
||||
</SidebarGroupLabel>
|
||||
}
|
||||
>
|
||||
if (!collapsible) {
|
||||
return (
|
||||
<SidebarGroupLabel className={className}>
|
||||
<CollapsibleTrigger>
|
||||
<Trans i18nKey={label} defaults={label} />
|
||||
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<Trans i18nKey={label} defaults={label} />
|
||||
</SidebarGroupLabel>
|
||||
</If>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarGroupLabel className={cn(className, 'cursor-pointer')}>
|
||||
<CollapsibleTrigger className="flex w-full items-center gap-1">
|
||||
<ChevronRight className="size-3.5 shrink-0 transition-transform duration-200 data-[panel-open]:rotate-90" />
|
||||
<span className="truncate">
|
||||
<Trans i18nKey={label} defaults={label} />
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,8 +165,8 @@ function SidebarNavigationSubItems({
|
||||
{(items) =>
|
||||
items.length > 0 && (
|
||||
<SidebarMenuSub
|
||||
className={cn({
|
||||
'mx-0 px-1.5': !open,
|
||||
className={cn('border-sidebar-border ml-3.5 border-l pl-2', {
|
||||
'mx-0 border-l-0 px-1.5': !open,
|
||||
})}
|
||||
>
|
||||
{items.map((child) => {
|
||||
@@ -241,7 +241,7 @@ function SidebarNavigationRouteChildItem({
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'transition-width w-auto transition-opacity duration-500',
|
||||
'transition-width w-auto transition-opacity duration-300',
|
||||
{
|
||||
'w-0 opacity-0': !open,
|
||||
},
|
||||
@@ -250,9 +250,9 @@ function SidebarNavigationRouteChildItem({
|
||||
<Trans i18nKey={child.label} defaults={child.label} />
|
||||
</span>
|
||||
|
||||
<ChevronDown
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'ml-auto size-4 transition-transform group-data-[state=open]/collapsible:rotate-180',
|
||||
'ml-auto size-4 shrink-0 transition-transform duration-200 data-[panel-open]:rotate-90',
|
||||
{
|
||||
'hidden size-0': !open,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
@@ -12,9 +14,31 @@ function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
|
||||
/**
|
||||
* CollapsibleContent (Panel) with smooth height animation.
|
||||
*
|
||||
* Base UI sets `--collapsible-panel-height` automatically and provides
|
||||
* `data-open`, `data-closed`, `data-starting-style`, `data-ending-style`
|
||||
* for CSS-driven animations.
|
||||
*/
|
||||
function CollapsibleContent({
|
||||
className,
|
||||
...props
|
||||
}: CollapsiblePrimitive.Panel.Props) {
|
||||
return (
|
||||
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
|
||||
<CollapsiblePrimitive.Panel
|
||||
data-slot="collapsible-content"
|
||||
className={cn(
|
||||
[
|
||||
'h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out',
|
||||
'data-[closed]:h-0',
|
||||
'data-[starting-style]:h-0',
|
||||
'data-[ending-style]:h-0',
|
||||
],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user