feat: pre-existing local changes — fischerei, verband, modules, members, packages
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 6m20s
Workflow / ⚫️ Test (push) Has been skipped

Commits all remaining uncommitted local work:

- apps/web: fischerei, verband, modules, members-cms, documents,
  newsletter, meetings, site-builder, courses, bookings, events,
  finance pages and components
- apps/web: marketing page updates, layout, paths config,
  next.config.mjs, styles/makerkit.css
- apps/web/i18n: documents, fischerei, marketing, verband (de+en)
- packages/features: finance, fischerei, member-management,
  module-builder, newsletter, sitzungsprotokolle, verbandsverwaltung
  server APIs and components
- packages/ui: button.tsx updates
- pnpm-lock.yaml
This commit is contained in:
Zaid Marzguioui
2026-04-02 01:19:54 +02:00
parent a1719671df
commit b26e5aaafa
153 changed files with 2329 additions and 1227 deletions

1
.bg-shell/manifest.json Normal file
View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,37 @@
'use client';
import { useEffect, useRef } from 'react';
export function AnimateOnScroll(props: {
children: React.ReactNode;
className?: string;
}) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const reveals = el.querySelectorAll('.reveal');
reveals.forEach((r) => r.setAttribute('data-visible', 'true'));
observer.disconnect();
}
}
},
{ threshold: 0.15 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<div ref={ref} className={props.className}>
{props.children}
</div>
);
}

View File

@@ -3,7 +3,6 @@ import Link from 'next/link';
import { import {
ArrowRightIcon, ArrowRightIcon,
BookOpenIcon,
CalendarIcon, CalendarIcon,
FileTextIcon, FileTextIcon,
GraduationCapIcon, GraduationCapIcon,
@@ -27,15 +26,19 @@ import {
EcosystemShowcase, EcosystemShowcase,
FeatureShowcase, FeatureShowcase,
FeatureShowcaseIconContainer, FeatureShowcaseIconContainer,
GradientText,
Hero, Hero,
Pill, Pill,
SecondaryHero, SecondaryHero,
} from '@kit/ui/marketing'; } from '@kit/ui/marketing';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import billingConfig from '~/config/billing.config'; import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config'; import pathsConfig from '~/config/paths.config';
import { AnimateOnScroll } from './_components/animate-on-scroll';
function Home() { function Home() {
return ( return (
<div className={'mt-4 flex flex-col space-y-24 py-14 lg:space-y-36'}> <div className={'mt-4 flex flex-col space-y-24 py-14 lg:space-y-36'}>
@@ -51,7 +54,10 @@ function Home() {
} }
title={ title={
<span className="text-secondary-foreground"> <span className="text-secondary-foreground">
<Trans i18nKey={'marketing.heroTitle'} /> <Trans i18nKey={'marketing.heroTitleLine1'} />{' '}
<GradientText className="from-primary to-primary/60">
<Trans i18nKey={'marketing.heroTitleLine2'} />
</GradientText>
</span> </span>
} }
subtitle={ subtitle={
@@ -61,6 +67,11 @@ function Home() {
} }
cta={<MainCallToActionButton />} cta={<MainCallToActionButton />}
image={ image={
<div className="relative">
<div
className="bg-primary/10 absolute inset-0 -z-10 mx-auto max-w-3xl rounded-full blur-3xl"
aria-hidden="true"
/>
<Image <Image
priority priority
className={ className={
@@ -71,30 +82,30 @@ function Home() {
src={`/images/dashboard.webp`} src={`/images/dashboard.webp`}
alt={`MyEasyCMS Dashboard`} alt={`MyEasyCMS Dashboard`}
/> />
</div>
} }
/> />
</div> </div>
{/* Trust Indicators */} {/* Stats Bar */}
<AnimateOnScroll>
<div className={'container mx-auto'}> <div className={'container mx-auto'}>
<div className="flex flex-col items-center gap-8"> <div className="border-border border-y py-8">
<p className="text-muted-foreground text-sm font-medium tracking-widest uppercase"> <p className="text-muted-foreground mb-6 text-center text-sm font-medium tracking-widest uppercase">
<Trans i18nKey={'marketing.trustedBy'} /> <Trans i18nKey={'marketing.trustedBy'} />
</p> </p>
<div className="reveal-stagger divide-border flex flex-wrap items-center justify-center divide-x">
<div className="flex flex-wrap items-center justify-center gap-x-12 gap-y-6"> <StatItem value="69,000+" labelKey="marketing.statMembers" />
<TrustItem icon={UsersIcon} label="marketing.trustAssociations" /> <StatItem value="90+" labelKey="marketing.statOrganizations" />
<TrustItem <StatItem value="22" labelKey="marketing.statYears" />
icon={GraduationCapIcon} <StatItem value="3" labelKey="marketing.statFederations" />
label="marketing.trustSchools"
/>
<TrustItem icon={BookOpenIcon} label="marketing.trustClubs" />
<TrustItem icon={GlobeIcon} label="marketing.trustOrganizations" />
</div> </div>
</div> </div>
</div> </div>
</AnimateOnScroll>
{/* Core Modules Feature Grid */} {/* Core Modules Feature Grid */}
<AnimateOnScroll>
<div className={'container mx-auto'}> <div className={'container mx-auto'}>
<div className={'py-4 xl:py-8'}> <div className={'py-4 xl:py-8'}>
<FeatureShowcase <FeatureShowcase
@@ -118,7 +129,7 @@ function Home() {
</FeatureShowcaseIconContainer> </FeatureShowcaseIconContainer>
} }
> >
<div className="mt-2 grid w-full grid-cols-1 gap-4 md:mt-6 md:grid-cols-2 lg:grid-cols-3"> <div className="reveal-stagger mt-2 grid w-full grid-cols-1 gap-4 md:mt-6 md:grid-cols-2 lg:grid-cols-3">
<IconFeatureCard <IconFeatureCard
icon={UsersIcon} icon={UsersIcon}
titleKey="marketing.featureMembersTitle" titleKey="marketing.featureMembersTitle"
@@ -128,49 +139,78 @@ function Home() {
icon={GraduationCapIcon} icon={GraduationCapIcon}
titleKey="marketing.featureCoursesTitle" titleKey="marketing.featureCoursesTitle"
descKey="marketing.featureCoursesDesc" descKey="marketing.featureCoursesDesc"
accentBg="bg-chart-1/10"
accentText="text-chart-1"
/> />
<IconFeatureCard <IconFeatureCard
icon={BedDoubleIcon} icon={BedDoubleIcon}
titleKey="marketing.featureBookingsTitle" titleKey="marketing.featureBookingsTitle"
descKey="marketing.featureBookingsDesc" descKey="marketing.featureBookingsDesc"
accentBg="bg-chart-2/10"
accentText="text-chart-2"
/> />
<IconFeatureCard <IconFeatureCard
icon={CalendarIcon} icon={CalendarIcon}
titleKey="marketing.featureEventsTitle" titleKey="marketing.featureEventsTitle"
descKey="marketing.featureEventsDesc" descKey="marketing.featureEventsDesc"
accentBg="bg-chart-3/10"
accentText="text-chart-3"
/> />
<IconFeatureCard <IconFeatureCard
icon={WalletIcon} icon={WalletIcon}
titleKey="marketing.featureFinanceTitle" titleKey="marketing.featureFinanceTitle"
descKey="marketing.featureFinanceDesc" descKey="marketing.featureFinanceDesc"
accentBg="bg-chart-4/10"
accentText="text-chart-4"
/> />
<IconFeatureCard <IconFeatureCard
icon={MailIcon} icon={MailIcon}
titleKey="marketing.featureNewsletterTitle" titleKey="marketing.featureNewsletterTitle"
descKey="marketing.featureNewsletterDesc" descKey="marketing.featureNewsletterDesc"
accentBg="bg-chart-5/10"
accentText="text-chart-5"
/> />
</div> </div>
</FeatureShowcase> </FeatureShowcase>
</div> </div>
</div> </div>
</AnimateOnScroll>
{/* Dashboard Showcase */} {/* Testimonials */}
<div className={'container mx-auto'}> <AnimateOnScroll>
<EcosystemShowcase <div className="container mx-auto">
heading={<Trans i18nKey={'marketing.showcaseHeading'} />} <div className="flex flex-col items-center gap-12">
description={<Trans i18nKey={'marketing.showcaseDescription'} />} <div className="text-center">
> <h2 className="text-3xl font-medium tracking-tight xl:text-5xl dark:text-white">
<Image <Trans i18nKey={'marketing.testimonialsHeading'} />
className="rounded-lg shadow-lg" </h2>
src={'/images/dashboard.webp'} <p className="text-secondary-foreground/70 mx-auto mt-4 max-w-2xl text-xl font-medium tracking-tight">
alt="MyEasyCMS Dashboard" <Trans i18nKey={'marketing.testimonialsSubheading'} />
width={1200} </p>
height={800}
/>
</EcosystemShowcase>
</div> </div>
<div className="reveal-stagger grid w-full grid-cols-1 gap-6 md:grid-cols-3">
<TestimonialCard
quoteKey="marketing.testimonial1Quote"
nameKey="marketing.testimonial1Name"
roleKey="marketing.testimonial1Role"
/>
<TestimonialCard
quoteKey="marketing.testimonial2Quote"
nameKey="marketing.testimonial2Name"
roleKey="marketing.testimonial2Role"
/>
<TestimonialCard
quoteKey="marketing.testimonial3Quote"
nameKey="marketing.testimonial3Name"
roleKey="marketing.testimonial3Role"
/>
</div>
</div>
</div>
</AnimateOnScroll>
{/* Additional Features Row */} {/* Additional Features Row */}
<AnimateOnScroll>
<div className={'container mx-auto'}> <div className={'container mx-auto'}>
<div className={'py-4 xl:py-8'}> <div className={'py-4 xl:py-8'}>
<FeatureShowcase <FeatureShowcase
@@ -194,33 +234,42 @@ function Home() {
</FeatureShowcaseIconContainer> </FeatureShowcaseIconContainer>
} }
> >
<div className="mt-2 grid w-full grid-cols-1 gap-4 md:mt-6 md:grid-cols-2 lg:grid-cols-3"> <div className="reveal-stagger mt-2 grid w-full grid-cols-1 gap-4 md:mt-6 md:grid-cols-2 lg:grid-cols-3">
<IconFeatureCard <IconFeatureCard
icon={FileTextIcon} icon={FileTextIcon}
titleKey="marketing.featureDocumentsTitle" titleKey="marketing.featureDocumentsTitle"
descKey="marketing.featureDocumentsDesc" descKey="marketing.featureDocumentsDesc"
accentBg="bg-chart-1/10"
accentText="text-chart-1"
/> />
<IconFeatureCard <IconFeatureCard
icon={GlobeIcon} icon={GlobeIcon}
titleKey="marketing.featureSiteBuilderTitle" titleKey="marketing.featureSiteBuilderTitle"
descKey="marketing.featureSiteBuilderDesc" descKey="marketing.featureSiteBuilderDesc"
accentBg="bg-chart-2/10"
accentText="text-chart-2"
/> />
<IconFeatureCard <IconFeatureCard
icon={LayoutDashboardIcon} icon={LayoutDashboardIcon}
titleKey="marketing.featureModulesTitle" titleKey="marketing.featureModulesTitle"
descKey="marketing.featureModulesDesc" descKey="marketing.featureModulesDesc"
accentBg="bg-chart-3/10"
accentText="text-chart-3"
/> />
</div> </div>
</FeatureShowcase> </FeatureShowcase>
</div> </div>
</div> </div>
</AnimateOnScroll>
{/* Why Choose Us Section */} {/* Why Choose Us Section */}
<AnimateOnScroll>
<div className={'container mx-auto'}> <div className={'container mx-auto'}>
<EcosystemShowcase <EcosystemShowcase
heading={<Trans i18nKey={'marketing.whyChooseHeading'} />} heading={<Trans i18nKey={'marketing.whyChooseHeading'} />}
description={<Trans i18nKey={'marketing.whyChooseDescription'} />} description={<Trans i18nKey={'marketing.whyChooseDescription'} />}
textPosition="right" textPosition="right"
className="border-primary/10 rounded-xl border"
> >
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<WhyItem <WhyItem
@@ -246,8 +295,10 @@ function Home() {
</div> </div>
</EcosystemShowcase> </EcosystemShowcase>
</div> </div>
</AnimateOnScroll>
{/* How It Works */} {/* How It Works */}
<AnimateOnScroll>
<div className="container mx-auto"> <div className="container mx-auto">
<div className="flex flex-col items-center gap-12"> <div className="flex flex-col items-center gap-12">
<div className="text-center"> <div className="text-center">
@@ -259,7 +310,11 @@ function Home() {
</p> </p>
</div> </div>
<div className="grid w-full grid-cols-1 gap-8 md:grid-cols-3"> <div className="relative grid w-full grid-cols-1 gap-8 md:grid-cols-3">
<div
className="border-primary/30 absolute top-10 right-[16.67%] left-[16.67%] hidden h-px border-t border-dashed md:block"
aria-hidden="true"
/>
<StepCard <StepCard
step="01" step="01"
titleKey="marketing.howStep1Title" titleKey="marketing.howStep1Title"
@@ -278,8 +333,10 @@ function Home() {
</div> </div>
</div> </div>
</div> </div>
</AnimateOnScroll>
{/* Pricing Section */} {/* Pricing Section */}
<AnimateOnScroll>
<div className={'container mx-auto'}> <div className={'container mx-auto'}>
<div <div
className={ className={
@@ -292,7 +349,11 @@ function Home() {
<Trans i18nKey={'marketing.pricingPillText'} /> <Trans i18nKey={'marketing.pricingPillText'} />
</Pill> </Pill>
} }
heading={<Trans i18nKey={'marketing.pricingHeading'} />} heading={
<GradientText className="from-primary to-primary/60">
<Trans i18nKey={'marketing.pricingHeading'} />
</GradientText>
}
subheading={<Trans i18nKey={'marketing.pricingSubheading'} />} subheading={<Trans i18nKey={'marketing.pricingSubheading'} />}
/> />
@@ -307,26 +368,30 @@ function Home() {
</div> </div>
</div> </div>
</div> </div>
</AnimateOnScroll>
{/* Final CTA */} {/* Final CTA */}
<AnimateOnScroll>
<div className="container mx-auto"> <div className="container mx-auto">
<div className="bg-primary/5 flex flex-col items-center gap-8 rounded-2xl border p-12 text-center lg:p-16"> <div className="ring-primary/10 from-primary/10 via-background to-primary/5 flex flex-col items-center gap-8 rounded-2xl border bg-gradient-to-br p-12 text-center ring-1 lg:p-16">
<h2 className="max-w-3xl text-3xl font-medium tracking-tight xl:text-5xl dark:text-white"> <h2 className="max-w-3xl text-3xl font-medium tracking-tight xl:text-5xl dark:text-white">
<GradientText className="from-primary to-primary/60">
<Trans i18nKey={'marketing.ctaHeading'} /> <Trans i18nKey={'marketing.ctaHeading'} />
</GradientText>
</h2> </h2>
<p className="text-secondary-foreground/70 max-w-2xl text-lg"> <p className="text-secondary-foreground/70 max-w-2xl text-lg">
<Trans i18nKey={'marketing.ctaDescription'} /> <Trans i18nKey={'marketing.ctaDescription'} />
</p> </p>
<div className="flex flex-col gap-3 sm:flex-row"> <div className="flex flex-col gap-3 sm:flex-row">
<CtaButton className="h-12 px-8 text-base"> <CtaButton className="h-14 px-10 text-lg">
<Link href={'/auth/sign-up'}> <Link href={'/auth/sign-up'}>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Trans i18nKey={'marketing.ctaButtonPrimary'} /> <Trans i18nKey={'marketing.ctaButtonPrimary'} />
<ArrowRightIcon className="h-4 w-4" /> <ArrowRightIcon className="h-5 w-5" />
</span> </span>
</Link> </Link>
</CtaButton> </CtaButton>
<CtaButton variant={'outline'} className="h-12 px-8 text-base"> <CtaButton variant={'outline'} className="h-14 px-10 text-lg">
<Link href={'/contact'}> <Link href={'/contact'}>
<Trans i18nKey={'marketing.ctaButtonSecondary'} /> <Trans i18nKey={'marketing.ctaButtonSecondary'} />
</Link> </Link>
@@ -338,6 +403,7 @@ function Home() {
</p> </p>
</div> </div>
</div> </div>
</AnimateOnScroll>
</div> </div>
); );
} }
@@ -347,7 +413,7 @@ export default Home;
function MainCallToActionButton() { function MainCallToActionButton() {
return ( return (
<div className={'flex space-x-2.5'}> <div className={'flex space-x-2.5'}>
<CtaButton className="h-10 text-sm"> <CtaButton className="h-12 px-8 text-base shadow-lg">
<Link href={'/auth/sign-up'}> <Link href={'/auth/sign-up'}>
<span className={'flex items-center space-x-0.5'}> <span className={'flex items-center space-x-0.5'}>
<span> <span>
@@ -364,7 +430,7 @@ function MainCallToActionButton() {
</Link> </Link>
</CtaButton> </CtaButton>
<CtaButton variant={'link'} className="h-10 text-sm"> <CtaButton variant={'link'} className="h-12 px-8 text-base">
<Link href={'/pricing'}> <Link href={'/pricing'}>
<Trans i18nKey={'common.pricing'} /> <Trans i18nKey={'common.pricing'} />
</Link> </Link>
@@ -377,11 +443,20 @@ function IconFeatureCard(props: {
icon: React.ComponentType<{ className?: string }>; icon: React.ComponentType<{ className?: string }>;
titleKey: string; titleKey: string;
descKey: string; descKey: string;
accentBg?: string;
accentText?: string;
}) { }) {
return ( return (
<div className="bg-muted/50 flex flex-col gap-3 rounded p-6"> <div className="reveal bg-muted/50 hover:border-primary/20 flex flex-col gap-3 rounded-xl border border-transparent p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-md">
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg"> <div
<props.icon className="text-primary h-5 w-5" /> className={cn(
'flex h-10 w-10 items-center justify-center rounded-lg',
props.accentBg ?? 'bg-primary/10',
)}
>
<props.icon
className={cn('h-5 w-5', props.accentText ?? 'text-primary')}
/>
</div> </div>
<h4 className="text-lg font-medium"> <h4 className="text-lg font-medium">
<Trans i18nKey={props.titleKey} /> <Trans i18nKey={props.titleKey} />
@@ -393,16 +468,39 @@ function IconFeatureCard(props: {
); );
} }
function TrustItem(props: { function StatItem(props: { value: string; labelKey: string }) {
icon: React.ComponentType<{ className?: string }>; return (
label: string; <div className="reveal flex flex-col items-center gap-1 px-6 py-4">
<span className="text-primary text-3xl font-bold tracking-tight lg:text-4xl">
{props.value}
</span>
<span className="text-muted-foreground text-sm font-medium">
<Trans i18nKey={props.labelKey} />
</span>
</div>
);
}
function TestimonialCard(props: {
quoteKey: string;
nameKey: string;
roleKey: string;
}) { }) {
return ( return (
<div className="text-muted-foreground flex items-center gap-2.5"> <div className="reveal border-border bg-card flex flex-col gap-4 rounded-xl border p-6 shadow-sm">
<props.icon className="h-5 w-5" /> <p className="text-secondary-foreground text-sm leading-relaxed italic">
<span className="text-sm font-medium"> &ldquo;
<Trans i18nKey={props.label} /> <Trans i18nKey={props.quoteKey} />
</span> &rdquo;
</p>
<div className="border-border border-t pt-4">
<p className="text-sm font-medium">
<Trans i18nKey={props.nameKey} />
</p>
<p className="text-muted-foreground text-xs">
<Trans i18nKey={props.roleKey} />
</p>
</div>
</div> </div>
); );
} }
@@ -414,7 +512,7 @@ function WhyItem(props: {
}) { }) {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
<div className="bg-primary/10 flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"> <div className="ring-primary/20 bg-primary/10 flex h-12 w-12 shrink-0 items-center justify-center rounded-xl ring-1">
<props.icon className="text-primary h-5 w-5" /> <props.icon className="text-primary h-5 w-5" />
</div> </div>
<div> <div>
@@ -431,8 +529,10 @@ function WhyItem(props: {
function StepCard(props: { step: string; titleKey: string; descKey: string }) { function StepCard(props: { step: string; titleKey: string; descKey: string }) {
return ( return (
<div className="bg-muted/50 relative flex flex-col gap-4 rounded-lg p-6"> <div className="reveal border-border bg-card relative flex flex-col items-center gap-4 rounded-xl border p-8 text-center shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-md">
<span className="text-primary/20 text-6xl font-bold">{props.step}</span> <div className="bg-primary text-primary-foreground shadow-primary/20 relative z-10 flex h-14 w-14 items-center justify-center rounded-full text-xl font-bold shadow-lg">
{props.step}
</div>
<h3 className="text-secondary-foreground text-xl font-medium"> <h3 className="text-secondary-foreground text-xl font-medium">
<Trans i18nKey={props.titleKey} /> <Trans i18nKey={props.titleKey} />
</h3> </h3>

View File

@@ -1,4 +1,5 @@
import { UserCircle, Plus } from 'lucide-react'; import { UserCircle, Plus } 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';
@@ -16,6 +17,7 @@ interface PageProps {
export default async function GuestsPage({ params }: PageProps) { export default async function GuestsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('bookings');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -25,7 +27,7 @@ export default async function GuestsPage({ params }: PageProps) {
if (!acct) { if (!acct) {
return ( return (
<CmsPageShell account={account} title="Gäste"> <CmsPageShell account={account} title={t('guests.title')}>
<AccountNotFound /> <AccountNotFound />
</CmsPageShell> </CmsPageShell>
); );
@@ -35,38 +37,50 @@ export default async function GuestsPage({ params }: PageProps) {
const guests = await api.listGuests(acct.id); const guests = await api.listGuests(acct.id);
return ( return (
<CmsPageShell account={account} title="Gäste"> <CmsPageShell account={account} title={t('guests.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-muted-foreground">Gästeverwaltung</p> <p className="text-muted-foreground">{t('guests.manage')}</p>
<Button data-test="guests-new-btn"> <Button data-test="guests-new-btn">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Neuer Gast {t('guests.newGuest')}
</Button> </Button>
</div> </div>
{guests.length === 0 ? ( {guests.length === 0 ? (
<EmptyState <EmptyState
icon={<UserCircle className="h-8 w-8" />} icon={<UserCircle className="h-8 w-8" />}
title="Keine Gäste vorhanden" title={t('guests.noGuests')}
description="Legen Sie Ihren ersten Gast an." description={t('guests.addFirst')}
actionLabel="Neuer Gast" actionLabel={t('guests.newGuest')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Gäste ({guests.length})</CardTitle> <CardTitle>
{t('guests.allGuests', { count: guests.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">E-Mail</th> {t('guests.name')}
<th className="p-3 text-left font-medium">Telefon</th> </th>
<th className="p-3 text-left font-medium">Stadt</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">Land</th> {t('guests.email')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('guests.phone')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('guests.city')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('guests.country')}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createBookingManagementApi } from '@kit/booking-management/api'; import { createBookingManagementApi } from '@kit/booking-management/api';
import { CreateBookingForm } from '@kit/booking-management/components'; import { CreateBookingForm } from '@kit/booking-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -11,6 +13,7 @@ interface Props {
export default async function NewBookingPage({ params }: Props) { export default async function NewBookingPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('bookings');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -19,7 +22,7 @@ export default async function NewBookingPage({ params }: Props) {
.single(); .single();
if (!acct) { if (!acct) {
return ( return (
<CmsPageShell account={account} title="Neue Buchung"> <CmsPageShell account={account} title={t('nav.newBooking')}>
<AccountNotFound /> <AccountNotFound />
</CmsPageShell> </CmsPageShell>
); );
@@ -31,8 +34,8 @@ export default async function NewBookingPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neue Buchung" title={t('newBooking.title')}
description="Buchung erstellen" description={t('newBooking.description')}
> >
<CreateBookingForm <CreateBookingForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,6 +1,8 @@
import { BedDouble, Plus } from 'lucide-react'; import { BedDouble, Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createBookingManagementApi } from '@kit/booking-management/api'; import { createBookingManagementApi } from '@kit/booking-management/api';
import { formatCurrencyAmount } 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 { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -17,6 +19,7 @@ interface PageProps {
export default async function RoomsPage({ params }: PageProps) { export default async function RoomsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('bookings');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -26,7 +29,7 @@ export default async function RoomsPage({ params }: PageProps) {
if (!acct) { if (!acct) {
return ( return (
<CmsPageShell account={account} title="Zimmer"> <CmsPageShell account={account} title={t('rooms.title')}>
<AccountNotFound /> <AccountNotFound />
</CmsPageShell> </CmsPageShell>
); );
@@ -36,41 +39,53 @@ export default async function RoomsPage({ params }: PageProps) {
const rooms = await api.listRooms(acct.id); const rooms = await api.listRooms(acct.id);
return ( return (
<CmsPageShell account={account} title="Zimmer"> <CmsPageShell account={account} title={t('rooms.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-muted-foreground">Zimmerverwaltung</p> <p className="text-muted-foreground">{t('rooms.manage')}</p>
<Button data-test="rooms-new-btn"> <Button data-test="rooms-new-btn">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Neues Zimmer {t('rooms.newRoom')}
</Button> </Button>
</div> </div>
{rooms.length === 0 ? ( {rooms.length === 0 ? (
<EmptyState <EmptyState
icon={<BedDouble className="h-8 w-8" />} icon={<BedDouble className="h-8 w-8" />}
title="Keine Zimmer vorhanden" title={t('rooms.noRooms')}
description="Fügen Sie Ihr erstes Zimmer hinzu." description={t('rooms.addFirst')}
actionLabel="Neues Zimmer" actionLabel={t('rooms.newRoom')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Zimmer ({rooms.length})</CardTitle> <CardTitle>
{t('rooms.allRooms', { count: rooms.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Zimmernr.</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">Name</th> {t('rooms.roomNumber')}
<th className="p-3 text-left font-medium">Typ</th> </th>
<th className="p-3 text-right font-medium">Kapazität</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-right font-medium"> {t('rooms.name')}
Preis/Nacht </th>
<th scope="col" className="p-3 text-left font-medium">
{t('rooms.type')}
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('rooms.capacity')}
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('rooms.price')}
</th>
<th scope="col" className="p-3 text-center font-medium">
{t('rooms.active')}
</th> </th>
<th className="p-3 text-center font-medium">Aktiv</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -95,7 +110,9 @@ export default async function RoomsPage({ params }: PageProps) {
</td> </td>
<td className="p-3 text-right"> <td className="p-3 text-right">
{room.price_per_night != null {room.price_per_night != null
? `${Number(room.price_per_night).toFixed(2)}` ? formatCurrencyAmount(
room.price_per_night as number,
)
: '—'} : '—'}
</td> </td>
<td className="p-3 text-center"> <td className="p-3 text-center">

View File

@@ -69,12 +69,16 @@ export function AttendanceGrid({
Keine Teilnehmer in diesem Kurs Keine Teilnehmer in diesem Kurs
</p> </p>
) : ( ) : (
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Teilnehmer</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-center font-medium">Anwesend</th> Teilnehmer
</th>
<th scope="col" className="p-3 text-center font-medium">
Anwesend
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,4 +1,5 @@
import { ClipboardCheck, Calendar } from 'lucide-react'; import { ClipboardCheck, Calendar } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
import { formatDate } from '@kit/shared/dates'; import { formatDate } from '@kit/shared/dates';
@@ -25,6 +26,7 @@ export default async function AttendancePage({
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createCourseManagementApi(client); const api = createCourseManagementApi(client);
const t = await getTranslations('courses');
const [course, sessions, participants] = await Promise.all([ const [course, sessions, participants] = await Promise.all([
api.getCourse(courseId), api.getCourse(courseId),
@@ -58,10 +60,9 @@ export default async function AttendancePage({
})); }));
return ( return (
<CmsPageShell account={account} title="Anwesenheit"> <CmsPageShell account={account} title={t('attendance.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div> <div>
<h1 className="text-2xl font-bold">Anwesenheit</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{String((course as Record<string, unknown>).name)} {String((course as Record<string, unknown>).name)}
</p> </p>
@@ -70,14 +71,14 @@ export default async function AttendancePage({
{sessions.length === 0 ? ( {sessions.length === 0 ? (
<EmptyState <EmptyState
icon={<Calendar className="h-8 w-8" />} icon={<Calendar className="h-8 w-8" />}
title="Keine Termine vorhanden" title={t('attendance.noSessions')}
description="Erstellen Sie zuerst Termine für diesen Kurs." description={t('attendance.noSessionsDescription')}
/> />
) : ( ) : (
<> <>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Termin auswählen</CardTitle> <CardTitle>{t('attendance.selectSession')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@@ -107,7 +108,7 @@ export default async function AttendancePage({
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<ClipboardCheck className="h-5 w-5" /> <ClipboardCheck className="h-5 w-5" />
Anwesenheitsliste {t('attendance.attendanceList')}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -119,7 +120,7 @@ export default async function AttendancePage({
/> />
) : ( ) : (
<p className="text-muted-foreground py-6 text-center text-sm"> <p className="text-muted-foreground py-6 text-center text-sm">
Bitte wählen Sie einen Termin aus {t('attendance.selectSessionPrompt')}
</p> </p>
)} )}
</CardContent> </CardContent>

View File

@@ -39,12 +39,14 @@ export function CreateSessionDialog({ courseId }: Props) {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger
render={
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" aria-hidden="true" />
Neuer Termin Neuer Termin
</Button> </Button>
</DialogTrigger> }
/>
<DialogContent> <DialogContent>
<form <form
onSubmit={(e) => { onSubmit={(e) => {

View File

@@ -35,17 +35,19 @@ export function DeleteCourseButton({ courseId, accountSlug }: Props) {
return ( return (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger
render={
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
disabled={isPending} disabled={isPending}
data-test="course-cancel-btn" data-test="course-cancel-btn"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" aria-hidden="true" />
Kurs absagen Kurs absagen
</Button> </Button>
</AlertDialogTrigger> }
/>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Kurs absagen?</AlertDialogTitle> <AlertDialogTitle>Kurs absagen?</AlertDialogTitle>

View File

@@ -1,6 +1,7 @@
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';
@@ -11,6 +12,7 @@ interface PageProps {
export default async function EditCoursePage({ params }: PageProps) { export default async function EditCoursePage({ params }: PageProps) {
const { account, courseId } = await params; const { account, courseId } = await params;
const t = await getTranslations('courses');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
@@ -28,7 +30,10 @@ export default async function EditCoursePage({ params }: PageProps) {
const c = course as Record<string, unknown>; const c = course as Record<string, unknown>;
return ( return (
<CmsPageShell account={account} title={`${String(c.name)} — Bearbeiten`}> <CmsPageShell
account={account}
title={`${String(c.name)}${t('pages.editCourseTitle')}`}
>
<CreateCourseForm <CreateCourseForm
accountId={acct.id} accountId={acct.id}
account={account} account={account}

View File

@@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Plus, Users } from 'lucide-react'; import { Plus, Users } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
import { formatDate } from '@kit/shared/dates'; import { formatDate } from '@kit/shared/dates';
@@ -27,10 +28,10 @@ const STATUS_VARIANT: Record<
completed: 'outline', completed: 'outline',
}; };
const STATUS_LABEL: Record<string, string> = { const ENROLLMENT_STATUS_LABEL: Record<string, string> = {
enrolled: 'Angemeldet', enrolled: 'Eingeschrieben',
waitlisted: 'Warteliste', waitlisted: 'Warteliste',
cancelled: 'Abgemeldet', cancelled: 'Storniert',
completed: 'Abgeschlossen', completed: 'Abgeschlossen',
}; };
@@ -38,6 +39,7 @@ export default async function ParticipantsPage({ params }: PageProps) {
const { account, courseId } = await params; const { account, courseId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createCourseManagementApi(client); const api = createCourseManagementApi(client);
const t = await getTranslations('courses');
const [course, participants] = await Promise.all([ const [course, participants] = await Promise.all([
api.getCourse(courseId), api.getCourse(courseId),
@@ -47,45 +49,54 @@ export default async function ParticipantsPage({ params }: PageProps) {
if (!course) return <AccountNotFound />; if (!course) return <AccountNotFound />;
return ( return (
<CmsPageShell account={account} title="Teilnehmer"> <CmsPageShell account={account} title={t('participants.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">Teilnehmer</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{String((course as Record<string, unknown>).name)} {' '} {String((course as Record<string, unknown>).name)} {' '}
{participants.length} Teilnehmer {participants.length} {t('participants.title')}
</p> </p>
</div> </div>
<Button data-test="participants-add-btn"> <Button data-test="participants-add-btn">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Teilnehmer anmelden {t('participants.add')}
</Button> </Button>
</div> </div>
{participants.length === 0 ? ( {participants.length === 0 ? (
<EmptyState <EmptyState
icon={<Users className="h-8 w-8" />} icon={<Users className="h-8 w-8" />}
title="Keine Teilnehmer" title={t('participants.none')}
description="Melden Sie den ersten Teilnehmer für diesen Kurs an." description={t('participants.noneDescription')}
actionLabel="Teilnehmer anmelden" actionLabel={t('participants.add')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Teilnehmer ({participants.length})</CardTitle> <CardTitle>
{t('participants.allTitle', { count: participants.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">E-Mail</th> {t('common.name')}
<th className="p-3 text-left font-medium">Telefon</th> </th>
<th className="p-3 text-left font-medium">Status</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium"> {t('common.email')}
Anmeldedatum </th>
<th scope="col" className="p-3 text-left font-medium">
{t('common.phone')}
</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">
{t('enrollment.registrationDate')}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -107,7 +118,8 @@ export default async function ParticipantsPage({ params }: PageProps) {
STATUS_VARIANT[String(p.status)] ?? 'secondary' STATUS_VARIANT[String(p.status)] ?? 'secondary'
} }
> >
{STATUS_LABEL[String(p.status)] ?? String(p.status)} {ENROLLMENT_STATUS_LABEL[String(p.status)] ??
String(p.status)}
</Badge> </Badge>
</td> </td>
<td className="p-3"> <td className="p-3">

View File

@@ -5,6 +5,7 @@ import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { createCategory } from '@kit/course-management/actions/course-actions'; import { createCategory } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -26,6 +27,7 @@ interface CreateCategoryDialogProps {
} }
export function CreateCategoryDialog({ accountId }: CreateCategoryDialogProps) { export function CreateCategoryDialog({ accountId }: CreateCategoryDialogProps) {
const t = useTranslations('courses');
const router = useRouter(); const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(''); const [name, setName] = useState('');
@@ -76,7 +78,7 @@ export function CreateCategoryDialog({ accountId }: CreateCategoryDialogProps) {
id="cat-name" id="cat-name"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="z. B. Sprachkurse" placeholder={t('categories.namePlaceholder')}
required required
minLength={1} minLength={1}
maxLength={128} maxLength={128}
@@ -88,7 +90,7 @@ export function CreateCategoryDialog({ accountId }: CreateCategoryDialogProps) {
id="cat-description" id="cat-description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Kurze Beschreibung" placeholder={t('categories.descriptionPlaceholder')}
/> />
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { FolderTree } from 'lucide-react'; import { FolderTree } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -17,6 +18,7 @@ interface PageProps {
export default async function CategoriesPage({ params }: PageProps) { export default async function CategoriesPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('courses');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -30,36 +32,40 @@ export default async function CategoriesPage({ params }: PageProps) {
const categories = await api.listCategories(acct.id); const categories = await api.listCategories(acct.id);
return ( return (
<CmsPageShell account={account} title="Kategorien"> <CmsPageShell account={account} title={t('pages.categoriesTitle')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-muted-foreground">Kurskategorien verwalten</p> <p className="text-muted-foreground">{t('categories.manage')}</p>
<CreateCategoryDialog accountId={acct.id} /> <CreateCategoryDialog accountId={acct.id} />
</div> </div>
{categories.length === 0 ? ( {categories.length === 0 ? (
<EmptyState <EmptyState
icon={<FolderTree className="h-8 w-8" />} icon={<FolderTree className="h-8 w-8" />}
title="Keine Kategorien vorhanden" title={t('categories.noCategories')}
description="Erstellen Sie Ihre erste Kurskategorie." description={t('categories.manage')}
actionLabel="Neue Kategorie" actionLabel={t('categories.newCategory')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Kategorien ({categories.length})</CardTitle> <CardTitle>
{t('categories.allTitle', { count: categories.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium"> {t('common.name')}
Beschreibung
</th> </th>
<th className="p-3 text-left font-medium"> <th scope="col" className="p-3 text-left font-medium">
Übergeordnet {t('common.description')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('common.parent')}
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@@ -5,6 +5,7 @@ import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { createInstructor } from '@kit/course-management/actions/course-actions'; import { createInstructor } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -29,6 +30,7 @@ interface CreateInstructorDialogProps {
export function CreateInstructorDialog({ export function CreateInstructorDialog({
accountId, accountId,
}: CreateInstructorDialogProps) { }: CreateInstructorDialogProps) {
const t = useTranslations('courses');
const router = useRouter(); const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
@@ -101,7 +103,7 @@ export function CreateInstructorDialog({
id="inst-first-name" id="inst-first-name"
value={firstName} value={firstName}
onChange={(e) => setFirstName(e.target.value)} onChange={(e) => setFirstName(e.target.value)}
placeholder="Vorname" placeholder={t('instructors.firstNamePlaceholder')}
required required
minLength={1} minLength={1}
maxLength={128} maxLength={128}
@@ -113,7 +115,7 @@ export function CreateInstructorDialog({
id="inst-last-name" id="inst-last-name"
value={lastName} value={lastName}
onChange={(e) => setLastName(e.target.value)} onChange={(e) => setLastName(e.target.value)}
placeholder="Nachname" placeholder={t('instructors.lastNamePlaceholder')}
required required
minLength={1} minLength={1}
maxLength={128} maxLength={128}
@@ -150,7 +152,7 @@ export function CreateInstructorDialog({
id="inst-qualifications" id="inst-qualifications"
value={qualifications} value={qualifications}
onChange={(e) => setQualifications(e.target.value)} onChange={(e) => setQualifications(e.target.value)}
placeholder="z. B. Zertifizierter Trainer, Erste-Hilfe-Ausbilder" placeholder={t('instructors.qualificationsPlaceholder')}
rows={3} rows={3}
/> />
</div> </div>

View File

@@ -1,6 +1,8 @@
import { GraduationCap } from 'lucide-react'; import { GraduationCap } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
import { formatCurrencyAmount } from '@kit/shared/dates';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
@@ -17,6 +19,7 @@ interface PageProps {
export default async function InstructorsPage({ params }: PageProps) { export default async function InstructorsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('courses');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -30,38 +33,46 @@ export default async function InstructorsPage({ params }: PageProps) {
const instructors = await api.listInstructors(acct.id); const instructors = await api.listInstructors(acct.id);
return ( return (
<CmsPageShell account={account} title="Dozenten"> <CmsPageShell account={account} title={t('pages.instructorsTitle')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-muted-foreground">Dozentenpool verwalten</p> <p className="text-muted-foreground">{t('instructors.manage')}</p>
<CreateInstructorDialog accountId={acct.id} /> <CreateInstructorDialog accountId={acct.id} />
</div> </div>
{instructors.length === 0 ? ( {instructors.length === 0 ? (
<EmptyState <EmptyState
icon={<GraduationCap className="h-8 w-8" />} icon={<GraduationCap className="h-8 w-8" />}
title="Keine Dozenten vorhanden" title={t('instructors.noInstructors')}
description="Fügen Sie Ihren ersten Dozenten hinzu." description={t('instructors.manage')}
actionLabel="Neuer Dozent" actionLabel={t('instructors.newInstructor')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Dozenten ({instructors.length})</CardTitle> <CardTitle>
{t('instructors.allTitle', { count: instructors.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">E-Mail</th> {t('common.name')}
<th className="p-3 text-left font-medium">Telefon</th>
<th className="p-3 text-left font-medium">
Qualifikation
</th> </th>
<th className="p-3 text-right font-medium"> <th scope="col" className="p-3 text-left font-medium">
Stundensatz {t('common.email')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('common.phone')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('instructors.qualification')}
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('instructors.hourlyRate')}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -82,7 +93,7 @@ export default async function InstructorsPage({ params }: PageProps) {
</td> </td>
<td className="p-3 text-right"> <td className="p-3 text-right">
{inst.hourly_rate != null {inst.hourly_rate != null
? `${Number(inst.hourly_rate).toFixed(2)}` ? formatCurrencyAmount(inst.hourly_rate as number)
: '—'} : '—'}
</td> </td>
</tr> </tr>

View File

@@ -5,6 +5,7 @@ import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { createLocation } from '@kit/course-management/actions/course-actions'; import { createLocation } from '@kit/course-management/actions/course-actions';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -26,6 +27,7 @@ interface CreateLocationDialogProps {
} }
export function CreateLocationDialog({ accountId }: CreateLocationDialogProps) { export function CreateLocationDialog({ accountId }: CreateLocationDialogProps) {
const t = useTranslations('courses');
const router = useRouter(); const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(''); const [name, setName] = useState('');
@@ -82,7 +84,7 @@ export function CreateLocationDialog({ accountId }: CreateLocationDialogProps) {
id="loc-name" id="loc-name"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="z. B. Vereinsheim" placeholder={t('locations.namePlaceholder')}
required required
minLength={1} minLength={1}
maxLength={128} maxLength={128}
@@ -94,7 +96,7 @@ export function CreateLocationDialog({ accountId }: CreateLocationDialogProps) {
id="loc-address" id="loc-address"
value={address} value={address}
onChange={(e) => setAddress(e.target.value)} onChange={(e) => setAddress(e.target.value)}
placeholder="Musterstr. 1, 12345 Musterstadt" placeholder={t('locations.addressPlaceholder')}
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -104,7 +106,7 @@ export function CreateLocationDialog({ accountId }: CreateLocationDialogProps) {
id="loc-room" id="loc-room"
value={room} value={room}
onChange={(e) => setRoom(e.target.value)} onChange={(e) => setRoom(e.target.value)}
placeholder="z. B. Raum 101" placeholder={t('locations.roomPlaceholder')}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -1,4 +1,5 @@
import { MapPin } from 'lucide-react'; import { MapPin } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -17,6 +18,7 @@ interface PageProps {
export default async function LocationsPage({ params }: PageProps) { export default async function LocationsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('courses');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -30,36 +32,44 @@ export default async function LocationsPage({ params }: PageProps) {
const locations = await api.listLocations(acct.id); const locations = await api.listLocations(acct.id);
return ( return (
<CmsPageShell account={account} title="Orte"> <CmsPageShell account={account} title={t('pages.locationsTitle')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-muted-foreground"> <p className="text-muted-foreground">{t('locations.manage')}</p>
Kurs- und Veranstaltungsorte verwalten
</p>
<CreateLocationDialog accountId={acct.id} /> <CreateLocationDialog accountId={acct.id} />
</div> </div>
{locations.length === 0 ? ( {locations.length === 0 ? (
<EmptyState <EmptyState
icon={<MapPin className="h-8 w-8" />} icon={<MapPin className="h-8 w-8" />}
title="Keine Orte vorhanden" title={t('locations.noLocations')}
description="Fügen Sie Ihren ersten Veranstaltungsort hinzu." description={t('locations.noLocationsDescription')}
actionLabel="Neuer Ort" actionLabel={t('locations.newLocationLabel')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Orte ({locations.length})</CardTitle> <CardTitle>
{t('locations.allTitle', { count: locations.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">Adresse</th> {t('common.name')}
<th className="p-3 text-left font-medium">Raum</th> </th>
<th className="p-3 text-right font-medium">Kapazität</th> <th scope="col" className="p-3 text-left font-medium">
{t('common.address')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('common.room')}
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('list.capacity')}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
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';
@@ -11,6 +13,8 @@ interface Props {
export default async function NewCoursePage({ params }: Props) { export default async function NewCoursePage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('courses');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -21,8 +25,8 @@ export default async function NewCoursePage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neuer Kurs" title={t('pages.newCourseTitle')}
description="Kurs anlegen" description={t('pages.newCourseDescription')}
> >
<CreateCourseForm accountId={acct.id} account={account} /> <CreateCourseForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -5,6 +5,7 @@ import {
TrendingUp, TrendingUp,
BarChart3, BarChart3,
} from 'lucide-react'; } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -22,6 +23,7 @@ interface PageProps {
export default async function CourseStatisticsPage({ params }: PageProps) { export default async function CourseStatisticsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('courses');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -34,32 +36,32 @@ export default async function CourseStatisticsPage({ params }: PageProps) {
const stats = await api.getStatistics(acct.id); const stats = await api.getStatistics(acct.id);
const statusChartData = [ const statusChartData = [
{ name: 'Aktiv', value: stats.openCourses }, { name: t('stats.active'), value: stats.openCourses },
{ name: 'Abgeschlossen', value: stats.completedCourses }, { name: t('stats.completed'), value: stats.completedCourses },
{ name: 'Gesamt', value: stats.totalCourses }, { name: t('stats.total'), value: stats.totalCourses },
]; ];
return ( return (
<CmsPageShell account={account} title="Kurs-Statistiken"> <CmsPageShell account={account} title={t('pages.statisticsTitle')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<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">
<StatsCard <StatsCard
title="Kurse gesamt" title={t('stats.totalCourses')}
value={stats.totalCourses} value={stats.totalCourses}
icon={<GraduationCap className="h-5 w-5" />} icon={<GraduationCap className="h-5 w-5" />}
/> />
<StatsCard <StatsCard
title="Aktive Kurse" title={t('stats.activeCourses')}
value={stats.openCourses} value={stats.openCourses}
icon={<Calendar className="h-5 w-5" />} icon={<Calendar className="h-5 w-5" />}
/> />
<StatsCard <StatsCard
title="Teilnehmer" title={t('stats.participants')}
value={stats.totalParticipants} value={stats.totalParticipants}
icon={<Users className="h-5 w-5" />} icon={<Users className="h-5 w-5" />}
/> />
<StatsCard <StatsCard
title="Abgeschlossen" title={t('stats.completed')}
value={stats.completedCourses} value={stats.completedCourses}
icon={<TrendingUp className="h-5 w-5" />} icon={<TrendingUp className="h-5 w-5" />}
/> />
@@ -70,7 +72,7 @@ export default async function CourseStatisticsPage({ params }: PageProps) {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" /> <BarChart3 className="h-5 w-5" />
Kursauslastung {t('stats.utilization')}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -82,7 +84,7 @@ export default async function CourseStatisticsPage({ params }: PageProps) {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" /> <TrendingUp className="h-5 w-5" />
Verteilung {t('stats.distribution')}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@@ -2,9 +2,12 @@
import React from 'react'; import React from 'react';
import type { SupabaseClient } from '@supabase/supabase-js';
import { createDocumentGeneratorApi } from '@kit/document-generator/api'; import { createDocumentGeneratorApi } from '@kit/document-generator/api';
import { formatDate } from '@kit/shared/dates'; import { formatDate } from '@kit/shared/dates';
import { getLogger } from '@kit/shared/logger'; import { getLogger } from '@kit/shared/logger';
import type { Database } from '@kit/supabase/database';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
export type GenerateDocumentInput = { export type GenerateDocumentInput = {
@@ -86,7 +89,7 @@ function fmtDate(d: string | null): string {
// Member Card PDF — premium design with color accent bar, structured layout // Member Card PDF — premium design with color accent bar, structured layout
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
async function generateMemberCards( async function generateMemberCards(
client: ReturnType<typeof getSupabaseServerClient>, client: SupabaseClient<Database>,
accountId: string, accountId: string,
accountName: string, accountName: string,
input: GenerateDocumentInput, input: GenerateDocumentInput,
@@ -413,7 +416,7 @@ async function generateMemberCards(
// Address Labels (HTML — Avery L7163) // Address Labels (HTML — Avery L7163)
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
async function generateLabels( async function generateLabels(
client: ReturnType<typeof getSupabaseServerClient>, client: SupabaseClient<Database>,
accountId: string, accountId: string,
input: GenerateDocumentInput, input: GenerateDocumentInput,
): Promise<GenerateDocumentResult> { ): Promise<GenerateDocumentResult> {
@@ -461,7 +464,7 @@ async function generateLabels(
// Member Report (Excel) // Member Report (Excel)
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
async function generateMemberReport( async function generateMemberReport(
client: ReturnType<typeof getSupabaseServerClient>, client: SupabaseClient<Database>,
accountId: string, accountId: string,
input: GenerateDocumentInput, input: GenerateDocumentInput,
): Promise<GenerateDocumentResult> { ): Promise<GenerateDocumentResult> {

View File

@@ -1,6 +1,7 @@
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 { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -23,15 +24,6 @@ interface PageProps {
searchParams: Promise<{ type?: string }>; searchParams: Promise<{ type?: string }>;
} }
const DOCUMENT_LABELS: Record<string, string> = {
'member-card': 'Mitgliedsausweis',
invoice: 'Rechnung',
labels: 'Etiketten',
report: 'Bericht',
letter: 'Brief',
certificate: 'Zertifikat',
};
export default async function GenerateDocumentPage({ export default async function GenerateDocumentPage({
params, params,
searchParams, searchParams,
@@ -39,6 +31,7 @@ export default async function GenerateDocumentPage({
const { account } = await params; const { account } = await params;
const { type } = await searchParams; const { type } = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('documents');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -49,10 +42,13 @@ export default async function GenerateDocumentPage({
if (!acct) return <AccountNotFound />; if (!acct) return <AccountNotFound />;
const selectedType = type ?? 'member-card'; const selectedType = type ?? 'member-card';
const selectedLabel = DOCUMENT_LABELS[selectedType] ?? 'Dokument'; // Resolve the label from translations; fall back to generic 'Document'
const selectedLabel =
(t.raw(`types.${selectedType}`) as string | undefined) ??
t('generate.document');
return ( return (
<CmsPageShell account={account} title="Dokument generieren"> <CmsPageShell account={account} title={t('generate.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
{/* Back link */} {/* Back link */}
<div> <div>
@@ -61,16 +57,16 @@ export default async function GenerateDocumentPage({
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm" className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
> >
<ArrowLeft className="mr-1 h-4 w-4" /> <ArrowLeft className="mr-1 h-4 w-4" />
Zurück zu Dokumente {t('generate.backToDocuments')}
</Link> </Link>
</div> </div>
<Card className="max-w-2xl"> <Card className="max-w-2xl">
<CardHeader> <CardHeader>
<CardTitle>{selectedLabel} generieren</CardTitle> <CardTitle>
<CardDescription> {t('generate.generateLabel', { label: selectedLabel })}
Wählen Sie den Dokumenttyp und die gewünschten Optionen. </CardTitle>
</CardDescription> <CardDescription>{t('generate.chooseOptions')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -82,7 +78,7 @@ export default async function GenerateDocumentPage({
<CardFooter> <CardFooter>
<Link href={`/home/${account}/documents`}> <Link href={`/home/${account}/documents`}>
<Button variant="outline">Abbrechen</Button> <Button variant="outline">{t('generate.cancel')}</Button>
</Link> </Link>
</CardFooter> </CardFooter>
</Card> </Card>

View File

@@ -8,6 +8,7 @@ import {
Mail, Mail,
Award, Award,
} from 'lucide-react'; } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -23,45 +24,31 @@ interface PageProps {
const DOCUMENT_TYPES = [ const DOCUMENT_TYPES = [
{ {
id: 'member-card', id: 'member-card',
title: 'Mitgliedsausweis',
description:
'Mitgliedsausweise mit Foto, Name und Mitgliedsnummer generieren.',
icon: CreditCard, icon: CreditCard,
color: 'text-blue-600 bg-blue-50', color: 'text-blue-600 bg-blue-50',
}, },
{ {
id: 'invoice', id: 'invoice',
title: 'Rechnung',
description:
'Professionelle Rechnungen im PDF-Format mit Logo und Positionen.',
icon: FileText, icon: FileText,
color: 'text-green-600 bg-green-50', color: 'text-green-600 bg-green-50',
}, },
{ {
id: 'labels', id: 'labels',
title: 'Etiketten',
description: 'Adressetiketten für Serienbriefe im Avery-Format drucken.',
icon: Tag, icon: Tag,
color: 'text-orange-600 bg-orange-50', color: 'text-orange-600 bg-orange-50',
}, },
{ {
id: 'report', id: 'report',
title: 'Bericht',
description: 'Statistische Auswertungen und Berichte als PDF oder Excel.',
icon: BarChart3, icon: BarChart3,
color: 'text-purple-600 bg-purple-50', color: 'text-purple-600 bg-purple-50',
}, },
{ {
id: 'letter', id: 'letter',
title: 'Brief',
description: 'Serienbriefe mit personalisierten Platzhaltern erstellen.',
icon: Mail, icon: Mail,
color: 'text-rose-600 bg-rose-50', color: 'text-rose-600 bg-rose-50',
}, },
{ {
id: 'certificate', id: 'certificate',
title: 'Zertifikat',
description: 'Teilnahmebescheinigungen und Zertifikate mit Unterschrift.',
icon: Award, icon: Award,
color: 'text-amber-600 bg-amber-50', color: 'text-amber-600 bg-amber-50',
}, },
@@ -70,6 +57,7 @@ const DOCUMENT_TYPES = [
export default async function DocumentsPage({ params }: PageProps) { export default async function DocumentsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('documents');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -82,14 +70,14 @@ export default async function DocumentsPage({ params }: PageProps) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Dokumente" title={t('overview.title')}
description="Dokumente erstellen und verwalten" description={t('overview.subtitle')}
> >
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
{/* Actions */} {/* Actions */}
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<Link href={`/home/${account}/documents/templates`}> <Link href={`/home/${account}/documents/templates`}>
<Button variant="outline">Vorlagen verwalten</Button> <Button variant="outline">{t('overview.manageTemplates')}</Button>
</Link> </Link>
</div> </div>
@@ -104,18 +92,20 @@ export default async function DocumentsPage({ params }: PageProps) {
<Icon className="h-6 w-6" /> <Icon className="h-6 w-6" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<CardTitle className="text-base">{docType.title}</CardTitle> <CardTitle className="text-base">
{t(`types.${docType.id}`)}
</CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-1 flex-col justify-between gap-4"> <CardContent className="flex flex-1 flex-col justify-between gap-4">
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{docType.description} {t(`typeDescriptions.${docType.id}`)}
</p> </p>
<Link <Link
href={`/home/${account}/documents/generate?type=${docType.id}`} href={`/home/${account}/documents/generate?type=${docType.id}`}
> >
<Button variant="outline" size="sm" className="w-full"> <Button variant="outline" size="sm" className="w-full">
Erstellen {t('overview.generate')}
</Button> </Button>
</Link> </Link>
</CardContent> </CardContent>

View File

@@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { FileText, Plus } from 'lucide-react'; import { FileText, Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -17,6 +18,7 @@ interface PageProps {
export default async function DocumentTemplatesPage({ params }: PageProps) { export default async function DocumentTemplatesPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('documents');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -35,20 +37,17 @@ export default async function DocumentTemplatesPage({ params }: PageProps) {
}> = []; }> = [];
return ( return (
<CmsPageShell account={account} title="Dokumentvorlagen"> <CmsPageShell account={account} title={t('templates.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">Dokumentvorlagen</h1> <p className="text-muted-foreground">{t('templates.subtitle')}</p>
<p className="text-muted-foreground">
Vorlagen für Mitgliedsausweise, Rechnungen, Etiketten und mehr
</p>
</div> </div>
<Button data-test="document-templates-new-btn"> <Button data-test="document-templates-new-btn">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Neue Vorlage {t('templates.newTemplate')}
</Button> </Button>
</div> </div>
@@ -56,24 +55,30 @@ export default async function DocumentTemplatesPage({ params }: PageProps) {
{templates.length === 0 ? ( {templates.length === 0 ? (
<EmptyState <EmptyState
icon={<FileText className="h-8 w-8" />} icon={<FileText className="h-8 w-8" />}
title="Keine Vorlagen vorhanden" title={t('templates.noTemplates')}
description="Erstellen Sie Ihre erste Dokumentvorlage, um Mitgliedsausweise, Rechnungen und mehr zu generieren." description={t('templates.createFirstLong')}
actionLabel="Neue Vorlage" actionLabel={t('templates.newTemplate')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Vorlagen ({templates.length})</CardTitle> <CardTitle>
{t('templates.allTemplates', { count: templates.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">Typ</th> {t('templates.name')}
<th className="p-3 text-left font-medium"> </th>
Beschreibung <th scope="col" className="p-3 text-left font-medium">
{t('templates.type')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('templates.description')}
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@@ -35,12 +35,14 @@ export function DeleteEventButton({ eventId, accountSlug }: Props) {
return ( return (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger
render={
<Button variant="destructive" size="sm" disabled={isPending}> <Button variant="destructive" size="sm" disabled={isPending}>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" aria-hidden="true" />
Absagen Absagen
</Button> </Button>
</AlertDialogTrigger> }
/>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Veranstaltung absagen?</AlertDialogTitle> <AlertDialogTitle>Veranstaltung absagen?</AlertDialogTitle>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createEventManagementApi } from '@kit/event-management/api'; import { createEventManagementApi } from '@kit/event-management/api';
import { CreateEventForm } from '@kit/event-management/components'; import { CreateEventForm } from '@kit/event-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -12,6 +14,7 @@ interface PageProps {
export default async function EditEventPage({ params }: PageProps) { export default async function EditEventPage({ params }: PageProps) {
const { account, eventId } = await params; const { account, eventId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('cms.events');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -28,7 +31,10 @@ export default async function EditEventPage({ params }: PageProps) {
const e = event as Record<string, unknown>; const e = event as Record<string, unknown>;
return ( return (
<CmsPageShell account={account} title={`${String(e.name)} — Bearbeiten`}> <CmsPageShell
account={account}
title={`${String(e.name)}${t('editTitle')}`}
>
<CreateEventForm <CreateEventForm
accountId={acct.id} accountId={acct.id}
account={account} account={account}

View File

@@ -2,7 +2,7 @@ import { Ticket, Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { createEventManagementApi } from '@kit/event-management/api'; import { createEventManagementApi } from '@kit/event-management/api';
import { formatDate } from '@kit/shared/dates'; import { formatDate, formatCurrencyAmount } from '@kit/shared/dates';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
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';
@@ -36,7 +36,6 @@ export default async function HolidayPassesPage({ params }: PageProps) {
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">{t('holidayPasses')}</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{t('holidayPassesDescription')} {t('holidayPassesDescription')}
</p> </p>
@@ -62,19 +61,23 @@ export default async function HolidayPassesPage({ params }: PageProps) {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">{t('name')}</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">{t('year')}</th> {t('name')}
<th className="p-3 text-right font-medium"> </th>
<th scope="col" className="p-3 text-left font-medium">
{t('year')}
</th>
<th scope="col" className="p-3 text-right font-medium">
{t('price')} {t('price')}
</th> </th>
<th className="p-3 text-left font-medium"> <th scope="col" className="p-3 text-left font-medium">
{t('validFrom')} {t('validFrom')}
</th> </th>
<th className="p-3 text-left font-medium"> <th scope="col" className="p-3 text-left font-medium">
{t('validUntil')} {t('validUntil')}
</th> </th>
</tr> </tr>
@@ -89,7 +92,7 @@ export default async function HolidayPassesPage({ params }: PageProps) {
<td className="p-3">{String(pass.year ?? '—')}</td> <td className="p-3">{String(pass.year ?? '—')}</td>
<td className="p-3 text-right"> <td className="p-3 text-right">
{pass.price != null {pass.price != null
? `${Number(pass.price).toFixed(2)}` ? formatCurrencyAmount(pass.price as number)
: '—'} : '—'}
</td> </td>
<td className="p-3"> <td className="p-3">

View File

@@ -5,6 +5,7 @@ import { useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { Download, FileIcon } from 'lucide-react'; import { Download, FileIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { deleteFile } from '@kit/module-builder/actions/file-actions'; import { deleteFile } from '@kit/module-builder/actions/file-actions';
import { Badge } from '@kit/ui/badge'; import { Badge } from '@kit/ui/badge';
@@ -67,6 +68,7 @@ function getMimeLabel(mimeType: string): string {
} }
export function FilesTable({ files, pagination }: FilesTableProps) { export function FilesTable({ files, pagination }: FilesTableProps) {
const t = useTranslations('common');
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { total, page, pageSize } = pagination; const { total, page, pageSize } = pagination;
@@ -104,15 +106,25 @@ export function FilesTable({ files, pagination }: FilesTableProps) {
</p> </p>
</div> </div>
) : ( ) : (
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Dateiname</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">Typ</th> Dateiname
<th className="p-3 text-right font-medium">Größe</th> </th>
<th className="p-3 text-left font-medium">Hochgeladen</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-right font-medium">Aktionen</th> Typ
</th>
<th scope="col" className="p-3 text-right font-medium">
Größe
</th>
<th scope="col" className="p-3 text-left font-medium">
Hochgeladen
</th>
<th scope="col" className="p-3 text-right font-medium">
Aktionen
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -149,8 +161,8 @@ export function FilesTable({ files, pagination }: FilesTableProps) {
</Button> </Button>
</a> </a>
<DeleteConfirmButton <DeleteConfirmButton
title="Datei löschen" title={t('deleteFile')}
description="Möchten Sie diese Datei wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden." description={t('deleteFileConfirm')}
isPending={isDeleting} isPending={isDeleting}
onConfirm={() => executeDelete({ fileId: file.id })} onConfirm={() => executeDelete({ fileId: file.id })}
/> />

View File

@@ -1,3 +1,5 @@
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 { ListToolbar } from '@kit/ui/list-toolbar'; import { ListToolbar } from '@kit/ui/list-toolbar';
@@ -15,6 +17,7 @@ interface Props {
export default async function FilesPage({ params, searchParams }: Props) { export default async function FilesPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('common');
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
@@ -51,12 +54,12 @@ export default async function FilesPage({ params, searchParams }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Dateien" title={t('filesTitle')}
description="Dateien hochladen und verwalten" description={t('filesSubtitle')}
> >
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<ListToolbar searchPlaceholder="Datei suchen..." /> <ListToolbar searchPlaceholder={t('filesSearch')} />
<FileUploadDialog accountId={acct.id} /> <FileUploadDialog accountId={acct.id} />
</div> </div>

View File

@@ -0,0 +1,141 @@
'use client';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { CheckCircle, Send } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
interface SendInvoiceButtonProps {
invoiceId: string;
accountId: string;
}
export function SendInvoiceButton({
invoiceId,
accountId,
}: SendInvoiceButtonProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleSend = () => {
startTransition(async () => {
try {
const response = await fetch(
`/api/finance/invoices/${invoiceId}/send`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId }),
},
);
if (response.ok) {
router.refresh();
}
} catch (error) {
console.error('Failed to send invoice:', error);
}
});
};
return (
<AlertDialog>
<AlertDialogTrigger
render={
<Button disabled={isPending}>
<Send className="mr-2 h-4 w-4" aria-hidden="true" />
Senden
</Button>
}
/>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Rechnung senden?</AlertDialogTitle>
<AlertDialogDescription>
Diese Aktion kann nicht rückgängig gemacht werden. Die Rechnung wird
an den Empfänger gesendet und der Status auf Versendet" gesetzt.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
<AlertDialogAction onClick={handleSend}>
{isPending ? 'Wird gesendet...' : 'Senden'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
interface MarkPaidButtonProps {
invoiceId: string;
accountId: string;
}
export function MarkPaidButton({ invoiceId, accountId }: MarkPaidButtonProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleMarkPaid = () => {
startTransition(async () => {
try {
const response = await fetch(
`/api/finance/invoices/${invoiceId}/mark-paid`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId }),
},
);
if (response.ok) {
router.refresh();
}
} catch (error) {
console.error('Failed to mark invoice as paid:', error);
}
});
};
return (
<AlertDialog>
<AlertDialogTrigger
render={
<Button variant="outline" disabled={isPending}>
<CheckCircle className="mr-2 h-4 w-4" aria-hidden="true" />
Bezahlt markieren
</Button>
}
/>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Als bezahlt markieren?</AlertDialogTitle>
<AlertDialogDescription>
Der Status der Rechnung wird auf „Bezahlt" gesetzt. Diese Aktion
bestätigt den Zahlungseingang.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
<AlertDialogAction onClick={handleMarkPaid}>
{isPending ? 'Wird gespeichert...' : 'Bestätigen'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { CreateInvoiceForm } from '@kit/finance/components'; import { CreateInvoiceForm } from '@kit/finance/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -10,6 +12,7 @@ interface Props {
export default async function NewInvoicePage({ params }: Props) { export default async function NewInvoicePage({ params }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('finance');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -21,8 +24,8 @@ export default async function NewInvoicePage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neue Rechnung" title={t('invoices.newInvoice')}
description="Rechnung mit Positionen erstellen" description={t('invoices.newInvoiceDesc')}
> >
<CreateInvoiceForm accountId={acct.id} account={account} /> <CreateInvoiceForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -7,6 +7,7 @@ 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';
@@ -23,6 +24,7 @@ const formatCurrency = (amount: number) =>
export default async function PaymentsPage({ params }: PageProps) { export default async function PaymentsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('finance');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
@@ -78,41 +80,40 @@ export default async function PaymentsPage({ params }: PageProps) {
); );
return ( return (
<CmsPageShell account={account} title="Zahlungen"> <CmsPageShell account={account} title={t('payments.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-2xl font-bold">Zahlungsübersicht</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Zusammenfassung aller Zahlungen und offenen Beträge {t('payments.subtitle')}
</p> </p>
</div> </div>
{/* Stats */} {/* Stats */}
<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">
<StatsCard <StatsCard
title="Bezahlt" title={t('payments.statPaid')}
value={formatCurrency(paidTotal)} value={formatCurrency(paidTotal)}
icon={<Euro className="h-5 w-5" />} icon={<Euro className="h-5 w-5" />}
description={`${paidInvoices.length} Rechnungen`} description={`${paidInvoices.length} ${t('payments.paidInvoices')}`}
/> />
<StatsCard <StatsCard
title="Offen" title={t('payments.statOpen')}
value={formatCurrency(openTotal)} value={formatCurrency(openTotal)}
icon={<CreditCard className="h-5 w-5" />} icon={<CreditCard className="h-5 w-5" />}
description={`${openInvoices.length} Rechnungen`} description={`${openInvoices.length} ${t('invoices.title')}`}
/> />
<StatsCard <StatsCard
title="Überfällig" title={t('payments.statOverdue')}
value={formatCurrency(overdueTotal)} value={formatCurrency(overdueTotal)}
icon={<TrendingUp className="h-5 w-5" />} icon={<TrendingUp className="h-5 w-5" />}
description={`${overdueInvoices.length} Rechnungen`} description={`${overdueInvoices.length} ${t('invoices.title')}`}
/> />
<StatsCard <StatsCard
title="SEPA-Einzüge" title={t('payments.sepaBatches')}
value={formatCurrency(sepaTotal)} value={formatCurrency(sepaTotal)}
icon={<Euro className="h-5 w-5" />} icon={<Euro className="h-5 w-5" />}
description={`${batches.length} Einzüge`} description={`${batches.length} ${t('payments.batchUnit')}`}
/> />
</div> </div>
@@ -120,7 +121,7 @@ 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">Offene Rechnungen</CardTitle> <CardTitle className="text-base">{t('payments.openInvoices')}</CardTitle>
<Badge <Badge
variant={openInvoices.length > 0 ? 'default' : 'secondary'} variant={openInvoices.length > 0 ? 'default' : 'secondary'}
> >
@@ -130,21 +131,21 @@ 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
? `${openInvoices.length} Rechnungen mit einem Gesamtbetrag von ${formatCurrency(openTotal)} sind offen.` ? t('payments.invoicesOpenSummary', { count: openInvoices.length, total: formatCurrency(openTotal) })
: 'Keine offenen Rechnungen vorhanden.'} : t('payments.noOpenInvoices')}
</p> </p>
<Button variant="outline" size="sm" asChild>
<Link href={`/home/${account}/finance/invoices`}> <Link href={`/home/${account}/finance/invoices`}>
<Button variant="outline" size="sm"> {t('payments.viewInvoices')}
Rechnungen anzeigen
<ArrowRight className="ml-2 h-4 w-4" /> <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link> </Link>
</Button>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">SEPA-Einzüge</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>
@@ -152,15 +153,15 @@ 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
? `${batches.length} SEPA-Einzüge mit einem Gesamtvolumen von ${formatCurrency(sepaTotal)}.` ? t('payments.batchSummary', { count: batches.length, total: formatCurrency(sepaTotal) })
: 'Keine SEPA-Einzüge vorhanden.'} : t('payments.noBatchesFound')}
</p> </p>
<Button variant="outline" size="sm" asChild>
<Link href={`/home/${account}/finance/sepa`}> <Link href={`/home/${account}/finance/sepa`}>
<Button variant="outline" size="sm"> {t('payments.viewBatches')}
Einzüge anzeigen
<ArrowRight className="ml-2 h-4 w-4" /> <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link> </Link>
</Button>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { CreateSepaBatchForm } from '@kit/finance/components'; import { CreateSepaBatchForm } from '@kit/finance/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -10,6 +12,7 @@ interface Props {
export default async function NewSepaBatchPage({ params }: Props) { export default async function NewSepaBatchPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('finance');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
@@ -23,8 +26,8 @@ export default async function NewSepaBatchPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neuer SEPA-Einzug" title={t('sepa.newBatch')}
description="SEPA-Lastschrifteinzug erstellen" description={t('sepa.newBatchDesc')}
> >
<CreateSepaBatchForm accountId={acct.id} account={account} /> <CreateSepaBatchForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -18,6 +20,7 @@ export default async function CatchBooksPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -48,7 +51,7 @@ export default async function CatchBooksPage({ params, searchParams }: Props) {
}); });
return ( return (
<CmsPageShell account={account} title="Fischerei - Fangbücher"> <CmsPageShell account={account} title={t('pages.catchBooksTitle')}>
<FischereiTabNavigation account={account} activeTab="catch-books" /> <FischereiTabNavigation account={account} activeTab="catch-books" />
<ListToolbar <ListToolbar
searchPlaceholder="Mitglied suchen..." searchPlaceholder="Mitglied suchen..."

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -15,6 +17,7 @@ interface Props {
export default async function NewCompetitionPage({ params }: Props) { export default async function NewCompetitionPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -33,7 +36,7 @@ export default async function NewCompetitionPage({ params }: Props) {
})); }));
return ( return (
<CmsPageShell account={account} title="Neuer Wettbewerb"> <CmsPageShell account={account} title={t('pages.newCompetitionTitle')}>
<FischereiTabNavigation account={account} activeTab="competitions" /> <FischereiTabNavigation account={account} activeTab="competitions" />
<CreateCompetitionForm <CreateCompetitionForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -21,6 +23,7 @@ export default async function CompetitionsPage({
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -50,7 +53,7 @@ export default async function CompetitionsPage({
}); });
return ( return (
<CmsPageShell account={account} title="Fischerei - Wettbewerbe"> <CmsPageShell account={account} title={t('pages.competitionsTitle')}>
<FischereiTabNavigation account={account} activeTab="competitions" /> <FischereiTabNavigation account={account} activeTab="competitions" />
<ListToolbar <ListToolbar
showSearch={false} showSearch={false}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -15,6 +17,7 @@ interface Props {
export default async function NewLeasePage({ params }: Props) { export default async function NewLeasePage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -33,7 +36,7 @@ export default async function NewLeasePage({ params }: Props) {
})); }));
return ( return (
<CmsPageShell account={account} title="Neue Pacht"> <CmsPageShell account={account} title={t('pages.newLeaseTitle')}>
<FischereiTabNavigation account={account} activeTab="leases" /> <FischereiTabNavigation account={account} activeTab="leases" />
<CreateLeaseForm accountId={acct.id} account={account} waters={waters} /> <CreateLeaseForm accountId={acct.id} account={account} waters={waters} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
@@ -23,6 +24,7 @@ export default async function LeasesPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -58,22 +60,21 @@ export default async function LeasesPage({ params, searchParams }: Props) {
]; ];
return ( return (
<CmsPageShell account={account} title="Fischerei - Pachten"> <CmsPageShell account={account} title={t('pages.leasesTitle')}>
<FischereiTabNavigation account={account} activeTab="leases" /> <FischereiTabNavigation account={account} activeTab="leases" />
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">Pachten</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Gewässerpachtverträge verwalten Gewässerpachtverträge verwalten
</p> </p>
</div> </div>
<Button size="sm" data-test="leases-new-btn" asChild>
<Link href={`/home/${account}/fischerei/leases/new`}> <Link href={`/home/${account}/fischerei/leases/new`}>
<Button size="sm" data-test="leases-new-btn">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Neue Pacht Neue Pacht
</Button>
</Link> </Link>
</Button>
</div> </div>
<ListToolbar <ListToolbar
showSearch={false} showSearch={false}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -15,6 +17,7 @@ interface PageProps {
export default async function FischereiPage({ params }: PageProps) { export default async function FischereiPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -28,7 +31,7 @@ export default async function FischereiPage({ params }: PageProps) {
const stats = await api.getDashboardStats(acct.id); const stats = await api.getDashboardStats(acct.id);
return ( return (
<CmsPageShell account={account} title="Fischerei"> <CmsPageShell account={account} title={t('pages.overviewTitle')}>
<FischereiTabNavigation account={account} activeTab="overview" /> <FischereiTabNavigation account={account} activeTab="overview" />
<FischereiDashboard stats={stats} account={account} /> <FischereiDashboard stats={stats} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -15,6 +17,7 @@ interface Props {
export default async function NewPermitPage({ params }: Props) { export default async function NewPermitPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -33,7 +36,7 @@ export default async function NewPermitPage({ params }: Props) {
})); }));
return ( return (
<CmsPageShell account={account} title="Neuer Erlaubnisschein"> <CmsPageShell account={account} title={t('pages.newPermitTitle')}>
<FischereiTabNavigation account={account} activeTab="permits" /> <FischereiTabNavigation account={account} activeTab="permits" />
<CreatePermitForm accountId={acct.id} account={account} waters={waters} /> <CreatePermitForm accountId={acct.id} account={account} waters={waters} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
@@ -20,6 +21,7 @@ interface Props {
export default async function PermitsPage({ params }: Props) { export default async function PermitsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -33,22 +35,21 @@ export default async function PermitsPage({ params }: Props) {
const permits = await api.listPermits(acct.id); const permits = await api.listPermits(acct.id);
return ( return (
<CmsPageShell account={account} title="Fischerei - Erlaubnisscheine"> <CmsPageShell account={account} title={t('pages.permitsTitle')}>
<FischereiTabNavigation account={account} activeTab="permits" /> <FischereiTabNavigation account={account} activeTab="permits" />
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">Erlaubnisscheine</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Erlaubnisscheine und Gewässerkarten verwalten Erlaubnisscheine und Gewässerkarten verwalten
</p> </p>
</div> </div>
<Button size="sm" data-test="permits-new-btn" asChild>
<Link href={`/home/${account}/fischerei/permits/new`}> <Link href={`/home/${account}/fischerei/permits/new`}>
<Button size="sm" data-test="permits-new-btn">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Neuer Erlaubnisschein Neuer Erlaubnisschein
</Button>
</Link> </Link>
</Button>
</div> </div>
<PermitsDataTable <PermitsDataTable
data={permits as Array<Record<string, unknown>>} data={permits as Array<Record<string, unknown>>}

View File

@@ -1,5 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -17,6 +19,7 @@ interface Props {
export default async function EditSpeciesPage({ params }: Props) { export default async function EditSpeciesPage({ params }: Props) {
const { account, speciesId } = await params; const { account, speciesId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -37,7 +40,7 @@ export default async function EditSpeciesPage({ params }: Props) {
} }
return ( return (
<CmsPageShell account={account} title="Fischart bearbeiten"> <CmsPageShell account={account} title={t('pages.editSpeciesTitle')}>
<FischereiTabNavigation account={account} activeTab="species" /> <FischereiTabNavigation account={account} activeTab="species" />
<CreateSpeciesForm <CreateSpeciesForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
CreateSpeciesForm, CreateSpeciesForm,
@@ -14,6 +16,7 @@ interface Props {
export default async function NewSpeciesPage({ params }: Props) { export default async function NewSpeciesPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -24,7 +27,7 @@ export default async function NewSpeciesPage({ params }: Props) {
if (!acct) return <AccountNotFound />; if (!acct) return <AccountNotFound />;
return ( return (
<CmsPageShell account={account} title="Neue Fischart"> <CmsPageShell account={account} title={t('pages.newSpeciesTitle')}>
<FischereiTabNavigation account={account} activeTab="species" /> <FischereiTabNavigation account={account} activeTab="species" />
<CreateSpeciesForm accountId={acct.id} account={account} /> <CreateSpeciesForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -18,6 +20,7 @@ export default async function SpeciesPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -41,7 +44,7 @@ export default async function SpeciesPage({ params, searchParams }: Props) {
}); });
return ( return (
<CmsPageShell account={account} title="Fischerei - Fischarten"> <CmsPageShell account={account} title={t('pages.speciesTitle')}>
<FischereiTabNavigation account={account} activeTab="species" /> <FischereiTabNavigation account={account} activeTab="species" />
<ListToolbar <ListToolbar
searchPlaceholder="Fischart suchen..." searchPlaceholder="Fischart suchen..."

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { FischereiTabNavigation } from '@kit/fischerei/components'; import { FischereiTabNavigation } from '@kit/fischerei/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
@@ -12,6 +14,7 @@ interface Props {
export default async function StatisticsPage({ params }: Props) { export default async function StatisticsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -22,11 +25,10 @@ export default async function StatisticsPage({ params }: Props) {
if (!acct) return <AccountNotFound />; if (!acct) return <AccountNotFound />;
return ( return (
<CmsPageShell account={account} title="Fischerei - Statistiken"> <CmsPageShell account={account} title={t('pages.statisticsTitle')}>
<FischereiTabNavigation account={account} activeTab="statistics" /> <FischereiTabNavigation account={account} activeTab="statistics" />
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<div> <div>
<h1 className="text-2xl font-bold">Statistiken</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Fangstatistiken und Auswertungen Fangstatistiken und Auswertungen
</p> </p>

View File

@@ -1,5 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -17,6 +19,7 @@ interface Props {
export default async function EditStockingPage({ params }: Props) { export default async function EditStockingPage({ params }: Props) {
const { account, stockingId } = await params; const { account, stockingId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -53,7 +56,7 @@ export default async function EditStockingPage({ params }: Props) {
})); }));
return ( return (
<CmsPageShell account={account} title="Besatz bearbeiten"> <CmsPageShell account={account} title={t('pages.editStockingTitle')}>
<FischereiTabNavigation account={account} activeTab="stocking" /> <FischereiTabNavigation account={account} activeTab="stocking" />
<CreateStockingForm <CreateStockingForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -15,6 +17,7 @@ interface Props {
export default async function NewStockingPage({ params }: Props) { export default async function NewStockingPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -43,7 +46,7 @@ export default async function NewStockingPage({ params }: Props) {
})); }));
return ( return (
<CmsPageShell account={account} title="Besatz eintragen"> <CmsPageShell account={account} title={t('pages.newStockingTitle')}>
<FischereiTabNavigation account={account} activeTab="stocking" /> <FischereiTabNavigation account={account} activeTab="stocking" />
<CreateStockingForm <CreateStockingForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -18,6 +20,7 @@ export default async function StockingPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -71,7 +74,7 @@ export default async function StockingPage({ params, searchParams }: Props) {
]; ];
return ( return (
<CmsPageShell account={account} title="Fischerei - Besatz"> <CmsPageShell account={account} title={t('pages.stockingTitle')}>
<FischereiTabNavigation account={account} activeTab="stocking" /> <FischereiTabNavigation account={account} activeTab="stocking" />
<ListToolbar <ListToolbar
showSearch={false} showSearch={false}

View File

@@ -1,5 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -17,6 +19,7 @@ interface Props {
export default async function EditWaterPage({ params }: Props) { export default async function EditWaterPage({ params }: Props) {
const { account, waterId } = await params; const { account, waterId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -37,7 +40,7 @@ export default async function EditWaterPage({ params }: Props) {
} }
return ( return (
<CmsPageShell account={account} title="Gewässer bearbeiten"> <CmsPageShell account={account} title={t('pages.editWaterTitle')}>
<FischereiTabNavigation account={account} activeTab="waters" /> <FischereiTabNavigation account={account} activeTab="waters" />
<CreateWaterForm accountId={acct.id} account={account} water={water} /> <CreateWaterForm accountId={acct.id} account={account} water={water} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
CreateWaterForm, CreateWaterForm,
@@ -14,6 +16,7 @@ interface Props {
export default async function NewWaterPage({ params }: Props) { export default async function NewWaterPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -24,7 +27,7 @@ export default async function NewWaterPage({ params }: Props) {
if (!acct) return <AccountNotFound />; if (!acct) return <AccountNotFound />;
return ( return (
<CmsPageShell account={account} title="Neues Gewässer"> <CmsPageShell account={account} title={t('pages.newWaterTitle')}>
<FischereiTabNavigation account={account} activeTab="waters" /> <FischereiTabNavigation account={account} activeTab="waters" />
<CreateWaterForm accountId={acct.id} account={account} /> <CreateWaterForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createFischereiApi } from '@kit/fischerei/api'; import { createFischereiApi } from '@kit/fischerei/api';
import { import {
FischereiTabNavigation, FischereiTabNavigation,
@@ -17,6 +19,7 @@ export default async function WatersPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('fischerei');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -36,7 +39,7 @@ export default async function WatersPage({ params, searchParams }: Props) {
}); });
return ( return (
<CmsPageShell account={account} title="Fischerei - Gewässer"> <CmsPageShell account={account} title={t('pages.watersTitle')}>
<FischereiTabNavigation account={account} activeTab="waters" /> <FischereiTabNavigation account={account} activeTab="waters" />
<WatersDataTable <WatersDataTable
data={result.data} data={result.data}

View File

@@ -169,7 +169,6 @@ async function SidebarLayout({
userId={data.user.id} userId={data.user.id}
accounts={accounts} accounts={accounts}
account={account} account={account}
config={config}
/> />
</div> </div>
</PageMobileNavigation> </PageMobileNavigation>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createMeetingsApi } from '@kit/sitzungsprotokolle/api'; import { createMeetingsApi } from '@kit/sitzungsprotokolle/api';
import { import {
MeetingsTabNavigation, MeetingsTabNavigation,
@@ -15,6 +17,7 @@ interface PageProps {
export default async function MeetingsPage({ params }: PageProps) { export default async function MeetingsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('meetings');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -33,7 +36,7 @@ export default async function MeetingsPage({ params }: PageProps) {
]); ]);
return ( return (
<CmsPageShell account={account} title="Sitzungsprotokolle"> <CmsPageShell account={account} title={t('pages.overviewTitle')}>
<MeetingsTabNavigation account={account} activeTab="overview" /> <MeetingsTabNavigation account={account} activeTab="overview" />
<MeetingsDashboard <MeetingsDashboard
stats={stats} stats={stats}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { import {
MeetingsTabNavigation, MeetingsTabNavigation,
CreateProtocolForm, CreateProtocolForm,
@@ -14,6 +16,7 @@ interface PageProps {
export default async function NewProtocolPage({ params }: PageProps) { export default async function NewProtocolPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('meetings');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -24,11 +27,10 @@ export default async function NewProtocolPage({ params }: PageProps) {
if (!acct) return <AccountNotFound />; if (!acct) return <AccountNotFound />;
return ( return (
<CmsPageShell account={account} title="Sitzungsprotokolle"> <CmsPageShell account={account} title={t('pages.newProtocolTitle')}>
<MeetingsTabNavigation account={account} activeTab="protocols" /> <MeetingsTabNavigation account={account} activeTab="protocols" />
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h1 className="text-2xl font-bold">Neues Protokoll erstellen</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Erstellen Sie ein neues Sitzungsprotokoll mit Tagesordnungspunkten. Erstellen Sie ein neues Sitzungsprotokoll mit Tagesordnungspunkten.
</p> </p>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createMeetingsApi } from '@kit/sitzungsprotokolle/api'; import { createMeetingsApi } from '@kit/sitzungsprotokolle/api';
import { import {
MeetingsTabNavigation, MeetingsTabNavigation,
@@ -20,6 +22,7 @@ export default async function ProtocolsPage({
const { account } = await params; const { account } = await params;
const sp = await searchParams; const sp = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('meetings');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -42,7 +45,7 @@ export default async function ProtocolsPage({
}); });
return ( return (
<CmsPageShell account={account} title="Sitzungsprotokolle"> <CmsPageShell account={account} title={t('pages.protocolsTitle')}>
<MeetingsTabNavigation account={account} activeTab="protocols" /> <MeetingsTabNavigation account={account} activeTab="protocols" />
<ProtocolsDataTable <ProtocolsDataTable
data={result.data} data={result.data}

View File

@@ -1,7 +1,10 @@
import { getTranslations } from 'next-intl/server';
import { createMeetingsApi } from '@kit/sitzungsprotokolle/api'; import { createMeetingsApi } from '@kit/sitzungsprotokolle/api';
import { import {
MeetingsTabNavigation, MeetingsTabNavigation,
OpenTasksView, OpenTasksView,
type OpenTask,
} from '@kit/sitzungsprotokolle/components'; } from '@kit/sitzungsprotokolle/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -17,6 +20,7 @@ export default async function TasksPage({ params, searchParams }: PageProps) {
const { account } = await params; const { account } = await params;
const sp = await searchParams; const sp = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('meetings');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -33,18 +37,17 @@ export default async function TasksPage({ params, searchParams }: PageProps) {
const result = await api.listOpenTasks(acct.id, { page }); const result = await api.listOpenTasks(acct.id, { page });
return ( return (
<CmsPageShell account={account} title="Sitzungsprotokolle"> <CmsPageShell account={account} title={t('pages.tasksTitle')}>
<MeetingsTabNavigation account={account} activeTab="tasks" /> <MeetingsTabNavigation account={account} activeTab="tasks" />
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h1 className="text-2xl font-bold">Offene Aufgaben</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Alle offenen und in Bearbeitung befindlichen Tagesordnungspunkte Alle offenen und in Bearbeitung befindlichen Tagesordnungspunkte
über alle Protokolle. über alle Protokolle.
</p> </p>
</div> </div>
<OpenTasksView <OpenTasksView
data={result.data as any} data={result.data as OpenTask[]}
total={result.total} total={result.total}
page={result.page} page={result.page}
pageSize={result.pageSize} pageSize={result.pageSize}

View File

@@ -1,6 +1,7 @@
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';
@@ -11,6 +12,7 @@ interface Props {
export default async function EditMemberPage({ params }: Props) { export default async function EditMemberPage({ params }: Props) {
const { account, memberId } = await params; const { account, memberId } = await params;
const t = await getTranslations('members');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -21,12 +23,12 @@ export default async function EditMemberPage({ params }: Props) {
const api = createMemberManagementApi(client); const api = createMemberManagementApi(client);
const member = await api.getMember(memberId); const member = await api.getMember(memberId);
if (!member) return <div>Mitglied nicht gefunden</div>; if (!member) return <div>{t('detail.notFound')}</div>;
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title={`${String(member.first_name)} ${String(member.last_name)} bearbeiten`} title={`${String(member.first_name)} ${String(member.last_name)} ${t('form.editTitle')}`}
> >
<EditMemberForm member={member} account={account} accountId={acct.id} /> <EditMemberForm member={member} account={account} accountId={acct.id} />
</CmsPageShell> </CmsPageShell>

View File

@@ -21,7 +21,7 @@ export default async function MemberDetailPage({ params }: Props) {
const api = createMemberManagementApi(client); const api = createMemberManagementApi(client);
const member = await api.getMember(memberId); const member = await api.getMember(memberId);
if (!member) return <div>Mitglied nicht gefunden</div>; if (!member) return <AccountNotFound />;
// Fetch sub-entities in parallel // Fetch sub-entities in parallel
const [roles, honors, mandates] = await Promise.all([ const [roles, honors, mandates] = await Promise.all([

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { ApplicationWorkflow } from '@kit/member-management/components'; import { ApplicationWorkflow } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -12,6 +14,7 @@ interface Props {
export default async function ApplicationsPage({ params }: Props) { export default async function ApplicationsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -25,8 +28,8 @@ export default async function ApplicationsPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Aufnahmeanträge" title={t('nav.applications')}
description="Mitgliedsanträge bearbeiten" description={t('applications.subtitle')}
> >
<ApplicationWorkflow <ApplicationWorkflow
applications={applications} applications={applications}

View File

@@ -1,4 +1,5 @@
import { CreditCard } from 'lucide-react'; import { CreditCard } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -17,6 +18,7 @@ interface Props {
export default async function MemberCardsPage({ params }: Props) { export default async function MemberCardsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -34,23 +36,23 @@ export default async function MemberCardsPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Mitgliedsausweise" title={t('cards.title')}
description="Ausweise erstellen und verwalten" description={t('cards.subtitle')}
> >
{members.length === 0 ? ( {members.length === 0 ? (
<EmptyState <EmptyState
icon={<CreditCard className="h-8 w-8" />} icon={<CreditCard className="h-8 w-8" />}
title="Keine aktiven Mitglieder" title={t('cards.noMembers')}
description="Erstellen Sie zuerst Mitglieder, um Ausweise zu generieren." description={t('cards.noMembersDesc')}
actionLabel="Mitglieder verwalten" actionLabel={t('nav.members')}
actionHref={`/home/${account}/members-cms`} actionHref={`/home/${account}/members-cms`}
/> />
) : ( ) : (
<EmptyState <EmptyState
icon={<CreditCard className="h-8 w-8" />} icon={<CreditCard className="h-8 w-8" />}
title="Feature in Entwicklung" title={t('cards.inDevelopment')}
description={`Die Ausweiserstellung für ${members.length} aktive Mitglieder wird derzeit entwickelt. Diese Funktion wird in einem kommenden Update verfügbar sein.`} description={t('cards.inDevelopmentDesc', { count: result.total })}
actionLabel="Mitglieder verwalten" actionLabel={t('nav.members')}
actionHref={`/home/${account}/members-cms`} actionHref={`/home/${account}/members-cms`}
/> />
)} )}

View File

@@ -5,6 +5,7 @@ import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { createDepartment } from '@kit/member-management/actions/member-actions'; import { createDepartment } from '@kit/member-management/actions/member-actions';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -28,14 +29,15 @@ interface CreateDepartmentDialogProps {
export function CreateDepartmentDialog({ export function CreateDepartmentDialog({
accountId, accountId,
}: CreateDepartmentDialogProps) { }: CreateDepartmentDialogProps) {
const t = useTranslations('members.departments');
const router = useRouter(); const router = useRouter();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const { execute, isPending } = useActionWithToast(createDepartment, { const { execute, isPending } = useActionWithToast(createDepartment, {
successMessage: 'Abteilung erstellt', successMessage: t('created'),
errorMessage: 'Fehler beim Erstellen der Abteilung', errorMessage: t('createError'),
onSuccess: () => { onSuccess: () => {
setOpen(false); setOpen(false);
setName(''); setName('');
@@ -61,42 +63,42 @@ export function CreateDepartmentDialog({
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button size="sm" />}> <DialogTrigger render={<Button size="sm" />}>
<Plus className="mr-1 h-4 w-4" /> <Plus className="mr-1 h-4 w-4" />
Neue Abteilung {t('newDepartment')}
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogHeader> <DialogHeader>
<DialogTitle>Neue Abteilung</DialogTitle> <DialogTitle>{t('newDepartment')}</DialogTitle>
<DialogDescription> <DialogDescription>
Erstellen Sie eine neue Abteilung oder Sparte für Ihren Verein. {t('createDialogDescription')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="dept-name">Name</Label> <Label htmlFor="dept-name">{t('name')}</Label>
<Input <Input
id="dept-name" id="dept-name"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="z. B. Jugendabteilung" placeholder={t('namePlaceholder')}
required required
minLength={1} minLength={1}
maxLength={128} maxLength={128}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="dept-description">Beschreibung (optional)</Label> <Label htmlFor="dept-description">{t('descriptionLabel')}</Label>
<Input <Input
id="dept-description" id="dept-description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Kurze Beschreibung" placeholder={t('descriptionPlaceholder')}
/> />
</div> </div>
</div> </div>
<DialogFooter className="mt-4"> <DialogFooter className="mt-4">
<Button type="submit" disabled={isPending || !name.trim()}> <Button type="submit" disabled={isPending || !name.trim()}>
{isPending ? 'Wird erstellt…' : 'Erstellen'} {isPending ? t('creating') : t('create')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -0,0 +1,93 @@
'use client';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trash2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
interface DeleteDepartmentButtonProps {
departmentId: string;
departmentName: string;
accountId: string;
}
export function DeleteDepartmentButton({
departmentId,
departmentName,
accountId,
}: DeleteDepartmentButtonProps) {
const t = useTranslations('members.departments');
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
startTransition(async () => {
try {
const response = await fetch(
`/api/members/departments/${departmentId}`,
{
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId }),
},
);
if (response.ok) {
router.refresh();
}
} catch (error) {
console.error('Failed to delete department:', error);
}
});
};
return (
<AlertDialog>
<AlertDialogTrigger
render={
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
disabled={isPending}
>
<Trash2 className="h-4 w-4" aria-hidden="true" />
<span className="sr-only">{t('deleteAria')}</span>
</Button>
}
/>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('deleteTitle')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteConfirm', { name: departmentName })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{t('delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -1,4 +1,5 @@
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -8,6 +9,7 @@ import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state'; import { EmptyState } from '~/components/empty-state';
import { CreateDepartmentDialog } from './create-department-dialog'; import { CreateDepartmentDialog } from './create-department-dialog';
import { DeleteDepartmentButton } from './delete-department-button';
interface Props { interface Props {
params: Promise<{ account: string }>; params: Promise<{ account: string }>;
@@ -16,6 +18,7 @@ interface Props {
export default async function DepartmentsPage({ params }: Props) { export default async function DepartmentsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -29,8 +32,8 @@ export default async function DepartmentsPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Abteilungen" title={t('departments.title')}
description="Sparten und Abteilungen verwalten" description={t('departments.subtitle')}
> >
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
@@ -40,16 +43,23 @@ export default async function DepartmentsPage({ params }: Props) {
{departments.length === 0 ? ( {departments.length === 0 ? (
<EmptyState <EmptyState
icon={<Users className="h-8 w-8" />} icon={<Users className="h-8 w-8" />}
title="Keine Abteilungen vorhanden" title={t('departments.noDepartments')}
description="Erstellen Sie Ihre erste Abteilung." description={t('departments.createFirst')}
/> />
) : ( ) : (
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">Beschreibung</th> {t('departments.name')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('departments.description')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('departments.actions')}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -62,6 +72,15 @@ export default async function DepartmentsPage({ params }: Props) {
<td className="text-muted-foreground p-3"> <td className="text-muted-foreground p-3">
{String(dept.description ?? '—')} {String(dept.description ?? '—')}
</td> </td>
<td className="p-3">
<div className="flex items-center gap-2">
<DeleteDepartmentButton
departmentId={String(dept.id)}
departmentName={String(dept.name)}
accountId={acct.id}
/>
</div>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { DuesCategoryManager } from '@kit/member-management/components'; import { DuesCategoryManager } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -11,6 +13,7 @@ interface Props {
export default async function DuesPage({ params }: Props) { export default async function DuesPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('members');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -25,8 +28,8 @@ export default async function DuesPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Beitragskategorien" title={t('dues.title')}
description="Mitgliedsbeiträge verwalten" description={t('dues.subtitle')}
> >
<DuesCategoryManager categories={categories} accountId={acct.id} /> <DuesCategoryManager categories={categories} accountId={acct.id} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { MemberImportWizard } from '@kit/member-management/components'; import { MemberImportWizard } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -11,6 +13,7 @@ interface Props {
export default async function MemberImportPage({ params }: Props) { export default async function MemberImportPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -21,8 +24,8 @@ export default async function MemberImportPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Mitglieder importieren" title={t('import.title')}
description="CSV-Datei importieren" description={t('import.subtitle')}
> >
<MemberImportWizard accountId={acct.id} account={account} /> <MemberImportWizard accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -13,6 +15,7 @@ interface Props {
export default async function InvitationsPage({ params }: Props) { export default async function InvitationsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -34,8 +37,8 @@ export default async function InvitationsPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Portal-Einladungen" title={t('invitations.title')}
description="Einladungen zum Mitgliederportal verwalten" description={t('invitations.subtitle')}
> >
<InvitationsView <InvitationsView
invitations={invitations} invitations={invitations}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { CreateMemberForm } from '@kit/member-management/components'; import { CreateMemberForm } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -11,6 +13,7 @@ interface Props {
export default async function NewMemberPage({ params }: Props) { export default async function NewMemberPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('members');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -25,8 +28,8 @@ export default async function NewMemberPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neues Mitglied" title={t('form.newMemberTitle')}
description="Mitglied manuell anlegen" description={t('form.newMemberDescription')}
> >
<CreateMemberForm <CreateMemberForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { MembersDataTable } from '@kit/member-management/components'; import { MembersDataTable } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -16,6 +18,7 @@ export default async function MembersPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -36,8 +39,8 @@ export default async function MembersPage({ params, searchParams }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Mitglieder" title={t('nav.members')}
description={`${result.total} Mitglieder`} description={`${result.total} ${t('nav.members')}`}
> >
<MembersDataTable <MembersDataTable
data={result.data} data={result.data}

View File

@@ -6,6 +6,7 @@ import {
BarChart3, BarChart3,
TrendingUp, TrendingUp,
} from 'lucide-react'; } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api'; import { createMemberManagementApi } from '@kit/member-management/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -23,6 +24,7 @@ interface PageProps {
export default async function MemberStatisticsPage({ params }: PageProps) { export default async function MemberStatisticsPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -36,33 +38,33 @@ export default async function MemberStatisticsPage({ params }: PageProps) {
const stats = await api.getMemberStatistics(acct.id); const stats = await api.getMemberStatistics(acct.id);
const statusChartData = [ const statusChartData = [
{ name: 'Aktiv', value: stats.active ?? 0 }, { name: t('status.active'), value: stats.active ?? 0 },
{ name: 'Inaktiv', value: stats.inactive ?? 0 }, { name: t('status.inactive'), value: stats.inactive ?? 0 },
{ name: 'Ausstehend', value: stats.pending ?? 0 }, { name: t('status.pending'), value: stats.pending ?? 0 },
{ name: 'Ausgetreten', value: stats.resigned ?? 0 }, { name: t('status.resigned'), value: stats.resigned ?? 0 },
]; ];
return ( return (
<CmsPageShell account={account} title="Mitglieder-Statistiken"> <CmsPageShell account={account} title={t('statistics.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
<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">
<StatsCard <StatsCard
title="Gesamt" title={t('statistics.totalMembers')}
value={stats.total ?? 0} value={stats.total ?? 0}
icon={<Users className="h-5 w-5" />} icon={<Users className="h-5 w-5" />}
/> />
<StatsCard <StatsCard
title="Aktiv" title={t('status.active')}
value={stats.active ?? 0} value={stats.active ?? 0}
icon={<UserCheck className="h-5 w-5" />} icon={<UserCheck className="h-5 w-5" />}
/> />
<StatsCard <StatsCard
title="Inaktiv" title={t('status.inactive')}
value={stats.inactive ?? 0} value={stats.inactive ?? 0}
icon={<UserMinus className="h-5 w-5" />} icon={<UserMinus className="h-5 w-5" />}
/> />
<StatsCard <StatsCard
title="Ausstehend" title={t('status.pending')}
value={stats.pending ?? 0} value={stats.pending ?? 0}
icon={<Clock className="h-5 w-5" />} icon={<Clock className="h-5 w-5" />}
/> />
@@ -73,7 +75,7 @@ export default async function MemberStatisticsPage({ params }: PageProps) {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" /> <BarChart3 className="h-5 w-5" />
Mitglieder nach Status {t('statistics.title')}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -85,7 +87,7 @@ export default async function MemberStatisticsPage({ params }: PageProps) {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" /> <TrendingUp className="h-5 w-5" />
Verteilung {t('statistics.totalMembers')}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@@ -1,6 +1,7 @@
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';
@@ -15,6 +16,7 @@ export default async function RecordDetailPage({
params, params,
}: RecordDetailPageProps) { }: RecordDetailPageProps) {
const { account, moduleId, recordId } = await params; const { account, moduleId, recordId } = await params;
const t = await getTranslations('cms.modules');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createModuleBuilderApi(client); const api = createModuleBuilderApi(client);
@@ -31,14 +33,14 @@ export default async function RecordDetailPage({
api.records.getRecord(recordId), api.records.getRecord(recordId),
]); ]);
if (!moduleWithFields || !record) return <div>Nicht gefunden</div>; if (!moduleWithFields || !record) return <div>{t('notFound')}</div>;
const fields = moduleWithFields.fields; const fields = moduleWithFields.fields;
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title={`${String(moduleWithFields.display_name)}Datensatz`} title={`${String(moduleWithFields.display_name)}${t('record')}`}
> >
<RecordDetailClient <RecordDetailClient
fields={fields as Parameters<typeof ModuleForm>[0]['fields']} fields={fields as Parameters<typeof ModuleForm>[0]['fields']}

View File

@@ -147,17 +147,19 @@ export function RecordDetailClient({
</Button> </Button>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger
render={
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
disabled={isBusy} disabled={isBusy}
data-test="record-delete-btn" data-test="record-delete-btn"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" aria-hidden="true" />
Löschen Löschen
</Button> </Button>
</AlertDialogTrigger> }
/>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Datensatz löschen?</AlertDialogTitle> <AlertDialogTitle>Datensatz löschen?</AlertDialogTitle>

View File

@@ -310,11 +310,13 @@ export function ImportWizard({
</div> </div>
) : ( ) : (
<> <>
<div className="max-h-80 overflow-auto rounded-md border"> <div className="max-h-80 overflow-x-auto rounded-md border">
<table className="w-full text-xs"> <table className="w-full min-w-[640px] text-xs">
<thead> <thead>
<tr className="bg-muted/50 border-b"> <tr className="bg-muted/50 border-b">
<th className="p-2 text-left">#</th> <th scope="col" className="p-2 text-left">
#
</th>
{mappedFields.map((f) => ( {mappedFields.map((f) => (
<th key={f.name} className="p-2 text-left"> <th key={f.name} className="p-2 text-left">
{f.display_name} {f.display_name}

View File

@@ -1,5 +1,6 @@
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';
@@ -11,11 +12,12 @@ interface ImportPageProps {
export default async function ImportPage({ params }: ImportPageProps) { export default async function ImportPage({ params }: ImportPageProps) {
const { account, moduleId } = await params; const { account, moduleId } = await params;
const t = await getTranslations('cms.modules');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createModuleBuilderApi(client); const api = createModuleBuilderApi(client);
const moduleWithFields = await api.modules.getModuleWithFields(moduleId); const moduleWithFields = await api.modules.getModuleWithFields(moduleId);
if (!moduleWithFields) return <div>Modul nicht gefunden</div>; if (!moduleWithFields) return <div>{t('notFound')}</div>;
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -23,7 +25,7 @@ export default async function ImportPage({ params }: ImportPageProps) {
.eq('slug', account) .eq('slug', account)
.single(); .single();
if (!acct) return <div>Account nicht gefunden</div>; if (!acct) return <div>{t('accountNotFound')}</div>;
const fields = (moduleWithFields.fields ?? []).map((f) => ({ const fields = (moduleWithFields.fields ?? []).map((f) => ({
name: String(f.name), name: String(f.name),
@@ -33,7 +35,7 @@ export default async function ImportPage({ params }: ImportPageProps) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title={`${String(moduleWithFields.display_name)}Import`} title={`${String(moduleWithFields.display_name)}${t('importTitle')}`}
> >
<ImportWizard <ImportWizard
moduleId={moduleId} moduleId={moduleId}

View File

@@ -1,6 +1,7 @@
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';
@@ -13,6 +14,7 @@ interface NewRecordPageProps {
export default async function NewRecordPage({ params }: NewRecordPageProps) { export default async function NewRecordPage({ params }: NewRecordPageProps) {
const { account, moduleId } = await params; const { account, moduleId } = await params;
const t = await getTranslations('cms.modules');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createModuleBuilderApi(client); const api = createModuleBuilderApi(client);
@@ -25,14 +27,14 @@ export default async function NewRecordPage({ params }: NewRecordPageProps) {
if (!accountData) return <AccountNotFound />; if (!accountData) return <AccountNotFound />;
const moduleWithFields = await api.modules.getModuleWithFields(moduleId); const moduleWithFields = await api.modules.getModuleWithFields(moduleId);
if (!moduleWithFields) return <div>Modul nicht gefunden</div>; if (!moduleWithFields) return <div>{t('notFound')}</div>;
const fields = moduleWithFields.fields; const fields = moduleWithFields.fields;
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title={`${String(moduleWithFields.display_name)}Neuer Datensatz`} title={`${String(moduleWithFields.display_name)}${t('newRecord')}`}
> >
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
<CreateRecordForm <CreateRecordForm

View File

@@ -1,11 +1,15 @@
import Link from 'next/link'; import Link from 'next/link';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
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 { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
import { decodeFilters } from './_lib/filter-params'; import { decodeFilters } from './_lib/filter-params';
import { ModuleRecordsTable } from './module-records-table'; import { ModuleRecordsTable } from './module-records-table';
import { ModuleSearchBar } from './module-search-bar'; import { ModuleSearchBar } from './module-search-bar';
@@ -22,12 +26,13 @@ export default async function ModuleDetailPage({
const { account, moduleId } = await params; const { account, moduleId } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('cms.modules');
const api = createModuleBuilderApi(client); const api = createModuleBuilderApi(client);
const moduleWithFields = await api.modules.getModuleWithFields(moduleId); const moduleWithFields = await api.modules.getModuleWithFields(moduleId);
if (!moduleWithFields) { if (!moduleWithFields) {
return <div>Modul nicht gefunden</div>; return <AccountNotFound />;
} }
const page = Number(search.page) || 1; const page = Number(search.page) || 1;
@@ -50,7 +55,20 @@ export default async function ModuleDetailPage({
sortField, sortField,
sortDirection, sortDirection,
search: (search.q as string) ?? undefined, search: (search.q as string) ?? undefined,
filters, filters: filters as Array<{
field: string;
operator:
| 'eq'
| 'neq'
| 'gt'
| 'gte'
| 'lt'
| 'lte'
| 'like'
| 'is_null'
| 'not_null';
value?: string;
}>,
}); });
const allFields = moduleWithFields.fields; const allFields = moduleWithFields.fields;
@@ -64,12 +82,14 @@ export default async function ModuleDetailPage({
})); }));
return ( return (
<CmsPageShell
account={account}
title={String(moduleWithFields.display_name)}
description={moduleWithFields.description ?? undefined}
>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">
{moduleWithFields.display_name}
</h1>
{moduleWithFields.description && ( {moduleWithFields.description && (
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{moduleWithFields.description} {moduleWithFields.description}
@@ -79,7 +99,7 @@ export default async function ModuleDetailPage({
<Button asChild> <Button asChild>
<Link href={`/home/${account}/modules/${moduleId}/new`}> <Link href={`/home/${account}/modules/${moduleId}/new`}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Neuer Datensatz {t('newRecord')}
</Link> </Link>
</Button> </Button>
</div> </div>
@@ -93,9 +113,12 @@ export default async function ModuleDetailPage({
account={account} account={account}
moduleId={moduleId} moduleId={moduleId}
currentSort={ currentSort={
sortField ? { field: sortField, direction: sortDirection } : undefined sortField
? { field: sortField, direction: sortDirection }
: undefined
} }
/> />
</div> </div>
</CmsPageShell>
); );
} }

View File

@@ -47,16 +47,18 @@ export function DeleteModuleButton({
return ( return (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger
render={
<Button <Button
variant="destructive" variant="destructive"
disabled={isDeleting || isPending} disabled={isDeleting || isPending}
data-test="module-archive-btn" data-test="module-archive-btn"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" aria-hidden="true" />
Modul archivieren Modul archivieren
</Button> </Button>
</AlertDialogTrigger> }
/>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Modul archivieren?</AlertDialogTitle> <AlertDialogTitle>Modul archivieren?</AlertDialogTitle>

View File

@@ -135,13 +135,15 @@ export function ModulePermissions({
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="bg-muted/50 border-b"> <tr className="bg-muted/50 border-b">
<th className="p-3 text-left">Rolle</th> <th scope="col" className="p-3 text-left">
Rolle
</th>
{PERMISSION_COLUMNS.map((col) => ( {PERMISSION_COLUMNS.map((col) => (
<th key={col.key} className="p-3 text-center text-xs"> <th key={col.key} className="p-3 text-center text-xs">
{col.label} {col.label}
</th> </th>
))} ))}
<th className="p-3 text-right" /> <th scope="col" className="p-3 text-right" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -132,12 +132,14 @@ export function ModuleRelations({
if (!v) resetForm(); if (!v) resetForm();
}} }}
> >
<DialogTrigger asChild> <DialogTrigger
render={
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" aria-hidden="true" />
Neue Verknüpfung Neue Verknüpfung
</Button> </Button>
</DialogTrigger> }
/>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Neue Verknüpfung erstellen</DialogTitle> <DialogTitle>Neue Verknüpfung erstellen</DialogTitle>
@@ -145,7 +147,10 @@ export function ModuleRelations({
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Quellfeld</Label> <Label>Quellfeld</Label>
<Select value={sourceFieldId} onValueChange={setSourceFieldId}> <Select
value={sourceFieldId}
onValueChange={(v) => setSourceFieldId(v ?? '')}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Feld auswählen" /> <SelectValue placeholder="Feld auswählen" />
</SelectTrigger> </SelectTrigger>
@@ -162,7 +167,7 @@ export function ModuleRelations({
<Label>Zielmodul</Label> <Label>Zielmodul</Label>
<Select <Select
value={targetModuleId} value={targetModuleId}
onValueChange={setTargetModuleId} onValueChange={(v) => setTargetModuleId(v ?? '')}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Modul auswählen" /> <SelectValue placeholder="Modul auswählen" />
@@ -178,7 +183,10 @@ export function ModuleRelations({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Beziehungstyp</Label> <Label>Beziehungstyp</Label>
<Select value={relationType} onValueChange={setRelationType}> <Select
value={relationType}
onValueChange={(v) => setRelationType(v ?? '')}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Typ auswählen" /> <SelectValue placeholder="Typ auswählen" />
</SelectTrigger> </SelectTrigger>

View File

@@ -97,16 +97,28 @@ export default async function ModuleSettingsPage({
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left">Name</th> <th scope="col" className="p-3 text-left">
<th className="p-3 text-left">Anzeigename</th> Name
<th className="p-3 text-left">Typ</th> </th>
<th className="p-3 text-left">Pflicht</th> <th scope="col" className="p-3 text-left">
<th className="p-3 text-left">Tabelle</th> Anzeigename
<th className="p-3 text-left">Formular</th> </th>
<th scope="col" className="p-3 text-left">
Typ
</th>
<th scope="col" className="p-3 text-left">
Pflicht
</th>
<th scope="col" className="p-3 text-left">
Tabelle
</th>
<th scope="col" className="p-3 text-left">
Formular
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,10 +1,13 @@
import Link from 'next/link'; import Link from 'next/link';
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 { 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 { ModuleToggles } from './_components/module-toggles'; import { ModuleToggles } from './_components/module-toggles';
@@ -14,6 +17,7 @@ interface ModulesPageProps {
export default async function ModulesPage({ params }: ModulesPageProps) { export default async function ModulesPage({ params }: ModulesPageProps) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('cms.modules');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createModuleBuilderApi(client); const api = createModuleBuilderApi(client);
@@ -42,18 +46,17 @@ export default async function ModulesPage({ params }: ModulesPageProps) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Module" title={t('title')}
description="Verwalten Sie Ihre Datenmodule" description={t('description')}
> >
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<ModuleToggles accountId={accountData.id} features={features} /> <ModuleToggles accountId={accountData.id} features={features} />
{modules.length === 0 ? ( {modules.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center"> <EmptyState
<p className="text-muted-foreground"> title={t('noModules')}
Noch keine Module vorhanden. Erstellen Sie Ihr erstes Modul. description={t('createFirstModule')}
</p> />
</div>
) : ( ) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{modules.map((module: Record<string, unknown>) => ( {modules.map((module: Record<string, unknown>) => (
@@ -69,7 +72,7 @@ export default async function ModulesPage({ params }: ModulesPageProps) {
</p> </p>
) : null} ) : null}
<div className="text-muted-foreground mt-2 text-xs"> <div className="text-muted-foreground mt-2 text-xs">
Status: {String(module.status)} {String(module.status)}
</div> </div>
</Link> </Link>
))} ))}

View File

@@ -48,15 +48,17 @@ export function DispatchNewsletterButton({
return ( return (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger
render={
<Button <Button
disabled={isDispatching || isPending || recipientCount === 0} disabled={isDispatching || isPending || recipientCount === 0}
data-test="newsletter-send-btn" data-test="newsletter-send-btn"
> >
<Send className="mr-2 h-4 w-4" /> <Send className="mr-2 h-4 w-4" aria-hidden="true" />
Newsletter versenden Newsletter versenden
</Button> </Button>
</AlertDialogTrigger> }
/>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Newsletter versenden?</AlertDialogTitle> <AlertDialogTitle>Newsletter versenden?</AlertDialogTitle>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createNewsletterApi } from '@kit/newsletter/api'; import { createNewsletterApi } from '@kit/newsletter/api';
import { CreateNewsletterForm } from '@kit/newsletter/components'; import { CreateNewsletterForm } from '@kit/newsletter/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -12,6 +14,7 @@ interface Props {
export default async function EditNewsletterPage({ params }: Props) { export default async function EditNewsletterPage({ params }: Props) {
const { account, campaignId } = await params; const { account, campaignId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('newsletter');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -28,7 +31,7 @@ export default async function EditNewsletterPage({ params }: Props) {
const n = newsletter as Record<string, unknown>; const n = newsletter as Record<string, unknown>;
return ( return (
<CmsPageShell account={account} title="Newsletter bearbeiten"> <CmsPageShell account={account} title={t('form.editTitle')}>
<CreateNewsletterForm <CreateNewsletterForm
accountId={acct.id} accountId={acct.id}
account={account} account={account}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { CreateNewsletterForm } from '@kit/newsletter/components'; import { CreateNewsletterForm } from '@kit/newsletter/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -11,18 +13,21 @@ interface Props {
export default async function NewNewsletterPage({ params }: Props) { export default async function NewNewsletterPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('newsletter');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
.eq('slug', account) .eq('slug', account)
.single(); .single();
if (!acct) return <AccountNotFound />; if (!acct) return <AccountNotFound />;
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neuer Newsletter" title={t('form.newTitle')}
description="Newsletter-Kampagne erstellen" description={t('form.newDescription')}
> >
<CreateNewsletterForm accountId={acct.id} account={account} /> <CreateNewsletterForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { FileText, Plus } from 'lucide-react'; import { FileText, Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createNewsletterApi } from '@kit/newsletter/api'; import { createNewsletterApi } from '@kit/newsletter/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -19,6 +20,7 @@ interface PageProps {
export default async function NewsletterTemplatesPage({ params }: PageProps) { export default async function NewsletterTemplatesPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('newsletter');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -33,20 +35,17 @@ export default async function NewsletterTemplatesPage({ params }: PageProps) {
const templates = templatesResult.data; const templates = templatesResult.data;
return ( return (
<CmsPageShell account={account} title="Newsletter-Vorlagen"> <CmsPageShell account={account} title={t('templates.title')}>
<div className="flex w-full flex-col gap-6"> <div className="flex w-full flex-col gap-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold">Newsletter-Vorlagen</h1> <p className="text-muted-foreground">{t('templates.subtitle')}</p>
<p className="text-muted-foreground">
Wiederverwendbare Vorlagen für Newsletter
</p>
</div> </div>
<Button data-test="newsletter-templates-new-btn"> <Button data-test="newsletter-templates-new-btn">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Neue Vorlage {t('templates.newTemplate')}
</Button> </Button>
</div> </div>
@@ -54,23 +53,31 @@ export default async function NewsletterTemplatesPage({ params }: PageProps) {
{templates.length === 0 ? ( {templates.length === 0 ? (
<EmptyState <EmptyState
icon={<FileText className="h-8 w-8" />} icon={<FileText className="h-8 w-8" />}
title="Keine Vorlagen vorhanden" title={t('templates.noTemplates')}
description="Erstellen Sie Ihre erste Newsletter-Vorlage, um sie in Kampagnen wiederzuverwenden." description={t('templates.createFirst')}
actionLabel="Neue Vorlage" actionLabel={t('templates.newTemplate')}
/> />
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Alle Vorlagen ({templates.length})</CardTitle> <CardTitle>
{t('templates.allTemplates', { count: templates.length })}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="rounded-md border"> <div className="overflow-x-auto rounded-md border">
<table className="w-full 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 className="p-3 text-left font-medium">Name</th> <th scope="col" className="p-3 text-left font-medium">
<th className="p-3 text-left font-medium">Betreff</th> {t('templates.name')}
<th className="p-3 text-left font-medium">Variablen</th> </th>
<th scope="col" className="p-3 text-left font-medium">
{t('templates.subject')}
</th>
<th scope="col" className="p-3 text-left font-medium">
{t('templates.variables')}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -12,6 +12,7 @@ import {
Activity, Activity,
BedDouble, BedDouble,
} from 'lucide-react'; } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createBookingManagementApi } from '@kit/booking-management/api'; import { createBookingManagementApi } from '@kit/booking-management/api';
import { createCourseManagementApi } from '@kit/course-management/api'; import { createCourseManagementApi } from '@kit/course-management/api';
@@ -44,6 +45,7 @@ export default async function TeamAccountHomePage({
}: TeamAccountHomePageProps) { }: TeamAccountHomePageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('common');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -111,41 +113,47 @@ export default async function TeamAccountHomePage({
{/* Stats Row */} {/* Stats Row */}
<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">
<StatsCard <StatsCard
title="Mitglieder" title={t('dashboard.members')}
value={memberStats.active} value={memberStats.active}
icon={<UserCheck className="h-5 w-5" />} icon={<UserCheck className="h-5 w-5" aria-hidden="true" />}
description={`${memberStats.total} gesamt, ${memberStats.pending} ausstehend`} description={t('dashboard.membersDescription', {
total: memberStats.total,
pending: memberStats.pending,
})}
/> />
<StatsCard <StatsCard
title="Kurse" title={t('dashboard.courses')}
value={courseStats.openCourses} value={courseStats.openCourses}
icon={<GraduationCap className="h-5 w-5" />} icon={<GraduationCap className="h-5 w-5" aria-hidden="true" />}
description={`${courseStats.totalCourses} gesamt, ${courseStats.totalParticipants} Teilnehmer`} description={t('dashboard.coursesDescription', {
total: courseStats.totalCourses,
participants: courseStats.totalParticipants,
})}
/> />
<StatsCard <StatsCard
title="Offene Rechnungen" title={t('dashboard.openInvoices')}
value={openInvoices.length} value={openInvoices.length}
icon={<FileText className="h-5 w-5" />} icon={<FileText className="h-5 w-5" aria-hidden="true" />}
description="Entwürfe zum Versenden" description={t('dashboard.openInvoicesDescription')}
/> />
<StatsCard <StatsCard
title="Newsletter" title={t('dashboard.newsletters')}
value={newsletters.length} value={newsletters.length}
icon={<Mail className="h-5 w-5" />} icon={<Mail className="h-5 w-5" aria-hidden="true" />}
description="Erstellt" description={t('dashboard.newslettersDescription')}
/> />
</div> </div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Letzte Aktivität */} {/* Recent Activity */}
<Card className="lg:col-span-2"> <Card className="lg:col-span-2">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" /> <Activity className="h-5 w-5" aria-hidden="true" />
Letzte Aktivität {t('dashboard.recentActivity')}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Aktuelle Buchungen und Veranstaltungen {t('dashboard.recentActivityDescription')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -160,14 +168,14 @@ export default async function TeamAccountHomePage({
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-full bg-blue-500/10 p-2 text-blue-600"> <div className="rounded-full bg-blue-500/10 p-2 text-blue-600">
<BedDouble className="h-4 w-4" /> <BedDouble className="h-4 w-4" aria-hidden="true" />
</div> </div>
<div> <div>
<Link <Link
href={`/home/${account}/bookings/${String(booking.id)}`} href={`/home/${account}/bookings/${String(booking.id)}`}
className="text-sm font-medium hover:underline" className="text-sm font-medium hover:underline"
> >
Buchung{' '} {t('dashboard.bookingFrom')}{' '}
{booking.check_in {booking.check_in
? formatDate(booking.check_in as string) ? formatDate(booking.check_in as string)
: '—'} : '—'}
@@ -194,7 +202,10 @@ export default async function TeamAccountHomePage({
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="rounded-full bg-amber-500/10 p-2 text-amber-600"> <div className="rounded-full bg-amber-500/10 p-2 text-amber-600">
<CalendarDays className="h-4 w-4" /> <CalendarDays
className="h-4 w-4"
aria-hidden="true"
/>
</div> </div>
<div> <div>
<Link <Link
@@ -216,20 +227,22 @@ export default async function TeamAccountHomePage({
{bookings.data.length === 0 && events.data.length === 0 && ( {bookings.data.length === 0 && events.data.length === 0 && (
<EmptyState <EmptyState
icon={<Activity className="h-8 w-8" />} icon={<Activity className="h-8 w-8" aria-hidden="true" />}
title="Noch keine Aktivitäten" title={t('dashboard.recentActivityEmpty')}
description="Aktuelle Buchungen und Veranstaltungen werden hier angezeigt." description={t('dashboard.recentActivityEmptyDescription')}
/> />
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Schnellaktionen */} {/* Quick Actions */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Schnellaktionen</CardTitle> <CardTitle>{t('dashboard.quickActions')}</CardTitle>
<CardDescription>Häufig verwendete Aktionen</CardDescription> <CardDescription>
{t('dashboard.quickActionsDescription')}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2"> <CardContent className="flex flex-col gap-2">
<Link <Link
@@ -237,10 +250,10 @@ export default async function TeamAccountHomePage({
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all" className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<UserPlus className="h-4 w-4" /> <UserPlus className="h-4 w-4" aria-hidden="true" />
Neues Mitglied {t('dashboard.newMember')}
</span> </span>
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
<Link <Link
@@ -248,10 +261,10 @@ export default async function TeamAccountHomePage({
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all" className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<GraduationCap className="h-4 w-4" /> <GraduationCap className="h-4 w-4" aria-hidden="true" />
Neuer Kurs {t('dashboard.newCourse')}
</span> </span>
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
<Link <Link
@@ -260,9 +273,9 @@ export default async function TeamAccountHomePage({
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Mail className="h-4 w-4" /> <Mail className="h-4 w-4" />
Newsletter erstellen {t('dashboard.createNewsletter')}
</span> </span>
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
<Link <Link
@@ -270,10 +283,10 @@ export default async function TeamAccountHomePage({
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all" className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<BedDouble className="h-4 w-4" /> <BedDouble className="h-4 w-4" aria-hidden="true" />
Neue Buchung {t('dashboard.newBooking')}
</span> </span>
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
<Link <Link
@@ -281,98 +294,14 @@ export default async function TeamAccountHomePage({
className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all" className="border-border bg-background hover:bg-muted hover:text-foreground inline-flex w-full items-center justify-between gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-all"
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" aria-hidden="true" />
Neue Veranstaltung {t('dashboard.newEvent')}
</span> </span>
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link> </Link>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Module Overview Row */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-sm font-medium">
Buchungen
</p>
<p className="text-2xl font-bold">{bookings.total}</p>
<p className="text-muted-foreground text-xs">
{
bookings.data.filter(
(b: Record<string, unknown>) =>
b.status === 'confirmed' || b.status === 'checked_in',
).length
}{' '}
aktiv
</p>
</div>
<Link
href={`/home/${account}/bookings`}
className="hover:bg-muted hover:text-foreground inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium transition-all"
>
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-sm font-medium">
Veranstaltungen
</p>
<p className="text-2xl font-bold">{events.total}</p>
<p className="text-muted-foreground text-xs">
{
events.data.filter(
(e: Record<string, unknown>) =>
e.status === 'published' ||
e.status === 'registration_open',
).length
}{' '}
aktiv
</p>
</div>
<Link
href={`/home/${account}/events`}
className="hover:bg-muted hover:text-foreground inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium transition-all"
>
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-sm font-medium">
Kurse abgeschlossen
</p>
<p className="text-2xl font-bold">
{courseStats.completedCourses}
</p>
<p className="text-muted-foreground text-xs">
von {courseStats.totalCourses} insgesamt
</p>
</div>
<Link
href={`/home/${account}/courses`}
className="hover:bg-muted hover:text-foreground inline-flex h-9 w-9 items-center justify-center rounded-lg text-sm font-medium transition-all"
>
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</CardContent>
</Card>
</div>
</div> </div>
</CmsPageShell> </CmsPageShell>
); );

View File

@@ -1,5 +1,6 @@
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';
@@ -10,6 +11,7 @@ interface Props {
export default async function NewSitePage({ params }: Props) { export default async function NewSitePage({ params }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('siteBuilder');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -21,8 +23,8 @@ export default async function NewSitePage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neue Seite" title={t('pages.newPage')}
description="Seite für Ihre Vereinswebsite erstellen" description={t('pages.newPageDescription')}
> >
<CreatePageForm accountId={acct.id} account={account} /> <CreatePageForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,5 +1,6 @@
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';
@@ -10,6 +11,7 @@ interface Props {
export default async function NewPostPage({ params }: Props) { export default async function NewPostPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const t = await getTranslations('siteBuilder');
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -21,8 +23,8 @@ export default async function NewPostPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Neuer Beitrag" title={t('posts.newPost')}
description="Beitrag erstellen" description={t('posts.newPostDescription')}
> >
<CreatePostForm accountId={acct.id} account={account} /> <CreatePostForm accountId={acct.id} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { createSiteBuilderApi } from '@kit/site-builder/api'; import { createSiteBuilderApi } from '@kit/site-builder/api';
import { SiteSettingsForm } from '@kit/site-builder/components'; import { SiteSettingsForm } from '@kit/site-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -12,6 +14,8 @@ interface Props {
export default async function SiteSettingsPage({ params }: Props) { export default async function SiteSettingsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('siteBuilder');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
.select('id') .select('id')
@@ -25,8 +29,8 @@ export default async function SiteSettingsPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Website-Einstellungen" title={t('settings.title')}
description="Design und Kontaktdaten" description={t('settings.description')}
> >
<SiteSettingsForm <SiteSettingsForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -15,6 +17,7 @@ interface Props {
export default async function EditClubPage({ params }: Props) { export default async function EditClubPage({ params }: Props) {
const { account, clubId } = await params; const { account, clubId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -35,7 +38,7 @@ export default async function EditClubPage({ params }: Props) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title={`${String((club as Record<string, unknown>).name)}Bearbeiten`} title={`${String((club as Record<string, unknown>).name)}${t('pages.editClubTitle')}`}
> >
<VerbandTabNavigation account={account} activeTab="clubs" /> <VerbandTabNavigation account={account} activeTab="clubs" />
<CreateClubForm <CreateClubForm

View File

@@ -1,6 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Pencil } from 'lucide-react'; import { Pencil } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -22,6 +23,7 @@ interface Props {
export default async function ClubDetailPage({ params }: Props) { export default async function ClubDetailPage({ params }: Props) {
const { account, clubId } = await params; const { account, clubId } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -44,29 +46,28 @@ export default async function ClubDetailPage({ params }: Props) {
if (!detail?.club) return <AccountNotFound />; if (!detail?.club) return <AccountNotFound />;
return ( return (
<CmsPageShell account={account} title={`Verein ${detail.club.name}`}> <CmsPageShell
account={account}
title={`${t('pages.clubDetailTitle')} ${detail.club.name}`}
>
<VerbandTabNavigation account={account} activeTab="clubs" /> <VerbandTabNavigation account={account} activeTab="clubs" />
<div className="space-y-6"> <div className="space-y-6">
{/* Club Header */} {/* Club Header */}
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h1 className="text-2xl font-bold">{detail.club.name}</h1>
{detail.club.short_name && ( {detail.club.short_name && (
<p className="text-muted-foreground">{detail.club.short_name}</p> <p className="text-muted-foreground">{detail.club.short_name}</p>
)} )}
<div className="text-muted-foreground mt-2 flex flex-wrap gap-4 text-sm"> <div className="text-muted-foreground mt-2 flex flex-wrap gap-4 text-sm">
{detail.club.city && ( {detail.club.address_city && (
<span> <span>
{detail.club.zip} {detail.club.city} {detail.club.address_zip} {detail.club.address_city}
</span> </span>
)} )}
{detail.club.member_count != null && ( {detail.club.member_count != null && (
<span>{detail.club.member_count} Mitglieder</span> <span>{detail.club.member_count} Mitglieder</span>
)} )}
{detail.club.founded_year && (
<span>Gegr. {detail.club.founded_year}</span>
)}
</div> </div>
</div> </div>
<Button asChild variant="outline" size="sm"> <Button asChild variant="outline" size="sm">

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -15,6 +17,7 @@ interface Props {
export default async function NewClubPage({ params }: Props) { export default async function NewClubPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -28,7 +31,7 @@ export default async function NewClubPage({ params }: Props) {
const types = await api.listTypes(acct.id); const types = await api.listTypes(acct.id);
return ( return (
<CmsPageShell account={account} title="Neuer Verein"> <CmsPageShell account={account} title={t('pages.newClubTitle')}>
<VerbandTabNavigation account={account} activeTab="clubs" /> <VerbandTabNavigation account={account} activeTab="clubs" />
<CreateClubForm <CreateClubForm
accountId={acct.id} accountId={acct.id}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -17,6 +19,7 @@ export default async function ClubsPage({ params, searchParams }: Props) {
const { account } = await params; const { account } = await params;
const search = await searchParams; const search = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -42,7 +45,7 @@ export default async function ClubsPage({ params, searchParams }: Props) {
]); ]);
return ( return (
<CmsPageShell account={account} title="Verbandsverwaltung - Vereine"> <CmsPageShell account={account} title={t('pages.clubsTitle')}>
<VerbandTabNavigation account={account} activeTab="clubs" /> <VerbandTabNavigation account={account} activeTab="clubs" />
<ClubsDataTable <ClubsDataTable
data={result.data} data={result.data}

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -25,6 +27,7 @@ export default async function HierarchyEventsPage({
const { account } = await params; const { account } = await params;
const sp = await searchParams; const sp = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -49,8 +52,8 @@ export default async function HierarchyEventsPage({
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Verbandsverwaltung - Veranstaltungen" title={t('pages.eventsTitle')}
description="Veranstaltungen aller verknüpften Organisationen anzeigen und filtern" description={t('pages.eventsDescription')}
> >
<VerbandTabNavigation account={account} activeTab="events" /> <VerbandTabNavigation account={account} activeTab="events" />
<HierarchyEvents <HierarchyEvents

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -15,6 +17,7 @@ interface PageProps {
export default async function HierarchyPage({ params }: PageProps) { export default async function HierarchyPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -34,8 +37,8 @@ export default async function HierarchyPage({ params }: PageProps) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Verbandsverwaltung - Hierarchie" title={t('pages.hierarchyTitle')}
description="Verwalten Sie die Organisationsstruktur Ihres Verbands" description={t('pages.hierarchyDescription')}
> >
<VerbandTabNavigation account={account} activeTab="hierarchy" /> <VerbandTabNavigation account={account} activeTab="hierarchy" />
<HierarchyTree <HierarchyTree

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -25,6 +27,7 @@ export default async function CrossOrgMembersPage({
const { account } = await params; const { account } = await params;
const sp = await searchParams; const sp = await searchParams;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -75,8 +78,8 @@ export default async function CrossOrgMembersPage({
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Verbandsverwaltung - Mitgliedersuche" title={t('pages.memberSearchTitle')}
description="Suchen Sie Mitglieder in allen verknüpften Organisationen" description={t('pages.memberSearchDescription')}
> >
<VerbandTabNavigation account={account} activeTab="members" /> <VerbandTabNavigation account={account} activeTab="members" />
<CrossOrgMemberSearch <CrossOrgMemberSearch

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -15,6 +17,7 @@ interface PageProps {
export default async function VerbandPage({ params }: PageProps) { export default async function VerbandPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -28,7 +31,7 @@ export default async function VerbandPage({ params }: PageProps) {
const stats = await api.getDashboardStats(acct.id); const stats = await api.getDashboardStats(acct.id);
return ( return (
<CmsPageShell account={account} title="Verbandsverwaltung"> <CmsPageShell account={account} title={t('pages.overviewTitle')}>
<VerbandTabNavigation account={account} activeTab="overview" /> <VerbandTabNavigation account={account} activeTab="overview" />
<VerbandDashboard stats={stats} account={account} /> <VerbandDashboard stats={stats} account={account} />
</CmsPageShell> </CmsPageShell>

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { import {
@@ -15,6 +17,7 @@ interface PageProps {
export default async function ReportingPage({ params }: PageProps) { export default async function ReportingPage({ params }: PageProps) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -34,8 +37,8 @@ export default async function ReportingPage({ params }: PageProps) {
return ( return (
<CmsPageShell <CmsPageShell
account={account} account={account}
title="Verbandsverwaltung - Berichte" title={t('pages.reportingTitle')}
description="Aggregierte Berichte und Kennzahlen aller Organisationen im Verband" description={t('pages.reportingDescription')}
> >
<VerbandTabNavigation account={account} activeTab="reporting" /> <VerbandTabNavigation account={account} activeTab="reporting" />
<HierarchyReport summary={summary} report={report} /> <HierarchyReport summary={summary} report={report} />

View File

@@ -3,6 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Plus, Pencil, Trash2, Settings } from 'lucide-react'; import { Plus, Pencil, Trash2, Settings } from 'lucide-react';
import { useTranslations } from 'next-intl';
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';
@@ -141,7 +142,7 @@ function SettingsSection({
<> <>
<div> <div>
<span className="font-medium">{String(item.name)}</span> <span className="font-medium">{String(item.name)}</span>
{item.description && ( {Boolean(item.description) && (
<p className="text-muted-foreground text-xs"> <p className="text-muted-foreground text-xs">
{String(item.description)} {String(item.description)}
</p> </p>
@@ -183,6 +184,7 @@ export default function SettingsContent({
types, types,
feeTypes, feeTypes,
}: SettingsContentProps) { }: SettingsContentProps) {
const t = useTranslations('verband');
// Roles // Roles
const { execute: execCreateRole, isPending: isCreatingRole } = const { execute: execCreateRole, isPending: isCreatingRole } =
useActionWithToast(createRole, { useActionWithToast(createRole, {
@@ -228,14 +230,13 @@ export default function SettingsContent({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-2xl font-bold">Einstellungen</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Funktionen, Vereinstypen und Beitragsarten verwalten {t('settings.subtitle')}
</p> </p>
</div> </div>
<SettingsSection <SettingsSection
title="Funktionen (Rollen)" title={t('settings.roles')}
items={roles} items={roles}
onAdd={(name, description) => onAdd={(name, description) =>
execCreateRole({ accountId, name, description, sortOrder: 0 }) execCreateRole({ accountId, name, description, sortOrder: 0 })
@@ -247,7 +248,7 @@ export default function SettingsContent({
/> />
<SettingsSection <SettingsSection
title="Vereinstypen" title={t('settings.types')}
items={types} items={types}
onAdd={(name, description) => onAdd={(name, description) =>
execCreateType({ accountId, name, description, sortOrder: 0 }) execCreateType({ accountId, name, description, sortOrder: 0 })
@@ -259,7 +260,7 @@ export default function SettingsContent({
/> />
<SettingsSection <SettingsSection
title="Beitragsarten" title={t('settings.feeTypes')}
items={feeTypes} items={feeTypes}
onAdd={(name, description) => onAdd={(name, description) =>
execCreateFeeType({ accountId, name, description, isActive: true }) execCreateFeeType({ accountId, name, description, isActive: true })

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api'; import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { VerbandTabNavigation } from '@kit/verbandsverwaltung/components'; import { VerbandTabNavigation } from '@kit/verbandsverwaltung/components';
@@ -14,6 +16,7 @@ interface Props {
export default async function SettingsPage({ params }: Props) { export default async function SettingsPage({ params }: Props) {
const { account } = await params; const { account } = await params;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client const { data: acct } = await client
.from('accounts') .from('accounts')
@@ -31,7 +34,7 @@ export default async function SettingsPage({ params }: Props) {
]); ]);
return ( return (
<CmsPageShell account={account} title="Verbandsverwaltung - Einstellungen"> <CmsPageShell account={account} title={t('pages.settingsTitle')}>
<VerbandTabNavigation account={account} activeTab="settings" /> <VerbandTabNavigation account={account} activeTab="settings" />
<SettingsContent <SettingsContent
accountId={acct.id} accountId={acct.id}

Some files were not shown because too many files have changed in this diff Show More