feat: pre-existing local changes — fischerei, verband, modules, members, packages
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:
1
.bg-shell/manifest.json
Normal file
1
.bg-shell/manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
apps/web/.bg-shell/manifest.json
Normal file
1
apps/web/.bg-shell/manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
“
|
||||
<Trans i18nKey={props.quoteKey} />
|
||||
”
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 })}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>>}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -169,7 +169,6 @@ async function SidebarLayout({
|
||||
userId={data.user.id}
|
||||
accounts={accounts}
|
||||
account={account}
|
||||
config={config}
|
||||
/>
|
||||
</div>
|
||||
</PageMobileNavigation>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user