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 {
ArrowRightIcon,
BookOpenIcon,
CalendarIcon,
FileTextIcon,
GraduationCapIcon,
@@ -27,15 +26,19 @@ import {
EcosystemShowcase,
FeatureShowcase,
FeatureShowcaseIconContainer,
GradientText,
Hero,
Pill,
SecondaryHero,
} from '@kit/ui/marketing';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { AnimateOnScroll } from './_components/animate-on-scroll';
function Home() {
return (
<div className={'mt-4 flex flex-col space-y-24 py-14 lg:space-y-36'}>
@@ -51,7 +54,10 @@ function Home() {
}
title={
<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>
}
subtitle={
@@ -61,283 +67,343 @@ function Home() {
}
cta={<MainCallToActionButton />}
image={
<Image
priority
className={
'dark:border-primary/10 w-full rounded-2xl border border-gray-200 shadow-2xl'
}
width={3558}
height={2222}
src={`/images/dashboard.webp`}
alt={`MyEasyCMS Dashboard`}
/>
<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
priority
className={
'dark:border-primary/10 w-full rounded-2xl border border-gray-200 shadow-2xl'
}
width={3558}
height={2222}
src={`/images/dashboard.webp`}
alt={`MyEasyCMS Dashboard`}
/>
</div>
}
/>
</div>
{/* Trust Indicators */}
<div className={'container mx-auto'}>
<div className="flex flex-col items-center gap-8">
<p className="text-muted-foreground text-sm font-medium tracking-widest uppercase">
<Trans i18nKey={'marketing.trustedBy'} />
</p>
<div className="flex flex-wrap items-center justify-center gap-x-12 gap-y-6">
<TrustItem icon={UsersIcon} label="marketing.trustAssociations" />
<TrustItem
icon={GraduationCapIcon}
label="marketing.trustSchools"
/>
<TrustItem icon={BookOpenIcon} label="marketing.trustClubs" />
<TrustItem icon={GlobeIcon} label="marketing.trustOrganizations" />
{/* Stats Bar */}
<AnimateOnScroll>
<div className={'container mx-auto'}>
<div className="border-border border-y py-8">
<p className="text-muted-foreground mb-6 text-center text-sm font-medium tracking-widest uppercase">
<Trans i18nKey={'marketing.trustedBy'} />
</p>
<div className="reveal-stagger divide-border flex flex-wrap items-center justify-center divide-x">
<StatItem value="69,000+" labelKey="marketing.statMembers" />
<StatItem value="90+" labelKey="marketing.statOrganizations" />
<StatItem value="22" labelKey="marketing.statYears" />
<StatItem value="3" labelKey="marketing.statFederations" />
</div>
</div>
</div>
</div>
</AnimateOnScroll>
{/* Core Modules Feature Grid */}
<div className={'container mx-auto'}>
<div className={'py-4 xl:py-8'}>
<FeatureShowcase
heading={
<>
<b className="font-medium tracking-tight dark:text-white">
<Trans i18nKey={'marketing.featuresHeading'} />
</b>
.{' '}
<span className="text-secondary-foreground/70 block font-normal tracking-tight">
<Trans i18nKey={'marketing.featuresSubheading'} />
</span>
</>
}
icon={
<FeatureShowcaseIconContainer>
<LayoutDashboardIcon className="h-4 w-4" />
<span>
<Trans i18nKey={'marketing.featuresLabel'} />
</span>
</FeatureShowcaseIconContainer>
}
>
<div className="mt-2 grid w-full grid-cols-1 gap-4 md:mt-6 md:grid-cols-2 lg:grid-cols-3">
<IconFeatureCard
icon={UsersIcon}
titleKey="marketing.featureMembersTitle"
descKey="marketing.featureMembersDesc"
<AnimateOnScroll>
<div className={'container mx-auto'}>
<div className={'py-4 xl:py-8'}>
<FeatureShowcase
heading={
<>
<b className="font-medium tracking-tight dark:text-white">
<Trans i18nKey={'marketing.featuresHeading'} />
</b>
.{' '}
<span className="text-secondary-foreground/70 block font-normal tracking-tight">
<Trans i18nKey={'marketing.featuresSubheading'} />
</span>
</>
}
icon={
<FeatureShowcaseIconContainer>
<LayoutDashboardIcon className="h-4 w-4" />
<span>
<Trans i18nKey={'marketing.featuresLabel'} />
</span>
</FeatureShowcaseIconContainer>
}
>
<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
icon={UsersIcon}
titleKey="marketing.featureMembersTitle"
descKey="marketing.featureMembersDesc"
/>
<IconFeatureCard
icon={GraduationCapIcon}
titleKey="marketing.featureCoursesTitle"
descKey="marketing.featureCoursesDesc"
accentBg="bg-chart-1/10"
accentText="text-chart-1"
/>
<IconFeatureCard
icon={BedDoubleIcon}
titleKey="marketing.featureBookingsTitle"
descKey="marketing.featureBookingsDesc"
accentBg="bg-chart-2/10"
accentText="text-chart-2"
/>
<IconFeatureCard
icon={CalendarIcon}
titleKey="marketing.featureEventsTitle"
descKey="marketing.featureEventsDesc"
accentBg="bg-chart-3/10"
accentText="text-chart-3"
/>
<IconFeatureCard
icon={WalletIcon}
titleKey="marketing.featureFinanceTitle"
descKey="marketing.featureFinanceDesc"
accentBg="bg-chart-4/10"
accentText="text-chart-4"
/>
<IconFeatureCard
icon={MailIcon}
titleKey="marketing.featureNewsletterTitle"
descKey="marketing.featureNewsletterDesc"
accentBg="bg-chart-5/10"
accentText="text-chart-5"
/>
</div>
</FeatureShowcase>
</div>
</div>
</AnimateOnScroll>
{/* Testimonials */}
<AnimateOnScroll>
<div className="container mx-auto">
<div className="flex flex-col items-center gap-12">
<div className="text-center">
<h2 className="text-3xl font-medium tracking-tight xl:text-5xl dark:text-white">
<Trans i18nKey={'marketing.testimonialsHeading'} />
</h2>
<p className="text-secondary-foreground/70 mx-auto mt-4 max-w-2xl text-xl font-medium tracking-tight">
<Trans i18nKey={'marketing.testimonialsSubheading'} />
</p>
</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"
/>
<IconFeatureCard
icon={GraduationCapIcon}
titleKey="marketing.featureCoursesTitle"
descKey="marketing.featureCoursesDesc"
<TestimonialCard
quoteKey="marketing.testimonial2Quote"
nameKey="marketing.testimonial2Name"
roleKey="marketing.testimonial2Role"
/>
<IconFeatureCard
icon={BedDoubleIcon}
titleKey="marketing.featureBookingsTitle"
descKey="marketing.featureBookingsDesc"
/>
<IconFeatureCard
icon={CalendarIcon}
titleKey="marketing.featureEventsTitle"
descKey="marketing.featureEventsDesc"
/>
<IconFeatureCard
icon={WalletIcon}
titleKey="marketing.featureFinanceTitle"
descKey="marketing.featureFinanceDesc"
/>
<IconFeatureCard
icon={MailIcon}
titleKey="marketing.featureNewsletterTitle"
descKey="marketing.featureNewsletterDesc"
<TestimonialCard
quoteKey="marketing.testimonial3Quote"
nameKey="marketing.testimonial3Name"
roleKey="marketing.testimonial3Role"
/>
</div>
</FeatureShowcase>
</div>
</div>
</div>
{/* Dashboard Showcase */}
<div className={'container mx-auto'}>
<EcosystemShowcase
heading={<Trans i18nKey={'marketing.showcaseHeading'} />}
description={<Trans i18nKey={'marketing.showcaseDescription'} />}
>
<Image
className="rounded-lg shadow-lg"
src={'/images/dashboard.webp'}
alt="MyEasyCMS Dashboard"
width={1200}
height={800}
/>
</EcosystemShowcase>
</div>
</AnimateOnScroll>
{/* Additional Features Row */}
<div className={'container mx-auto'}>
<div className={'py-4 xl:py-8'}>
<FeatureShowcase
heading={
<>
<b className="font-medium tracking-tight dark:text-white">
<Trans i18nKey={'marketing.additionalFeaturesHeading'} />
</b>
.{' '}
<span className="text-secondary-foreground/70 block font-normal tracking-tight">
<Trans i18nKey={'marketing.additionalFeaturesSubheading'} />
</span>
</>
}
icon={
<FeatureShowcaseIconContainer>
<ZapIcon className="h-4 w-4" />
<span>
<Trans i18nKey={'marketing.additionalFeaturesLabel'} />
</span>
</FeatureShowcaseIconContainer>
}
>
<div className="mt-2 grid w-full grid-cols-1 gap-4 md:mt-6 md:grid-cols-2 lg:grid-cols-3">
<IconFeatureCard
icon={FileTextIcon}
titleKey="marketing.featureDocumentsTitle"
descKey="marketing.featureDocumentsDesc"
/>
<IconFeatureCard
icon={GlobeIcon}
titleKey="marketing.featureSiteBuilderTitle"
descKey="marketing.featureSiteBuilderDesc"
/>
<IconFeatureCard
icon={LayoutDashboardIcon}
titleKey="marketing.featureModulesTitle"
descKey="marketing.featureModulesDesc"
/>
</div>
</FeatureShowcase>
<AnimateOnScroll>
<div className={'container mx-auto'}>
<div className={'py-4 xl:py-8'}>
<FeatureShowcase
heading={
<>
<b className="font-medium tracking-tight dark:text-white">
<Trans i18nKey={'marketing.additionalFeaturesHeading'} />
</b>
.{' '}
<span className="text-secondary-foreground/70 block font-normal tracking-tight">
<Trans i18nKey={'marketing.additionalFeaturesSubheading'} />
</span>
</>
}
icon={
<FeatureShowcaseIconContainer>
<ZapIcon className="h-4 w-4" />
<span>
<Trans i18nKey={'marketing.additionalFeaturesLabel'} />
</span>
</FeatureShowcaseIconContainer>
}
>
<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
icon={FileTextIcon}
titleKey="marketing.featureDocumentsTitle"
descKey="marketing.featureDocumentsDesc"
accentBg="bg-chart-1/10"
accentText="text-chart-1"
/>
<IconFeatureCard
icon={GlobeIcon}
titleKey="marketing.featureSiteBuilderTitle"
descKey="marketing.featureSiteBuilderDesc"
accentBg="bg-chart-2/10"
accentText="text-chart-2"
/>
<IconFeatureCard
icon={LayoutDashboardIcon}
titleKey="marketing.featureModulesTitle"
descKey="marketing.featureModulesDesc"
accentBg="bg-chart-3/10"
accentText="text-chart-3"
/>
</div>
</FeatureShowcase>
</div>
</div>
</div>
</AnimateOnScroll>
{/* Why Choose Us Section */}
<div className={'container mx-auto'}>
<EcosystemShowcase
heading={<Trans i18nKey={'marketing.whyChooseHeading'} />}
description={<Trans i18nKey={'marketing.whyChooseDescription'} />}
textPosition="right"
>
<div className="flex flex-col gap-6">
<WhyItem
icon={SmartphoneIcon}
titleKey="marketing.whyResponsiveTitle"
descKey="marketing.whyResponsiveDesc"
/>
<WhyItem
icon={LockIcon}
titleKey="marketing.whySecureTitle"
descKey="marketing.whySecureDesc"
/>
<WhyItem
icon={HeadsetIcon}
titleKey="marketing.whySupportTitle"
descKey="marketing.whySupportDesc"
/>
<WhyItem
icon={ShieldCheckIcon}
titleKey="marketing.whyGdprTitle"
descKey="marketing.whyGdprDesc"
/>
</div>
</EcosystemShowcase>
</div>
<AnimateOnScroll>
<div className={'container mx-auto'}>
<EcosystemShowcase
heading={<Trans i18nKey={'marketing.whyChooseHeading'} />}
description={<Trans i18nKey={'marketing.whyChooseDescription'} />}
textPosition="right"
className="border-primary/10 rounded-xl border"
>
<div className="flex flex-col gap-6">
<WhyItem
icon={SmartphoneIcon}
titleKey="marketing.whyResponsiveTitle"
descKey="marketing.whyResponsiveDesc"
/>
<WhyItem
icon={LockIcon}
titleKey="marketing.whySecureTitle"
descKey="marketing.whySecureDesc"
/>
<WhyItem
icon={HeadsetIcon}
titleKey="marketing.whySupportTitle"
descKey="marketing.whySupportDesc"
/>
<WhyItem
icon={ShieldCheckIcon}
titleKey="marketing.whyGdprTitle"
descKey="marketing.whyGdprDesc"
/>
</div>
</EcosystemShowcase>
</div>
</AnimateOnScroll>
{/* How It Works */}
<div className="container mx-auto">
<div className="flex flex-col items-center gap-12">
<div className="text-center">
<h2 className="text-3xl font-medium tracking-tight xl:text-5xl dark:text-white">
<Trans i18nKey={'marketing.howItWorksHeading'} />
</h2>
<p className="text-secondary-foreground/70 mx-auto mt-4 max-w-2xl text-xl font-medium tracking-tight">
<Trans i18nKey={'marketing.howItWorksSubheading'} />
</p>
</div>
<AnimateOnScroll>
<div className="container mx-auto">
<div className="flex flex-col items-center gap-12">
<div className="text-center">
<h2 className="text-3xl font-medium tracking-tight xl:text-5xl dark:text-white">
<Trans i18nKey={'marketing.howItWorksHeading'} />
</h2>
<p className="text-secondary-foreground/70 mx-auto mt-4 max-w-2xl text-xl font-medium tracking-tight">
<Trans i18nKey={'marketing.howItWorksSubheading'} />
</p>
</div>
<div className="grid w-full grid-cols-1 gap-8 md:grid-cols-3">
<StepCard
step="01"
titleKey="marketing.howStep1Title"
descKey="marketing.howStep1Desc"
/>
<StepCard
step="02"
titleKey="marketing.howStep2Title"
descKey="marketing.howStep2Desc"
/>
<StepCard
step="03"
titleKey="marketing.howStep3Title"
descKey="marketing.howStep3Desc"
/>
<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
step="01"
titleKey="marketing.howStep1Title"
descKey="marketing.howStep1Desc"
/>
<StepCard
step="02"
titleKey="marketing.howStep2Title"
descKey="marketing.howStep2Desc"
/>
<StepCard
step="03"
titleKey="marketing.howStep3Title"
descKey="marketing.howStep3Desc"
/>
</div>
</div>
</div>
</div>
</AnimateOnScroll>
{/* Pricing Section */}
<div className={'container mx-auto'}>
<div
className={
'flex flex-col items-center justify-center space-y-12 py-4 xl:py-8'
}
>
<SecondaryHero
pill={
<Pill label={<Trans i18nKey={'marketing.pricingPillLabel'} />}>
<Trans i18nKey={'marketing.pricingPillText'} />
</Pill>
<AnimateOnScroll>
<div className={'container mx-auto'}>
<div
className={
'flex flex-col items-center justify-center space-y-12 py-4 xl:py-8'
}
heading={<Trans i18nKey={'marketing.pricingHeading'} />}
subheading={<Trans i18nKey={'marketing.pricingSubheading'} />}
/>
<div className={'w-full'}>
<PricingTable
config={billingConfig}
paths={{
signUp: pathsConfig.auth.signUp,
return: pathsConfig.app.home,
}}
>
<SecondaryHero
pill={
<Pill label={<Trans i18nKey={'marketing.pricingPillLabel'} />}>
<Trans i18nKey={'marketing.pricingPillText'} />
</Pill>
}
heading={
<GradientText className="from-primary to-primary/60">
<Trans i18nKey={'marketing.pricingHeading'} />
</GradientText>
}
subheading={<Trans i18nKey={'marketing.pricingSubheading'} />}
/>
<div className={'w-full'}>
<PricingTable
config={billingConfig}
paths={{
signUp: pathsConfig.auth.signUp,
return: pathsConfig.app.home,
}}
/>
</div>
</div>
</div>
</div>
</AnimateOnScroll>
{/* Final CTA */}
<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">
<h2 className="max-w-3xl text-3xl font-medium tracking-tight xl:text-5xl dark:text-white">
<Trans i18nKey={'marketing.ctaHeading'} />
</h2>
<p className="text-secondary-foreground/70 max-w-2xl text-lg">
<Trans i18nKey={'marketing.ctaDescription'} />
</p>
<div className="flex flex-col gap-3 sm:flex-row">
<CtaButton className="h-12 px-8 text-base">
<Link href={'/auth/sign-up'}>
<span className="flex items-center gap-2">
<Trans i18nKey={'marketing.ctaButtonPrimary'} />
<ArrowRightIcon className="h-4 w-4" />
</span>
</Link>
</CtaButton>
<CtaButton variant={'outline'} className="h-12 px-8 text-base">
<Link href={'/contact'}>
<Trans i18nKey={'marketing.ctaButtonSecondary'} />
</Link>
</CtaButton>
<AnimateOnScroll>
<div className="container mx-auto">
<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">
<GradientText className="from-primary to-primary/60">
<Trans i18nKey={'marketing.ctaHeading'} />
</GradientText>
</h2>
<p className="text-secondary-foreground/70 max-w-2xl text-lg">
<Trans i18nKey={'marketing.ctaDescription'} />
</p>
<div className="flex flex-col gap-3 sm:flex-row">
<CtaButton className="h-14 px-10 text-lg">
<Link href={'/auth/sign-up'}>
<span className="flex items-center gap-2">
<Trans i18nKey={'marketing.ctaButtonPrimary'} />
<ArrowRightIcon className="h-5 w-5" />
</span>
</Link>
</CtaButton>
<CtaButton variant={'outline'} className="h-14 px-10 text-lg">
<Link href={'/contact'}>
<Trans i18nKey={'marketing.ctaButtonSecondary'} />
</Link>
</CtaButton>
</div>
<p className="text-muted-foreground flex items-center gap-2 text-sm">
<CheckIcon className="h-4 w-4" />
<Trans i18nKey={'marketing.ctaNote'} />
</p>
</div>
<p className="text-muted-foreground flex items-center gap-2 text-sm">
<CheckIcon className="h-4 w-4" />
<Trans i18nKey={'marketing.ctaNote'} />
</p>
</div>
</div>
</AnimateOnScroll>
</div>
);
}
@@ -347,7 +413,7 @@ export default Home;
function MainCallToActionButton() {
return (
<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'}>
<span className={'flex items-center space-x-0.5'}>
<span>
@@ -364,7 +430,7 @@ function MainCallToActionButton() {
</Link>
</CtaButton>
<CtaButton variant={'link'} className="h-10 text-sm">
<CtaButton variant={'link'} className="h-12 px-8 text-base">
<Link href={'/pricing'}>
<Trans i18nKey={'common.pricing'} />
</Link>
@@ -377,11 +443,20 @@ function IconFeatureCard(props: {
icon: React.ComponentType<{ className?: string }>;
titleKey: string;
descKey: string;
accentBg?: string;
accentText?: string;
}) {
return (
<div className="bg-muted/50 flex flex-col gap-3 rounded p-6">
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg">
<props.icon className="text-primary h-5 w-5" />
<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={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>
<h4 className="text-lg font-medium">
<Trans i18nKey={props.titleKey} />
@@ -393,16 +468,39 @@ function IconFeatureCard(props: {
);
}
function TrustItem(props: {
icon: React.ComponentType<{ className?: string }>;
label: string;
function StatItem(props: { value: string; labelKey: string }) {
return (
<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 (
<div className="text-muted-foreground flex items-center gap-2.5">
<props.icon className="h-5 w-5" />
<span className="text-sm font-medium">
<Trans i18nKey={props.label} />
</span>
<div className="reveal border-border bg-card flex flex-col gap-4 rounded-xl border p-6 shadow-sm">
<p className="text-secondary-foreground text-sm leading-relaxed italic">
&ldquo;
<Trans i18nKey={props.quoteKey} />
&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>
);
}
@@ -414,7 +512,7 @@ function WhyItem(props: {
}) {
return (
<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" />
</div>
<div>
@@ -431,8 +529,10 @@ function WhyItem(props: {
function StepCard(props: { step: string; titleKey: string; descKey: string }) {
return (
<div className="bg-muted/50 relative flex flex-col gap-4 rounded-lg p-6">
<span className="text-primary/20 text-6xl font-bold">{props.step}</span>
<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">
<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">
<Trans i18nKey={props.titleKey} />
</h3>

View File

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

View File

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

View File

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

View File

@@ -69,12 +69,16 @@ export function AttendanceGrid({
Keine Teilnehmer in diesem Kurs
</p>
) : (
<div className="rounded-md border">
<table className="w-full text-sm">
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Teilnehmer</th>
<th className="p-3 text-center font-medium">Anwesend</th>
<th scope="col" className="p-3 text-left font-medium">
Teilnehmer
</th>
<th scope="col" className="p-3 text-center font-medium">
Anwesend
</th>
</tr>
</thead>
<tbody>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { createCourseManagementApi } from '@kit/course-management/api';
import { CreateCourseForm } from '@kit/course-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -11,6 +12,7 @@ interface PageProps {
export default async function EditCoursePage({ params }: PageProps) {
const { account, courseId } = await params;
const t = await getTranslations('courses');
const client = getSupabaseServerClient();
const { data: acct } = await client
@@ -28,7 +30,10 @@ export default async function EditCoursePage({ params }: PageProps) {
const c = course as Record<string, unknown>;
return (
<CmsPageShell account={account} title={`${String(c.name)} — Bearbeiten`}>
<CmsPageShell
account={account}
title={`${String(c.name)}${t('pages.editCourseTitle')}`}
>
<CreateCourseForm
accountId={acct.id}
account={account}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Button } from '@kit/ui/button';
@@ -23,15 +24,6 @@ interface PageProps {
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({
params,
searchParams,
@@ -39,6 +31,7 @@ export default async function GenerateDocumentPage({
const { account } = await params;
const { type } = await searchParams;
const client = getSupabaseServerClient();
const t = await getTranslations('documents');
const { data: acct } = await client
.from('accounts')
@@ -49,10 +42,13 @@ export default async function GenerateDocumentPage({
if (!acct) return <AccountNotFound />;
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 (
<CmsPageShell account={account} title="Dokument generieren">
<CmsPageShell account={account} title={t('generate.title')}>
<div className="flex w-full flex-col gap-6">
{/* Back link */}
<div>
@@ -61,16 +57,16 @@ export default async function GenerateDocumentPage({
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
>
<ArrowLeft className="mr-1 h-4 w-4" />
Zurück zu Dokumente
{t('generate.backToDocuments')}
</Link>
</div>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>{selectedLabel} generieren</CardTitle>
<CardDescription>
Wählen Sie den Dokumenttyp und die gewünschten Optionen.
</CardDescription>
<CardTitle>
{t('generate.generateLabel', { label: selectedLabel })}
</CardTitle>
<CardDescription>{t('generate.chooseOptions')}</CardDescription>
</CardHeader>
<CardContent>
@@ -82,7 +78,7 @@ export default async function GenerateDocumentPage({
<CardFooter>
<Link href={`/home/${account}/documents`}>
<Button variant="outline">Abbrechen</Button>
<Button variant="outline">{t('generate.cancel')}</Button>
</Link>
</CardFooter>
</Card>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import { Ticket, Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
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 { Button } from '@kit/ui/button';
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 items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{t('holidayPasses')}</h1>
<p className="text-muted-foreground">
{t('holidayPassesDescription')}
</p>
@@ -62,19 +61,23 @@ export default async function HolidayPassesPage({ params }: PageProps) {
</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<table className="w-full text-sm">
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">{t('name')}</th>
<th className="p-3 text-left font-medium">{t('year')}</th>
<th className="p-3 text-right font-medium">
<th scope="col" className="p-3 text-left font-medium">
{t('name')}
</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')}
</th>
<th className="p-3 text-left font-medium">
<th scope="col" className="p-3 text-left font-medium">
{t('validFrom')}
</th>
<th className="p-3 text-left font-medium">
<th scope="col" className="p-3 text-left font-medium">
{t('validUntil')}
</th>
</tr>
@@ -89,7 +92,7 @@ export default async function HolidayPassesPage({ params }: PageProps) {
<td className="p-3">{String(pass.year ?? '—')}</td>
<td className="p-3 text-right">
{pass.price != null
? `${Number(pass.price).toFixed(2)}`
? formatCurrencyAmount(pass.price as number)
: '—'}
</td>
<td className="p-3">

View File

@@ -5,6 +5,7 @@ import { useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Download, FileIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { deleteFile } from '@kit/module-builder/actions/file-actions';
import { Badge } from '@kit/ui/badge';
@@ -67,6 +68,7 @@ function getMimeLabel(mimeType: string): string {
}
export function FilesTable({ files, pagination }: FilesTableProps) {
const t = useTranslations('common');
const router = useRouter();
const searchParams = useSearchParams();
const { total, page, pageSize } = pagination;
@@ -104,15 +106,25 @@ export function FilesTable({ files, pagination }: FilesTableProps) {
</p>
</div>
) : (
<div className="rounded-md border">
<table className="w-full text-sm">
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Dateiname</th>
<th className="p-3 text-left font-medium">Typ</th>
<th className="p-3 text-right font-medium">Größe</th>
<th className="p-3 text-left font-medium">Hochgeladen</th>
<th className="p-3 text-right font-medium">Aktionen</th>
<th scope="col" className="p-3 text-left font-medium">
Dateiname
</th>
<th scope="col" className="p-3 text-left font-medium">
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>
</thead>
<tbody>
@@ -149,8 +161,8 @@ export function FilesTable({ files, pagination }: FilesTableProps) {
</Button>
</a>
<DeleteConfirmButton
title="Datei löschen"
description="Möchten Sie diese Datei wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden."
title={t('deleteFile')}
description={t('deleteFileConfirm')}
isPending={isDeleting}
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 { getSupabaseServerClient } from '@kit/supabase/server-client';
import { ListToolbar } from '@kit/ui/list-toolbar';
@@ -15,6 +17,7 @@ interface Props {
export default async function FilesPage({ params, searchParams }: Props) {
const { account } = await params;
const t = await getTranslations('common');
const search = await searchParams;
const client = getSupabaseServerClient();
@@ -51,12 +54,12 @@ export default async function FilesPage({ params, searchParams }: Props) {
return (
<CmsPageShell
account={account}
title="Dateien"
description="Dateien hochladen und verwalten"
title={t('filesTitle')}
description={t('filesSubtitle')}
>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<ListToolbar searchPlaceholder="Datei suchen..." />
<ListToolbar searchPlaceholder={t('filesSearch')} />
<FileUploadDialog accountId={acct.id} />
</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 { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -10,6 +12,7 @@ interface Props {
export default async function NewInvoicePage({ params }: Props) {
const { account } = await params;
const t = await getTranslations('finance');
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
@@ -21,8 +24,8 @@ export default async function NewInvoicePage({ params }: Props) {
return (
<CmsPageShell
account={account}
title="Neue Rechnung"
description="Rechnung mit Positionen erstellen"
title={t('invoices.newInvoice')}
description={t('invoices.newInvoiceDesc')}
>
<CreateInvoiceForm accountId={acct.id} account={account} />
</CmsPageShell>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { createMemberManagementApi } from '@kit/member-management/api';
import { EditMemberForm } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -11,6 +12,7 @@ interface Props {
export default async function EditMemberPage({ params }: Props) {
const { account, memberId } = await params;
const t = await getTranslations('members');
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
@@ -21,12 +23,12 @@ export default async function EditMemberPage({ params }: Props) {
const api = createMemberManagementApi(client);
const member = await api.getMember(memberId);
if (!member) return <div>Mitglied nicht gefunden</div>;
if (!member) return <div>{t('detail.notFound')}</div>;
return (
<CmsPageShell
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} />
</CmsPageShell>

View File

@@ -21,7 +21,7 @@ export default async function MemberDetailPage({ params }: Props) {
const api = createMemberManagementApi(client);
const member = await api.getMember(memberId);
if (!member) return <div>Mitglied nicht gefunden</div>;
if (!member) return <AccountNotFound />;
// Fetch sub-entities in parallel
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 { ApplicationWorkflow } from '@kit/member-management/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -12,6 +14,7 @@ interface Props {
export default async function ApplicationsPage({ params }: Props) {
const { account } = await params;
const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client
.from('accounts')
.select('id')
@@ -25,8 +28,8 @@ export default async function ApplicationsPage({ params }: Props) {
return (
<CmsPageShell
account={account}
title="Aufnahmeanträge"
description="Mitgliedsanträge bearbeiten"
title={t('nav.applications')}
description={t('applications.subtitle')}
>
<ApplicationWorkflow
applications={applications}

View File

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

View File

@@ -5,6 +5,7 @@ import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Plus } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { createDepartment } from '@kit/member-management/actions/member-actions';
import { Button } from '@kit/ui/button';
@@ -28,14 +29,15 @@ interface CreateDepartmentDialogProps {
export function CreateDepartmentDialog({
accountId,
}: CreateDepartmentDialogProps) {
const t = useTranslations('members.departments');
const router = useRouter();
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const { execute, isPending } = useActionWithToast(createDepartment, {
successMessage: 'Abteilung erstellt',
errorMessage: 'Fehler beim Erstellen der Abteilung',
successMessage: t('created'),
errorMessage: t('createError'),
onSuccess: () => {
setOpen(false);
setName('');
@@ -61,42 +63,42 @@ export function CreateDepartmentDialog({
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button size="sm" />}>
<Plus className="mr-1 h-4 w-4" />
Neue Abteilung
{t('newDepartment')}
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Neue Abteilung</DialogTitle>
<DialogTitle>{t('newDepartment')}</DialogTitle>
<DialogDescription>
Erstellen Sie eine neue Abteilung oder Sparte für Ihren Verein.
{t('createDialogDescription')}
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
<div className="space-y-2">
<Label htmlFor="dept-name">Name</Label>
<Label htmlFor="dept-name">{t('name')}</Label>
<Input
id="dept-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="z. B. Jugendabteilung"
placeholder={t('namePlaceholder')}
required
minLength={1}
maxLength={128}
/>
</div>
<div className="space-y-2">
<Label htmlFor="dept-description">Beschreibung (optional)</Label>
<Label htmlFor="dept-description">{t('descriptionLabel')}</Label>
<Input
id="dept-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Kurze Beschreibung"
placeholder={t('descriptionPlaceholder')}
/>
</div>
</div>
<DialogFooter className="mt-4">
<Button type="submit" disabled={isPending || !name.trim()}>
{isPending ? 'Wird erstellt…' : 'Erstellen'}
{isPending ? t('creating') : t('create')}
</Button>
</DialogFooter>
</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 { getTranslations } from 'next-intl/server';
import { createMemberManagementApi } from '@kit/member-management/api';
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 { CreateDepartmentDialog } from './create-department-dialog';
import { DeleteDepartmentButton } from './delete-department-button';
interface Props {
params: Promise<{ account: string }>;
@@ -16,6 +18,7 @@ interface Props {
export default async function DepartmentsPage({ params }: Props) {
const { account } = await params;
const client = getSupabaseServerClient();
const t = await getTranslations('members');
const { data: acct } = await client
.from('accounts')
.select('id')
@@ -29,8 +32,8 @@ export default async function DepartmentsPage({ params }: Props) {
return (
<CmsPageShell
account={account}
title="Abteilungen"
description="Sparten und Abteilungen verwalten"
title={t('departments.title')}
description={t('departments.subtitle')}
>
<div className="space-y-4">
<div className="flex items-center justify-end">
@@ -40,16 +43,23 @@ export default async function DepartmentsPage({ params }: Props) {
{departments.length === 0 ? (
<EmptyState
icon={<Users className="h-8 w-8" />}
title="Keine Abteilungen vorhanden"
description="Erstellen Sie Ihre erste Abteilung."
title={t('departments.noDepartments')}
description={t('departments.createFirst')}
/>
) : (
<div className="rounded-md border">
<table className="w-full text-sm">
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[640px] text-sm">
<thead>
<tr className="bg-muted/50 border-b">
<th className="p-3 text-left font-medium">Name</th>
<th className="p-3 text-left font-medium">Beschreibung</th>
<th scope="col" className="p-3 text-left font-medium">
{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>
</thead>
<tbody>
@@ -62,6 +72,15 @@ export default async function DepartmentsPage({ params }: Props) {
<td className="text-muted-foreground p-3">
{String(dept.description ?? '—')}
</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>
))}
</tbody>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { createModuleBuilderApi } from '@kit/module-builder/api';
import { ModuleForm } from '@kit/module-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -15,6 +16,7 @@ export default async function RecordDetailPage({
params,
}: RecordDetailPageProps) {
const { account, moduleId, recordId } = await params;
const t = await getTranslations('cms.modules');
const client = getSupabaseServerClient();
const api = createModuleBuilderApi(client);
@@ -31,14 +33,14 @@ export default async function RecordDetailPage({
api.records.getRecord(recordId),
]);
if (!moduleWithFields || !record) return <div>Nicht gefunden</div>;
if (!moduleWithFields || !record) return <div>{t('notFound')}</div>;
const fields = moduleWithFields.fields;
return (
<CmsPageShell
account={account}
title={`${String(moduleWithFields.display_name)}Datensatz`}
title={`${String(moduleWithFields.display_name)}${t('record')}`}
>
<RecordDetailClient
fields={fields as Parameters<typeof ModuleForm>[0]['fields']}

View File

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

View File

@@ -310,11 +310,13 @@ export function ImportWizard({
</div>
) : (
<>
<div className="max-h-80 overflow-auto rounded-md border">
<table className="w-full text-xs">
<div className="max-h-80 overflow-x-auto rounded-md border">
<table className="w-full min-w-[640px] text-xs">
<thead>
<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) => (
<th key={f.name} className="p-2 text-left">
{f.display_name}

View File

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

View File

@@ -1,6 +1,7 @@
import { createModuleBuilderApi } from '@kit/module-builder/api';
import { ModuleForm } from '@kit/module-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -13,6 +14,7 @@ interface NewRecordPageProps {
export default async function NewRecordPage({ params }: NewRecordPageProps) {
const { account, moduleId } = await params;
const t = await getTranslations('cms.modules');
const client = getSupabaseServerClient();
const api = createModuleBuilderApi(client);
@@ -25,14 +27,14 @@ export default async function NewRecordPage({ params }: NewRecordPageProps) {
if (!accountData) return <AccountNotFound />;
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;
return (
<CmsPageShell
account={account}
title={`${String(moduleWithFields.display_name)}Neuer Datensatz`}
title={`${String(moduleWithFields.display_name)}${t('newRecord')}`}
>
<div className="mx-auto max-w-3xl">
<CreateRecordForm

View File

@@ -1,11 +1,15 @@
import Link from 'next/link';
import { Plus } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { createModuleBuilderApi } from '@kit/module-builder/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
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 { ModuleRecordsTable } from './module-records-table';
import { ModuleSearchBar } from './module-search-bar';
@@ -22,12 +26,13 @@ export default async function ModuleDetailPage({
const { account, moduleId } = await params;
const search = await searchParams;
const client = getSupabaseServerClient();
const t = await getTranslations('cms.modules');
const api = createModuleBuilderApi(client);
const moduleWithFields = await api.modules.getModuleWithFields(moduleId);
if (!moduleWithFields) {
return <div>Modul nicht gefunden</div>;
return <AccountNotFound />;
}
const page = Number(search.page) || 1;
@@ -50,7 +55,20 @@ export default async function ModuleDetailPage({
sortField,
sortDirection,
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;
@@ -64,38 +82,43 @@ export default async function ModuleDetailPage({
}));
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">
{moduleWithFields.display_name}
</h1>
{moduleWithFields.description && (
<p className="text-muted-foreground">
{moduleWithFields.description}
</p>
)}
<CmsPageShell
account={account}
title={String(moduleWithFields.display_name)}
description={moduleWithFields.description ?? undefined}
>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
{moduleWithFields.description && (
<p className="text-muted-foreground">
{moduleWithFields.description}
</p>
)}
</div>
<Button asChild>
<Link href={`/home/${account}/modules/${moduleId}/new`}>
<Plus className="mr-2 h-4 w-4" />
{t('newRecord')}
</Link>
</Button>
</div>
<Button asChild>
<Link href={`/home/${account}/modules/${moduleId}/new`}>
<Plus className="mr-2 h-4 w-4" />
Neuer Datensatz
</Link>
</Button>
<ModuleSearchBar fields={allFields} />
<ModuleRecordsTable
fields={allFields}
records={records}
pagination={result.pagination}
account={account}
moduleId={moduleId}
currentSort={
sortField
? { field: sortField, direction: sortDirection }
: undefined
}
/>
</div>
<ModuleSearchBar fields={allFields} />
<ModuleRecordsTable
fields={allFields}
records={records}
pagination={result.pagination}
account={account}
moduleId={moduleId}
currentSort={
sortField ? { field: sortField, direction: sortDirection } : undefined
}
/>
</div>
</CmsPageShell>
);
}

View File

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

View File

@@ -135,13 +135,15 @@ export function ModulePermissions({
<table className="w-full text-sm">
<thead>
<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) => (
<th key={col.key} className="p-3 text-center text-xs">
{col.label}
</th>
))}
<th className="p-3 text-right" />
<th scope="col" className="p-3 text-right" />
</tr>
</thead>
<tbody>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { CreatePageForm } from '@kit/site-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -10,6 +11,7 @@ interface Props {
export default async function NewSitePage({ params }: Props) {
const { account } = await params;
const t = await getTranslations('siteBuilder');
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
@@ -21,8 +23,8 @@ export default async function NewSitePage({ params }: Props) {
return (
<CmsPageShell
account={account}
title="Neue Seite"
description="Seite für Ihre Vereinswebsite erstellen"
title={t('pages.newPage')}
description={t('pages.newPageDescription')}
>
<CreatePageForm accountId={acct.id} account={account} />
</CmsPageShell>

View File

@@ -1,5 +1,6 @@
import { CreatePostForm } from '@kit/site-builder/components';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { getTranslations } from 'next-intl/server';
import { AccountNotFound } from '~/components/account-not-found';
import { CmsPageShell } from '~/components/cms-page-shell';
@@ -10,6 +11,7 @@ interface Props {
export default async function NewPostPage({ params }: Props) {
const { account } = await params;
const t = await getTranslations('siteBuilder');
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
@@ -21,8 +23,8 @@ export default async function NewPostPage({ params }: Props) {
return (
<CmsPageShell
account={account}
title="Neuer Beitrag"
description="Beitrag erstellen"
title={t('posts.newPost')}
description={t('posts.newPostDescription')}
>
<CreatePostForm accountId={acct.id} account={account} />
</CmsPageShell>

View File

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

View File

@@ -1,3 +1,5 @@
import { getTranslations } from 'next-intl/server';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createVerbandApi } from '@kit/verbandsverwaltung/api';
import {
@@ -15,6 +17,7 @@ interface Props {
export default async function EditClubPage({ params }: Props) {
const { account, clubId } = await params;
const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client
.from('accounts')
@@ -35,7 +38,7 @@ export default async function EditClubPage({ params }: Props) {
return (
<CmsPageShell
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" />
<CreateClubForm

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
import { useState } from 'react';
import { Plus, Pencil, Trash2, Settings } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
@@ -141,7 +142,7 @@ function SettingsSection({
<>
<div>
<span className="font-medium">{String(item.name)}</span>
{item.description && (
{Boolean(item.description) && (
<p className="text-muted-foreground text-xs">
{String(item.description)}
</p>
@@ -183,6 +184,7 @@ export default function SettingsContent({
types,
feeTypes,
}: SettingsContentProps) {
const t = useTranslations('verband');
// Roles
const { execute: execCreateRole, isPending: isCreatingRole } =
useActionWithToast(createRole, {
@@ -228,14 +230,13 @@ export default function SettingsContent({
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Einstellungen</h1>
<p className="text-muted-foreground">
Funktionen, Vereinstypen und Beitragsarten verwalten
{t('settings.subtitle')}
</p>
</div>
<SettingsSection
title="Funktionen (Rollen)"
title={t('settings.roles')}
items={roles}
onAdd={(name, description) =>
execCreateRole({ accountId, name, description, sortOrder: 0 })
@@ -247,7 +248,7 @@ export default function SettingsContent({
/>
<SettingsSection
title="Vereinstypen"
title={t('settings.types')}
items={types}
onAdd={(name, description) =>
execCreateType({ accountId, name, description, sortOrder: 0 })
@@ -259,7 +260,7 @@ export default function SettingsContent({
/>
<SettingsSection
title="Beitragsarten"
title={t('settings.feeTypes')}
items={feeTypes}
onAdd={(name, description) =>
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 { createVerbandApi } from '@kit/verbandsverwaltung/api';
import { VerbandTabNavigation } from '@kit/verbandsverwaltung/components';
@@ -14,6 +16,7 @@ interface Props {
export default async function SettingsPage({ params }: Props) {
const { account } = await params;
const client = getSupabaseServerClient();
const t = await getTranslations('verband');
const { data: acct } = await client
.from('accounts')
@@ -31,7 +34,7 @@ export default async function SettingsPage({ params }: Props) {
]);
return (
<CmsPageShell account={account} title="Verbandsverwaltung - Einstellungen">
<CmsPageShell account={account} title={t('pages.settingsTitle')}>
<VerbandTabNavigation account={account} activeTab="settings" />
<SettingsContent
accountId={acct.id}

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