fix: add missing newlines at the end of JSON files; clean up formatting in page components
This commit is contained in:
@@ -54,10 +54,7 @@ async function AuditPage(props: AdminAuditPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<PageHeader
|
<PageHeader title={t('title')} description={t('description')} />
|
||||||
title={t('title')}
|
|
||||||
description={t('description')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ export default async function PortalInvitePage({
|
|||||||
<Shield className="mx-auto mb-4 h-10 w-10 text-amber-500" />
|
<Shield className="mx-auto mb-4 h-10 w-10 text-amber-500" />
|
||||||
<h2 className="text-lg font-bold">{t('invite.expiredTitle')}</h2>
|
<h2 className="text-lg font-bold">{t('invite.expiredTitle')}</h2>
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<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>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -179,13 +179,17 @@ export function PortalLinkedAccounts({ slug }: { slug: string }) {
|
|||||||
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{t('linkedAccounts.title')}</AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
|
{t('linkedAccounts.title')}
|
||||||
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{t('linkedAccounts.disconnectDesc')}
|
{t('linkedAccounts.disconnectDesc')}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>{t('linkedAccounts.cancel')}</AlertDialogCancel>
|
<AlertDialogCancel>
|
||||||
|
{t('linkedAccounts.cancel')}
|
||||||
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={() => handleUnlink(identity)}
|
onClick={() => handleUnlink(identity)}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
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 { createClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import {
|
import { UserCircle, Mail, MapPin, Shield, Link2 } from 'lucide-react';
|
||||||
UserCircle,
|
|
||||||
Mail,
|
|
||||||
MapPin,
|
|
||||||
Shield,
|
|
||||||
Link2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { formatDate } from '@kit/shared/dates';
|
import { formatDate } from '@kit/shared/dates';
|
||||||
|
|||||||
@@ -99,7 +99,12 @@ export default async function BookingDetailPage({ params }: PageProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<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`}>
|
<Link href={`/home/${account}/bookings`}>
|
||||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { createBookingManagementApi } from '@kit/booking-management/api';
|
import { createBookingManagementApi } from '@kit/booking-management/api';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
@@ -139,14 +139,17 @@ export default async function BookingCalendarPage({ params }: PageProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<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`}>
|
<Link href={`/home/${account}/bookings`}>
|
||||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">{t('calendar.subtitle')}</p>
|
||||||
{t('calendar.subtitle')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,13 +157,23 @@ export default async function BookingCalendarPage({ params }: PageProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<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" />
|
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
{MONTH_NAMES[month]} {year}
|
{MONTH_NAMES[month]} {year}
|
||||||
</CardTitle>
|
</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" />
|
<ChevronRight className="h-4 w-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,7 +245,10 @@ export default async function BookingCalendarPage({ params }: PageProps) {
|
|||||||
<p className="text-2xl font-bold">{bookings.data.length}</p>
|
<p className="text-2xl font-bold">{bookings.data.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{t('calendar.daysOccupied', { occupied: occupiedDates.size, total: daysInMonth })}
|
{t('calendar.daysOccupied', {
|
||||||
|
occupied: occupiedDates.size,
|
||||||
|
total: daysInMonth,
|
||||||
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -250,7 +250,10 @@ export default async function BookingsPage({
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(STATUS_LABEL_KEYS[String(booking.status)] ?? String(booking.status))}
|
{t(
|
||||||
|
STATUS_LABEL_KEYS[String(booking.status)] ??
|
||||||
|
String(booking.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-right">
|
<td className="p-3 text-right">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { createCourseManagementApi } from '@kit/course-management/api';
|
import { createCourseManagementApi } from '@kit/course-management/api';
|
||||||
import { CreateCourseForm } from '@kit/course-management/components';
|
import { CreateCourseForm } from '@kit/course-management/components';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
|
|||||||
@@ -88,8 +88,10 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(COURSE_STATUS_LABEL_KEYS[String(courseData.status)] ??
|
{t(
|
||||||
String(courseData.status))}
|
COURSE_STATUS_LABEL_KEYS[String(courseData.status)] ??
|
||||||
|
String(courseData.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -129,7 +129,12 @@ export default async function CourseCalendarPage({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<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`}>
|
<Link href={`/home/${account}/courses`}>
|
||||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -142,7 +147,12 @@ export default async function CourseCalendarPage({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<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
|
<Link
|
||||||
href={`/home/${account}/courses/calendar?month=${
|
href={`/home/${account}/courses/calendar?month=${
|
||||||
month === 0
|
month === 0
|
||||||
@@ -156,7 +166,12 @@ export default async function CourseCalendarPage({
|
|||||||
<CardTitle>
|
<CardTitle>
|
||||||
{MONTH_NAMES[month]} {year}
|
{MONTH_NAMES[month]} {year}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button variant="ghost" size="icon" asChild aria-label={t('calendar.nextMonth')}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
asChild
|
||||||
|
aria-label={t('calendar.nextMonth')}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/home/${account}/courses/calendar?month=${
|
href={`/home/${account}/courses/calendar?month=${
|
||||||
month === 11
|
month === 11
|
||||||
|
|||||||
@@ -196,7 +196,10 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(COURSE_STATUS_LABEL_KEYS[String(course.status)] ?? String(course.status))}
|
{t(
|
||||||
|
COURSE_STATUS_LABEL_KEYS[String(course.status)] ??
|
||||||
|
String(course.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-right">
|
<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 { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
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';
|
import { DeleteEventButton } from './delete-event-button';
|
||||||
|
|
||||||
@@ -64,8 +67,10 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|||||||
}
|
}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
>
|
>
|
||||||
{t(EVENT_STATUS_LABEL_KEYS[String(eventData.status)] ??
|
{t(
|
||||||
String(eventData.status))}
|
EVENT_STATUS_LABEL_KEYS[String(eventData.status)] ??
|
||||||
|
String(eventData.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button>
|
<Button>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import { AccountNotFound } from '~/components/account-not-found';
|
|||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
import { EmptyState } from '~/components/empty-state';
|
import { EmptyState } from '~/components/empty-state';
|
||||||
import { StatsCard } from '~/components/stats-card';
|
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 {
|
interface PageProps {
|
||||||
params: Promise<{ account: string }>;
|
params: Promise<{ account: string }>;
|
||||||
@@ -178,7 +181,10 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(EVENT_STATUS_LABEL_KEYS[String(event.status)] ?? String(event.status))}
|
{t(
|
||||||
|
EVENT_STATUS_LABEL_KEYS[String(event.status)] ??
|
||||||
|
String(event.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-right font-medium">
|
<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 { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
import { EmptyState } from '~/components/empty-state';
|
import { EmptyState } from '~/components/empty-state';
|
||||||
import { StatsCard } from '~/components/stats-card';
|
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 {
|
interface PageProps {
|
||||||
params: Promise<{ account: string }>;
|
params: Promise<{ account: string }>;
|
||||||
@@ -156,7 +159,10 @@ export default async function EventRegistrationsPage({ params }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(EVENT_STATUS_LABEL_KEYS[event.status] ?? event.status)}
|
{t(
|
||||||
|
EVENT_STATUS_LABEL_KEYS[event.status] ??
|
||||||
|
event.status,
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-right">
|
<td className="p-3 text-right">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { createFinanceApi } from '@kit/finance/api';
|
import { createFinanceApi } from '@kit/finance/api';
|
||||||
import { formatDate } from '@kit/shared/dates';
|
import { formatDate } from '@kit/shared/dates';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
@@ -66,7 +66,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
{t('invoices.invoiceLabel', { number: String(invoice.invoice_number ?? '') })}
|
{t('invoices.invoiceLabel', {
|
||||||
|
number: String(invoice.invoice_number ?? ''),
|
||||||
|
})}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Badge variant={INVOICE_STATUS_VARIANT[status] ?? 'secondary'}>
|
<Badge variant={INVOICE_STATUS_VARIANT[status] ?? 'secondary'}>
|
||||||
{t(INVOICE_STATUS_LABEL_KEYS[status] ?? status)}
|
{t(INVOICE_STATUS_LABEL_KEYS[status] ?? status)}
|
||||||
@@ -125,7 +127,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
|
|||||||
{/* Line Items */}
|
{/* Line Items */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('invoiceForm.lineItems')} ({items.length})</CardTitle>
|
<CardTitle>
|
||||||
|
{t('invoiceForm.lineItems')} ({items.length})
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
@@ -140,11 +144,15 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
|
|||||||
<th scope="col" className="p-3 text-left font-medium">
|
<th scope="col" className="p-3 text-left font-medium">
|
||||||
{t('invoiceForm.itemDescription')}
|
{t('invoiceForm.itemDescription')}
|
||||||
</th>
|
</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">
|
<th scope="col" className="p-3 text-right font-medium">
|
||||||
{t('invoices.unitPriceCol')}
|
{t('invoices.unitPriceCol')}
|
||||||
</th>
|
</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -183,7 +191,9 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={3} className="p-3 text-right font-medium">
|
<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>
|
||||||
<td className="p-3 text-right">
|
<td className="p-3 text-right">
|
||||||
{formatCurrency(invoice.tax_amount ?? 0)}
|
{formatCurrency(invoice.tax_amount ?? 0)}
|
||||||
|
|||||||
@@ -216,8 +216,10 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
|
{t(
|
||||||
String(batch.status))}
|
BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
|
||||||
|
String(batch.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
@@ -318,7 +320,11 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(INVOICE_STATUS_LABEL_KEYS[String(invoice.status)] ?? String(invoice.status))}
|
{t(
|
||||||
|
INVOICE_STATUS_LABEL_KEYS[
|
||||||
|
String(invoice.status)
|
||||||
|
] ?? String(invoice.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -347,7 +353,12 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</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" />
|
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -366,7 +377,12 @@ export default async function FinancePage({ params, searchParams }: PageProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</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" />
|
<ChevronRight className="h-4 w-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Euro, CreditCard, TrendingUp, ArrowRight } from 'lucide-react';
|
import { Euro, CreditCard, TrendingUp, ArrowRight } from 'lucide-react';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { createFinanceApi } from '@kit/finance/api';
|
import { createFinanceApi } from '@kit/finance/api';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
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">
|
<div className="flex w-full flex-col gap-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">{t('payments.subtitle')}</p>
|
||||||
{t('payments.subtitle')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
@@ -121,7 +119,9 @@ export default async function PaymentsPage({ params }: PageProps) {
|
|||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<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
|
<Badge
|
||||||
variant={openInvoices.length > 0 ? 'default' : 'secondary'}
|
variant={openInvoices.length > 0 ? 'default' : 'secondary'}
|
||||||
>
|
>
|
||||||
@@ -131,7 +131,10 @@ export default async function PaymentsPage({ params }: PageProps) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-muted-foreground mb-4 text-sm">
|
<p className="text-muted-foreground mb-4 text-sm">
|
||||||
{openInvoices.length > 0
|
{openInvoices.length > 0
|
||||||
? t('payments.invoicesOpenSummary', { count: openInvoices.length, total: formatCurrency(openTotal) })
|
? t('payments.invoicesOpenSummary', {
|
||||||
|
count: openInvoices.length,
|
||||||
|
total: formatCurrency(openTotal),
|
||||||
|
})
|
||||||
: t('payments.noOpenInvoices')}
|
: t('payments.noOpenInvoices')}
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
@@ -145,7 +148,9 @@ export default async function PaymentsPage({ params }: PageProps) {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<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'}>
|
<Badge variant={batches.length > 0 ? 'default' : 'secondary'}>
|
||||||
{batches.length}
|
{batches.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -153,7 +158,10 @@ export default async function PaymentsPage({ params }: PageProps) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-muted-foreground mb-4 text-sm">
|
<p className="text-muted-foreground mb-4 text-sm">
|
||||||
{batches.length > 0
|
{batches.length > 0
|
||||||
? t('payments.batchSummary', { count: batches.length, total: formatCurrency(sepaTotal) })
|
? t('payments.batchSummary', {
|
||||||
|
count: batches.length,
|
||||||
|
total: formatCurrency(sepaTotal),
|
||||||
|
})
|
||||||
: t('payments.noBatchesFound')}
|
: t('payments.noBatchesFound')}
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { ArrowLeft, Download } from 'lucide-react';
|
import { ArrowLeft, Download } from 'lucide-react';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { createFinanceApi } from '@kit/finance/api';
|
import { createFinanceApi } from '@kit/finance/api';
|
||||||
import { formatDate } from '@kit/shared/dates';
|
import { formatDate } from '@kit/shared/dates';
|
||||||
@@ -8,7 +9,6 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
@@ -76,7 +76,9 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
|
|||||||
{/* Summary Card */}
|
{/* Summary Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<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'}>
|
<Badge variant={BATCH_STATUS_VARIANT[status] ?? 'secondary'}>
|
||||||
{t(BATCH_STATUS_LABEL_KEYS[status] ?? status)}
|
{t(BATCH_STATUS_LABEL_KEYS[status] ?? status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -133,7 +135,9 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
|
|||||||
{/* Items Table */}
|
{/* Items Table */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('sepa.itemCount')} ({items.length})</CardTitle>
|
<CardTitle>
|
||||||
|
{t('sepa.itemCount')} ({items.length})
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
@@ -145,10 +149,18 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
|
|||||||
<table className="w-full min-w-[640px] text-sm">
|
<table className="w-full min-w-[640px] text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-muted/50 border-b">
|
<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">
|
||||||
<th scope="col" className="p-3 text-left font-medium">IBAN</th>
|
Name
|
||||||
<th scope="col" className="p-3 text-right font-medium">{t('common.amount')}</th>
|
</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">
|
||||||
|
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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -176,7 +188,11 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
|
|||||||
ITEM_STATUS_VARIANT[itemStatus] ?? 'secondary'
|
ITEM_STATUS_VARIANT[itemStatus] ?? 'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(`sepaItemStatus.${itemStatus}` as Parameters<typeof t>[0]) ?? itemStatus}
|
{t(
|
||||||
|
`sepaItemStatus.${itemStatus}` as Parameters<
|
||||||
|
typeof t
|
||||||
|
>[0],
|
||||||
|
) ?? itemStatus}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
|||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
import { EmptyState } from '~/components/empty-state';
|
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 {
|
interface PageProps {
|
||||||
params: Promise<{ account: string }>;
|
params: Promise<{ account: string }>;
|
||||||
@@ -114,7 +117,10 @@ export default async function SepaPage({ params }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(BATCH_STATUS_LABEL_KEYS[String(batch.status)] ?? String(batch.status))}
|
{t(
|
||||||
|
BATCH_STATUS_LABEL_KEYS[String(batch.status)] ??
|
||||||
|
String(batch.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
|
|||||||
@@ -4,7 +4,24 @@ import { use } from 'react';
|
|||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { redirect } from 'next/navigation';
|
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 * as z from 'zod';
|
||||||
|
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
@@ -61,65 +78,132 @@ const getAccountFeatures = cache(async (accountSlug: string) => {
|
|||||||
return (settings?.features as Record<string, boolean>) ?? {};
|
return (settings?.features as Record<string, boolean>) ?? {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const iconClasses = 'w-4';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject per-account feature routes (e.g. Fischerei) into the parsed
|
* Inject per-account feature module route groups (Fischerei, Meetings, Verband)
|
||||||
* navigation config. The entry is inserted right after "Veranstaltungen".
|
* 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(
|
function injectAccountFeatureRoutes(
|
||||||
config: z.output<typeof NavigationConfigSchema>,
|
config: z.output<typeof NavigationConfigSchema>,
|
||||||
account: string,
|
account: string,
|
||||||
features: Record<string, boolean>,
|
features: Record<string, boolean>,
|
||||||
): z.output<typeof NavigationConfigSchema> {
|
): z.output<typeof NavigationConfigSchema> {
|
||||||
if (!features.fischerei && !features.meetings && !features.verband)
|
const featureGroups: z.output<typeof NavigationConfigSchema>['routes'] = [];
|
||||||
return config;
|
|
||||||
|
|
||||||
const featureEntries: Array<{
|
|
||||||
label: string;
|
|
||||||
path: string;
|
|
||||||
Icon: React.ReactNode;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
if (features.fischerei) {
|
if (features.fischerei) {
|
||||||
featureEntries.push({
|
featureGroups.push({
|
||||||
label: 'common.routes.fischerei',
|
label: 'common:routes.fisheriesManagement',
|
||||||
|
collapsible: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'common:routes.fisheriesOverview',
|
||||||
path: `/home/${account}/fischerei`,
|
path: `/home/${account}/fischerei`,
|
||||||
Icon: <Fish className="w-4" />,
|
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) {
|
if (features.meetings) {
|
||||||
featureEntries.push({
|
featureGroups.push({
|
||||||
label: 'common.routes.meetings',
|
label: 'common:routes.meetingProtocols',
|
||||||
|
collapsible: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'common:routes.meetingsOverview',
|
||||||
path: `/home/${account}/meetings`,
|
path: `/home/${account}/meetings`,
|
||||||
Icon: <FileSignature className="w-4" />,
|
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) {
|
if (features.verband) {
|
||||||
featureEntries.push({
|
featureGroups.push({
|
||||||
label: 'common.routes.verband',
|
label: 'common:routes.associationManagement',
|
||||||
|
collapsible: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'common:routes.associationOverview',
|
||||||
path: `/home/${account}/verband`,
|
path: `/home/${account}/verband`,
|
||||||
Icon: <Building2 className="w-4" />,
|
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 {
|
if (featureGroups.length === 0) return config;
|
||||||
...config,
|
|
||||||
routes: config.routes.map((group) => {
|
|
||||||
if (!('children' in group)) return group;
|
|
||||||
|
|
||||||
const eventsIndex = group.children.findIndex(
|
// Insert before the last group (Administration)
|
||||||
(child) => child.label === 'common.routes.events',
|
const routes = [...config.routes];
|
||||||
);
|
const adminIndex = routes.length - 1;
|
||||||
|
routes.splice(adminIndex, 0, ...featureGroups);
|
||||||
|
|
||||||
if (eventsIndex === -1) return group;
|
return { ...config, routes };
|
||||||
|
|
||||||
const newChildren = [...group.children];
|
|
||||||
newChildren.splice(eventsIndex + 1, 0, ...featureEntries);
|
|
||||||
|
|
||||||
return { ...group, children: newChildren };
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function SidebarLayout({
|
async function SidebarLayout({
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import { PageBody } from '@kit/ui/page';
|
|||||||
export default function AccountLoading() {
|
export default function AccountLoading() {
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<div className="flex flex-col gap-6 animate-pulse">
|
<div className="flex animate-pulse flex-col gap-6">
|
||||||
<div className="h-8 w-48 rounded-md bg-muted" />
|
<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">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[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>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
{[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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ export default async function ProtocolDetailPage({ params }: PageProps) {
|
|||||||
{MEETING_TYPE_LABELS[protocol.status] ?? protocol.status}
|
{MEETING_TYPE_LABELS[protocol.status] ?? protocol.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
{protocol.status === 'final' ? (
|
{protocol.status === 'final' ? (
|
||||||
<Badge variant="default">{t('pages.statusPublished')}</Badge>
|
<Badge variant="default">
|
||||||
|
{t('pages.statusPublished')}
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline">{t('pages.statusDraft')}</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 { createMemberManagementApi } from '@kit/member-management/api';
|
||||||
import { EditMemberForm } from '@kit/member-management/components';
|
import { EditMemberForm } from '@kit/member-management/components';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
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 { createModuleBuilderApi } from '@kit/module-builder/api';
|
||||||
import { ModuleForm } from '@kit/module-builder/components';
|
import { ModuleForm } from '@kit/module-builder/components';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
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 { createModuleBuilderApi } from '@kit/module-builder/api';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
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 { createModuleBuilderApi } from '@kit/module-builder/api';
|
||||||
import { ModuleForm } from '@kit/module-builder/components';
|
import { ModuleForm } from '@kit/module-builder/components';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
|
|||||||
@@ -171,7 +171,11 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS[rStatus] ?? rStatus)}
|
{t(
|
||||||
|
NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS[
|
||||||
|
rStatus
|
||||||
|
] ?? rStatus,
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -204,7 +204,10 @@ export default async function NewsletterPage({
|
|||||||
'secondary'
|
'secondary'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(NEWSLETTER_STATUS_LABEL_KEYS[String(nl.status)] ?? String(nl.status))}
|
{t(
|
||||||
|
NEWSLETTER_STATUS_LABEL_KEYS[String(nl.status)] ??
|
||||||
|
String(nl.status),
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-right">
|
<td className="p-3 text-right">
|
||||||
@@ -241,7 +244,12 @@ export default async function NewsletterPage({
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</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" />
|
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -256,11 +264,19 @@ export default async function NewsletterPage({
|
|||||||
href={`/home/${account}/newsletter${buildQuery(queryBase, { page: safePage + 1 })}`}
|
href={`/home/${account}/newsletter${buildQuery(queryBase, { page: safePage + 1 })}`}
|
||||||
aria-label={t('common.next')}
|
aria-label={t('common.next')}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" aria-hidden="true" />
|
<ChevronRight
|
||||||
|
className="h-4 w-4"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</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" />
|
<ChevronRight className="h-4 w-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { CreatePageForm } from '@kit/site-builder/components';
|
import { CreatePageForm } from '@kit/site-builder/components';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
|
|||||||
@@ -166,10 +166,7 @@ export default async function SiteBuilderDashboard({ params }: Props) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(pages as SitePage[]).map((page) => (
|
{(pages as SitePage[]).map((page) => (
|
||||||
<tr
|
<tr key={page.id} className="hover:bg-muted/30 border-b">
|
||||||
key={page.id}
|
|
||||||
className="hover:bg-muted/30 border-b"
|
|
||||||
>
|
|
||||||
<td className="p-3 font-medium">{page.title}</td>
|
<td className="p-3 font-medium">{page.title}</td>
|
||||||
<td className="text-muted-foreground p-3 font-mono text-xs">
|
<td className="text-muted-foreground p-3 font-mono text-xs">
|
||||||
/{page.slug}
|
/{page.slug}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { CreatePostForm } from '@kit/site-builder/components';
|
import { CreatePostForm } from '@kit/site-builder/components';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { AccountNotFound } from '~/components/account-not-found';
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ export default async function PostsManagerPage({ params }: Props) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(posts as SitePost[]).map((post) => (
|
{(posts as SitePost[]).map((post) => (
|
||||||
<tr
|
<tr key={post.id} className="hover:bg-muted/30 border-b">
|
||||||
key={post.id}
|
|
||||||
className="hover:bg-muted/30 border-b"
|
|
||||||
>
|
|
||||||
<td className="p-3 font-medium">{post.title}</td>
|
<td className="p-3 font-medium">{post.title}</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useTransition } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { toast } from '@kit/ui/sonner';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@kit/ui/alert-dialog';
|
} from '@kit/ui/alert-dialog';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { toast } from '@kit/ui/sonner';
|
||||||
|
|
||||||
interface PublishToggleButtonProps {
|
interface PublishToggleButtonProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -38,11 +38,14 @@ export function PublishToggleButton({
|
|||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/site-builder/pages/${pageId}/publish`, {
|
const response = await fetch(
|
||||||
|
`/api/site-builder/pages/${pageId}/publish`,
|
||||||
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ accountId, isPublished: !isPublished }),
|
body: JSON.stringify({ accountId, isPublished: !isPublished }),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
toast.error(t('pages.toggleError'));
|
toast.error(t('pages.toggleError'));
|
||||||
|
|||||||
@@ -230,9 +230,7 @@ export default function SettingsContent({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">{t('settings.subtitle')}</p>
|
||||||
{t('settings.subtitle')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export async function AccountNotFound({
|
|||||||
const t = await getTranslations('common');
|
const t = await getTranslations('common');
|
||||||
|
|
||||||
const resolvedTitle = title ?? t('accountNotFoundCard.title');
|
const resolvedTitle = title ?? t('accountNotFoundCard.title');
|
||||||
const resolvedDescription = description ?? t('accountNotFoundCard.description');
|
const resolvedDescription =
|
||||||
|
description ?? t('accountNotFoundCard.description');
|
||||||
const resolvedButtonLabel = buttonLabel ?? t('accountNotFoundCard.action');
|
const resolvedButtonLabel = buttonLabel ?? t('accountNotFoundCard.action');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export function CmsPageShell({
|
|||||||
<TeamAccountLayoutPageHeader
|
<TeamAccountLayoutPageHeader
|
||||||
account={account}
|
account={account}
|
||||||
title={title}
|
title={title}
|
||||||
description={description !== undefined ? description : <AppBreadcrumbs />}
|
description={
|
||||||
|
description !== undefined ? description : <AppBreadcrumbs />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageBody>{children}</PageBody>
|
<PageBody>{children}</PageBody>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
UserPlus,
|
UserPlus,
|
||||||
IdCard,
|
IdCard,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
KeyRound,
|
|
||||||
// Courses
|
// Courses
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
@@ -41,24 +40,6 @@ import {
|
|||||||
PanelTop,
|
PanelTop,
|
||||||
Newspaper,
|
Newspaper,
|
||||||
Palette,
|
Palette,
|
||||||
// Fisheries
|
|
||||||
Fish,
|
|
||||||
Waves,
|
|
||||||
Anchor,
|
|
||||||
BookOpen,
|
|
||||||
ShieldCheck,
|
|
||||||
Trophy,
|
|
||||||
// Meetings
|
|
||||||
BookMarked,
|
|
||||||
ListChecks,
|
|
||||||
ScrollText,
|
|
||||||
// Association (Verband)
|
|
||||||
Building2,
|
|
||||||
Network,
|
|
||||||
SearchCheck,
|
|
||||||
Share2,
|
|
||||||
PieChart,
|
|
||||||
LayoutTemplate,
|
|
||||||
// Modules
|
// Modules
|
||||||
Database,
|
Database,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -396,176 +377,51 @@ const getRoutes = (account: string) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Custom Modules ──
|
// Note: Fischerei, Meetings, and Verband sections are injected at runtime
|
||||||
if (featureFlagsConfig.enableModuleBuilder) {
|
// via injectAccountFeatureRoutes() in the layout, based on per-account
|
||||||
routes.push({
|
// settings (account_settings.features). They are NOT added here to avoid
|
||||||
label: 'common:routes.customModules',
|
// duplicate entries when both the global feature flag and per-account
|
||||||
collapsible: true,
|
// setting are enabled.
|
||||||
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} />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Administration ──
|
// ── Administration ──
|
||||||
routes.push({
|
{
|
||||||
label: 'common:routes.administration',
|
const adminChildren: Array<
|
||||||
collapsible: false,
|
| {
|
||||||
children: [
|
label: string;
|
||||||
|
path: string;
|
||||||
|
Icon: React.ReactNode;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> = [
|
||||||
{
|
{
|
||||||
label: 'common:routes.accountSettings',
|
label: 'common:routes.accountSettings',
|
||||||
path: createPath(pathsConfig.app.accountSettings, account),
|
path: createPath(pathsConfig.app.accountSettings, account),
|
||||||
Icon: <Settings className={iconClasses} />,
|
Icon: <Settings className={iconClasses} />,
|
||||||
},
|
},
|
||||||
featureFlagsConfig.enableTeamAccountBilling
|
];
|
||||||
? {
|
|
||||||
|
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',
|
label: 'common:routes.billing',
|
||||||
path: createPath(pathsConfig.app.accountBilling, account),
|
path: createPath(pathsConfig.app.accountBilling, account),
|
||||||
Icon: <CreditCard className={iconClasses} />,
|
Icon: <CreditCard className={iconClasses} />,
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.push({
|
||||||
|
label: 'common:routes.administration',
|
||||||
|
collapsible: false,
|
||||||
|
children: adminChildren,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -150,15 +150,7 @@
|
|||||||
"overview": "Kurstermine im Überblick",
|
"overview": "Kurstermine im Überblick",
|
||||||
"activeCourses": "Aktive Kurse ({count})",
|
"activeCourses": "Aktive Kurse ({count})",
|
||||||
"noActiveCourses": "Keine aktiven Kurse in diesem Monat.",
|
"noActiveCourses": "Keine aktiven Kurse in diesem Monat.",
|
||||||
"weekdays": [
|
"weekdays": ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
|
||||||
"Mo",
|
|
||||||
"Di",
|
|
||||||
"Mi",
|
|
||||||
"Do",
|
|
||||||
"Fr",
|
|
||||||
"Sa",
|
|
||||||
"So"
|
|
||||||
],
|
|
||||||
"months": [
|
"months": [
|
||||||
"Januar",
|
"Januar",
|
||||||
"Februar",
|
"Februar",
|
||||||
|
|||||||
@@ -150,15 +150,7 @@
|
|||||||
"overview": "Overview of course dates",
|
"overview": "Overview of course dates",
|
||||||
"activeCourses": "Active Courses ({count})",
|
"activeCourses": "Active Courses ({count})",
|
||||||
"noActiveCourses": "No active courses this month.",
|
"noActiveCourses": "No active courses this month.",
|
||||||
"weekdays": [
|
"weekdays": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
"Thu",
|
|
||||||
"Fri",
|
|
||||||
"Sat",
|
|
||||||
"Sun"
|
|
||||||
],
|
|
||||||
"months": [
|
"months": [
|
||||||
"January",
|
"January",
|
||||||
"February",
|
"February",
|
||||||
|
|||||||
@@ -223,7 +223,8 @@ export const COURSE_STATUS_LABEL = COURSE_STATUS_LABEL_KEYS;
|
|||||||
/** @deprecated Use APPLICATION_STATUS_LABEL_KEYS + t() */
|
/** @deprecated Use APPLICATION_STATUS_LABEL_KEYS + t() */
|
||||||
export const APPLICATION_STATUS_LABEL = APPLICATION_STATUS_LABEL_KEYS;
|
export const APPLICATION_STATUS_LABEL = APPLICATION_STATUS_LABEL_KEYS;
|
||||||
/** @deprecated Use NEWSLETTER_RECIPIENT_STATUS_LABEL_KEYS + t() */
|
/** @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() */
|
/** @deprecated Use BOOKING_STATUS_LABEL_KEYS + t() */
|
||||||
export const BOOKING_STATUS_LABEL = BOOKING_STATUS_LABEL_KEYS;
|
export const BOOKING_STATUS_LABEL = BOOKING_STATUS_LABEL_KEYS;
|
||||||
/** @deprecated Use MODULE_STATUS_LABEL_KEYS + t() */
|
/** @deprecated Use MODULE_STATUS_LABEL_KEYS + t() */
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const RouteGroup = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const NavigationConfigSchema = 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'),
|
sidebarCollapsedStyle: z.enum(['icon', 'offcanvas', 'none']).default('icon'),
|
||||||
routes: z.array(z.union([RouteGroup, Divider])),
|
routes: z.array(z.union([RouteGroup, Divider])),
|
||||||
style: z.enum(['sidebar', 'header', 'custom']).default('sidebar'),
|
style: z.enum(['sidebar', 'header', 'custom']).default('sidebar'),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ function MaybeCollapsible({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible defaultOpen={defaultOpen} className={'group/collapsible'}>
|
<Collapsible defaultOpen={defaultOpen} className="group/collapsible">
|
||||||
{children}
|
{children}
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
@@ -129,23 +129,23 @@ function SidebarNavigationRouteGroupLabel({
|
|||||||
}) {
|
}) {
|
||||||
const className = cn({ hidden: !open });
|
const className = cn({ hidden: !open });
|
||||||
|
|
||||||
|
if (!collapsible) {
|
||||||
return (
|
return (
|
||||||
<If
|
|
||||||
condition={collapsible}
|
|
||||||
fallback={
|
|
||||||
<SidebarGroupLabel className={className}>
|
<SidebarGroupLabel className={className}>
|
||||||
<Trans i18nKey={label} defaults={label} />
|
<Trans i18nKey={label} defaults={label} />
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<SidebarGroupLabel className={className}>
|
|
||||||
<CollapsibleTrigger>
|
|
||||||
<Trans i18nKey={label} defaults={label} />
|
|
||||||
|
|
||||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
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>
|
</CollapsibleTrigger>
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
</If>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +165,8 @@ function SidebarNavigationSubItems({
|
|||||||
{(items) =>
|
{(items) =>
|
||||||
items.length > 0 && (
|
items.length > 0 && (
|
||||||
<SidebarMenuSub
|
<SidebarMenuSub
|
||||||
className={cn({
|
className={cn('border-sidebar-border ml-3.5 border-l pl-2', {
|
||||||
'mx-0 px-1.5': !open,
|
'mx-0 border-l-0 px-1.5': !open,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{items.map((child) => {
|
{items.map((child) => {
|
||||||
@@ -241,7 +241,7 @@ function SidebarNavigationRouteChildItem({
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-width w-auto transition-opacity duration-500',
|
'transition-width w-auto transition-opacity duration-300',
|
||||||
{
|
{
|
||||||
'w-0 opacity-0': !open,
|
'w-0 opacity-0': !open,
|
||||||
},
|
},
|
||||||
@@ -250,9 +250,9 @@ function SidebarNavigationRouteChildItem({
|
|||||||
<Trans i18nKey={child.label} defaults={child.label} />
|
<Trans i18nKey={child.label} defaults={child.label} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ChevronDown
|
<ChevronRight
|
||||||
className={cn(
|
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,
|
'hidden size-0': !open,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
|
import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
|
||||||
|
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
||||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...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 (
|
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