feat: enhance accessibility and testing with data-test attributes and improve error handling
This commit is contained in:
@@ -39,7 +39,7 @@ export default async function GuestsPage({ params }: PageProps) {
|
|||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-muted-foreground">Gästeverwaltung</p>
|
<p className="text-muted-foreground">Gästeverwaltung</p>
|
||||||
<Button>
|
<Button data-test="guests-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neuer Gast
|
Neuer Gast
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ export default async function BookingsPage({
|
|||||||
// Post-filter by search query (guest name or room name/number)
|
// Post-filter by search query (guest name or room name/number)
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
const q = searchQuery.toLowerCase();
|
const q = searchQuery.toLowerCase();
|
||||||
bookingsData = bookingsData.filter((b) => {
|
bookingsData = bookingsData.filter((booking) => {
|
||||||
const room = b.room as Record<string, string> | null;
|
const room = booking.room as Record<string, string> | null;
|
||||||
const guest = b.guest as Record<string, string> | null;
|
const guest = booking.guest as Record<string, string> | null;
|
||||||
const roomName = (room?.name ?? '').toLowerCase();
|
const roomName = (room?.name ?? '').toLowerCase();
|
||||||
const roomNumber = (room?.room_number ?? '').toLowerCase();
|
const roomNumber = (room?.room_number ?? '').toLowerCase();
|
||||||
const guestFirst = (guest?.first_name ?? '').toLowerCase();
|
const guestFirst = (guest?.first_name ?? '').toLowerCase();
|
||||||
@@ -107,7 +107,8 @@ export default async function BookingsPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeBookings = bookingsData.filter(
|
const activeBookings = bookingsData.filter(
|
||||||
(b) => b.status === 'confirmed' || b.status === 'checked_in',
|
(booking) =>
|
||||||
|
booking.status === 'confirmed' || booking.status === 'checked_in',
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||||
@@ -122,7 +123,7 @@ export default async function BookingsPage({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link href={`/home/${account}/bookings/new`}>
|
<Link href={`/home/${account}/bookings/new`}>
|
||||||
<Button>
|
<Button data-test="bookings-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neue Buchung
|
Neue Buchung
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default async function RoomsPage({ params }: PageProps) {
|
|||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-muted-foreground">Zimmerverwaltung</p>
|
<p className="text-muted-foreground">Zimmerverwaltung</p>
|
||||||
<Button>
|
<Button data-test="rooms-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neues Zimmer
|
Neues Zimmer
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
|
||||||
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
import { EmptyState } from '~/components/empty-state';
|
import { EmptyState } from '~/components/empty-state';
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ export default async function AttendancePage({
|
|||||||
api.getParticipants(courseId),
|
api.getParticipants(courseId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!course) return <div>Kurs nicht gefunden</div>;
|
if (!course) return <AccountNotFound />;
|
||||||
|
|
||||||
const selectedSessionId =
|
const selectedSessionId =
|
||||||
(search.session as string) ??
|
(search.session as string) ??
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Badge } from '@kit/ui/badge';
|
|||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
|
||||||
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
@@ -52,12 +53,12 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
api.getSessions(courseId),
|
api.getSessions(courseId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!course) return <div>Kurs nicht gefunden</div>;
|
if (!course) return <AccountNotFound />;
|
||||||
|
|
||||||
const c = course as Record<string, unknown>;
|
const courseData = course as Record<string, unknown>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CmsPageShell account={account} title={String(c.name)}>
|
<CmsPageShell account={account} title={String(courseData.name)}>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
@@ -66,7 +67,7 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
<GraduationCap className="text-primary h-5 w-5" />
|
<GraduationCap className="text-primary h-5 w-5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Name</p>
|
<p className="text-muted-foreground text-xs">Name</p>
|
||||||
<p className="font-semibold">{String(c.name)}</p>
|
<p className="font-semibold">{String(courseData.name)}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -76,9 +77,12 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Status</p>
|
<p className="text-muted-foreground text-xs">Status</p>
|
||||||
<Badge
|
<Badge
|
||||||
variant={STATUS_VARIANT[String(c.status)] ?? 'secondary'}
|
variant={
|
||||||
|
STATUS_VARIANT[String(courseData.status)] ?? 'secondary'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{STATUS_LABEL[String(c.status)] ?? String(c.status)}
|
{STATUS_LABEL[String(courseData.status)] ??
|
||||||
|
String(courseData.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -89,7 +93,7 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Dozent</p>
|
<p className="text-muted-foreground text-xs">Dozent</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{String(c.instructor_id ?? '—')}
|
{String(courseData.instructor_id ?? '—')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -100,9 +104,9 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Beginn – Ende</p>
|
<p className="text-muted-foreground text-xs">Beginn – Ende</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{formatDate(c.start_date as string)}
|
{formatDate(courseData.start_date as string)}
|
||||||
{' – '}
|
{' – '}
|
||||||
{formatDate(c.end_date as string)}
|
{formatDate(courseData.end_date as string)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -113,7 +117,9 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Gebühr</p>
|
<p className="text-muted-foreground text-xs">Gebühr</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{c.fee != null ? `${Number(c.fee).toFixed(2)} €` : '—'}
|
{courseData.fee != null
|
||||||
|
? `${Number(courseData.fee).toFixed(2)} €`
|
||||||
|
: '—'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -124,7 +130,7 @@ export default async function CourseDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Teilnehmer</p>
|
<p className="text-muted-foreground text-xs">Teilnehmer</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{participants.length} / {String(c.capacity ?? '∞')}
|
{participants.length} / {String(courseData.capacity ?? '∞')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Badge } from '@kit/ui/badge';
|
|||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
|
|
||||||
|
import { AccountNotFound } from '~/components/account-not-found';
|
||||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||||
import { EmptyState } from '~/components/empty-state';
|
import { EmptyState } from '~/components/empty-state';
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export default async function ParticipantsPage({ params }: PageProps) {
|
|||||||
api.getParticipants(courseId),
|
api.getParticipants(courseId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!course) return <div>Kurs nicht gefunden</div>;
|
if (!course) return <AccountNotFound />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CmsPageShell account={account} title="Teilnehmer">
|
<CmsPageShell account={account} title="Teilnehmer">
|
||||||
@@ -56,7 +57,7 @@ export default async function ParticipantsPage({ params }: PageProps) {
|
|||||||
{participants.length} Teilnehmer
|
{participants.length} Teilnehmer
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button>
|
<Button data-test="participants-add-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Teilnehmer anmelden
|
Teilnehmer anmelden
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -67,10 +67,14 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
|||||||
const courseDates = new Set<number>();
|
const courseDates = new Set<number>();
|
||||||
|
|
||||||
for (const course of courses.data) {
|
for (const course of courses.data) {
|
||||||
const c = course as Record<string, unknown>;
|
const courseItem = course as Record<string, unknown>;
|
||||||
if (c.status === 'cancelled') continue;
|
if (courseItem.status === 'cancelled') continue;
|
||||||
const startDate = c.start_date ? new Date(String(c.start_date)) : null;
|
const startDate = courseItem.start_date
|
||||||
const endDate = c.end_date ? new Date(String(c.end_date)) : null;
|
? new Date(String(courseItem.start_date))
|
||||||
|
: null;
|
||||||
|
const endDate = courseItem.end_date
|
||||||
|
? new Date(String(courseItem.end_date))
|
||||||
|
: null;
|
||||||
|
|
||||||
if (!startDate) continue;
|
if (!startDate) continue;
|
||||||
|
|
||||||
@@ -112,8 +116,8 @@ export default async function CourseCalendarPage({ params }: PageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeCourses = courses.data.filter(
|
const activeCourses = courses.data.filter(
|
||||||
(c: Record<string, unknown>) =>
|
(courseItem: Record<string, unknown>) =>
|
||||||
c.status === 'open' || c.status === 'running',
|
courseItem.status === 'open' || courseItem.status === 'running',
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default async function CategoriesPage({ params }: PageProps) {
|
|||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-muted-foreground">Kurskategorien verwalten</p>
|
<p className="text-muted-foreground">Kurskategorien verwalten</p>
|
||||||
<Button>
|
<Button data-test="categories-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neue Kategorie
|
Neue Kategorie
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default async function InstructorsPage({ params }: PageProps) {
|
|||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-muted-foreground">Dozentenpool verwalten</p>
|
<p className="text-muted-foreground">Dozentenpool verwalten</p>
|
||||||
<Button>
|
<Button data-test="instructors-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neuer Dozent
|
Neuer Dozent
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default async function LocationsPage({ params }: PageProps) {
|
|||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Kurs- und Veranstaltungsorte verwalten
|
Kurs- und Veranstaltungsorte verwalten
|
||||||
</p>
|
</p>
|
||||||
<Button>
|
<Button data-test="locations-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neuer Ort
|
Neuer Ort
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default async function CoursesPage({ params, searchParams }: PageProps) {
|
|||||||
<p className="text-muted-foreground">Kursangebot verwalten</p>
|
<p className="text-muted-foreground">Kursangebot verwalten</p>
|
||||||
|
|
||||||
<Link href={`/home/${account}/courses/new`}>
|
<Link href={`/home/${account}/courses/new`}>
|
||||||
<Button>
|
<Button data-test="courses-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neuer Kurs
|
Neuer Kurs
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
|||||||
<select
|
<select
|
||||||
id="documentType"
|
id="documentType"
|
||||||
name="documentType"
|
name="documentType"
|
||||||
|
data-test="document-type-select"
|
||||||
value={selectedType}
|
value={selectedType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedType(e.target.value);
|
setSelectedType(e.target.value);
|
||||||
@@ -190,7 +191,11 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
|||||||
|
|
||||||
{/* Submit button */}
|
{/* Submit button */}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" disabled={isPending || isComingSoon}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
data-test="document-generate-btn"
|
||||||
|
disabled={isPending || isComingSoon}
|
||||||
|
>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
|||||||
@@ -259,10 +259,10 @@ async function generateMemberCards(
|
|||||||
orientation: input.orientation,
|
orientation: input.orientation,
|
||||||
style: s.page,
|
style: s.page,
|
||||||
},
|
},
|
||||||
...batch.map((m) =>
|
...batch.map((memberItem) =>
|
||||||
React.createElement(
|
React.createElement(
|
||||||
View,
|
View,
|
||||||
{ key: m.id, style: s.card },
|
{ key: memberItem.id, style: s.card },
|
||||||
// Accent bar
|
// Accent bar
|
||||||
React.createElement(View, { style: s.accentBar }),
|
React.createElement(View, { style: s.accentBar }),
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ async function generateMemberCards(
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Text,
|
Text,
|
||||||
{ style: s.memberNumber },
|
{ style: s.memberNumber },
|
||||||
`Nr. ${m.member_number ?? '–'}`,
|
`Nr. ${memberItem.member_number ?? '–'}`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ async function generateMemberCards(
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Text,
|
Text,
|
||||||
{ style: s.memberName },
|
{ style: s.memberName },
|
||||||
`${m.first_name} ${m.last_name}`,
|
`${memberItem.first_name} ${memberItem.last_name}`,
|
||||||
),
|
),
|
||||||
React.createElement(
|
React.createElement(
|
||||||
View,
|
View,
|
||||||
@@ -326,7 +326,7 @@ async function generateMemberCards(
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Text,
|
Text,
|
||||||
{ style: s.fieldValue },
|
{ style: s.fieldValue },
|
||||||
fmtDate(m.entry_date),
|
fmtDate(memberItem.entry_date),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Date of birth
|
// Date of birth
|
||||||
@@ -341,7 +341,7 @@ async function generateMemberCards(
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Text,
|
Text,
|
||||||
{ style: s.fieldValue },
|
{ style: s.fieldValue },
|
||||||
fmtDate(m.date_of_birth),
|
fmtDate(memberItem.date_of_birth),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Address
|
// Address
|
||||||
@@ -356,13 +356,16 @@ async function generateMemberCards(
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Text,
|
Text,
|
||||||
{ style: s.fieldValue },
|
{ style: s.fieldValue },
|
||||||
[m.street, m.house_number].filter(Boolean).join(' ') ||
|
[memberItem.street, memberItem.house_number]
|
||||||
'–',
|
.filter(Boolean)
|
||||||
|
.join(' ') || '–',
|
||||||
),
|
),
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Text,
|
Text,
|
||||||
{ style: { ...s.fieldValue, marginTop: 1 } },
|
{ style: { ...s.fieldValue, marginTop: 1 } },
|
||||||
[m.postal_code, m.city].filter(Boolean).join(' ') || '',
|
[memberItem.postal_code, memberItem.city]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ') || '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -428,12 +431,20 @@ async function generateLabels(
|
|||||||
return { success: false, error: 'Keine aktiven Mitglieder.' };
|
return { success: false, error: 'Keine aktiven Mitglieder.' };
|
||||||
|
|
||||||
const api = createDocumentGeneratorApi();
|
const api = createDocumentGeneratorApi();
|
||||||
const records = members.map((m) => ({
|
const records = members.map((record) => ({
|
||||||
line1: [m.salutation, m.title, m.first_name, m.last_name]
|
line1: [
|
||||||
|
record.salutation,
|
||||||
|
record.title,
|
||||||
|
record.first_name,
|
||||||
|
record.last_name,
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' '),
|
.join(' '),
|
||||||
line2: [m.street, m.house_number].filter(Boolean).join(' ') || undefined,
|
line2:
|
||||||
line3: [m.postal_code, m.city].filter(Boolean).join(' ') || undefined,
|
[record.street, record.house_number].filter(Boolean).join(' ') ||
|
||||||
|
undefined,
|
||||||
|
line3:
|
||||||
|
[record.postal_code, record.city].filter(Boolean).join(' ') || undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const html = api.generateLabelsHtml({ labelFormat: 'avery-l7163', records });
|
const html = api.generateLabelsHtml({ labelFormat: 'avery-l7163', records });
|
||||||
@@ -500,16 +511,16 @@ async function generateMemberReport(
|
|||||||
excluded: 'Ausgeschlossen',
|
excluded: 'Ausgeschlossen',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const m of members) {
|
for (const member of members) {
|
||||||
ws.addRow({
|
ws.addRow({
|
||||||
nr: m.member_number ?? '',
|
nr: member.member_number ?? '',
|
||||||
name: m.last_name,
|
name: member.last_name,
|
||||||
vorname: m.first_name,
|
vorname: member.first_name,
|
||||||
email: m.email ?? '',
|
email: member.email ?? '',
|
||||||
plz: m.postal_code ?? '',
|
plz: member.postal_code ?? '',
|
||||||
ort: m.city ?? '',
|
ort: member.city ?? '',
|
||||||
status: SL[m.status] ?? m.status,
|
status: SL[member.status] ?? member.status,
|
||||||
eintritt: m.entry_date ? formatDate(m.entry_date) : '',
|
eintritt: member.entry_date ? formatDate(member.entry_date) : '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default async function DocumentTemplatesPage({ params }: PageProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button>
|
<Button data-test="document-templates-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neue Vorlage
|
Neue Vorlage
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -56,20 +56,21 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|||||||
|
|
||||||
if (!event) return <div>Veranstaltung nicht gefunden</div>;
|
if (!event) return <div>Veranstaltung nicht gefunden</div>;
|
||||||
|
|
||||||
const e = event as Record<string, unknown>;
|
const eventData = event as Record<string, unknown>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CmsPageShell account={account} title={String(e.name)}>
|
<CmsPageShell account={account} title={String(eventData.name)}>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">{String(e.name)}</h1>
|
<h1 className="text-2xl font-bold">{String(eventData.name)}</h1>
|
||||||
<Badge
|
<Badge
|
||||||
variant={STATUS_VARIANT[String(e.status)] ?? 'secondary'}
|
variant={STATUS_VARIANT[String(eventData.status)] ?? 'secondary'}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
>
|
>
|
||||||
{STATUS_LABEL[String(e.status)] ?? String(e.status)}
|
{STATUS_LABEL[String(eventData.status)] ??
|
||||||
|
String(eventData.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button>
|
<Button>
|
||||||
@@ -86,7 +87,7 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Datum</p>
|
<p className="text-muted-foreground text-xs">Datum</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{formatDate(e.event_date as string)}
|
{formatDate(eventData.event_date as string)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -97,7 +98,8 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Uhrzeit</p>
|
<p className="text-muted-foreground text-xs">Uhrzeit</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{String(e.start_time ?? '—')} – {String(e.end_time ?? '—')}
|
{String(eventData.start_time ?? '—')} –{' '}
|
||||||
|
{String(eventData.end_time ?? '—')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -107,7 +109,9 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|||||||
<MapPin className="text-primary h-5 w-5" />
|
<MapPin className="text-primary h-5 w-5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Ort</p>
|
<p className="text-muted-foreground text-xs">Ort</p>
|
||||||
<p className="font-semibold">{String(e.location ?? '—')}</p>
|
<p className="font-semibold">
|
||||||
|
{String(eventData.location ?? '—')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -117,7 +121,7 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-xs">Anmeldungen</p>
|
<p className="text-muted-foreground text-xs">Anmeldungen</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
{registrations.length} / {String(e.capacity ?? '∞')}
|
{registrations.length} / {String(eventData.capacity ?? '∞')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -125,14 +129,14 @@ export default async function EventDetailPage({ params }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{e.description ? (
|
{eventData.description ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Beschreibung</CardTitle>
|
<CardTitle>Beschreibung</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-muted-foreground text-sm whitespace-pre-wrap">
|
<p className="text-muted-foreground text-sm whitespace-pre-wrap">
|
||||||
{String(e.description)}
|
{String(eventData.description)}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -47,19 +47,21 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
|||||||
const events = await api.listEvents(acct.id, { page });
|
const events = await api.listEvents(acct.id, { page });
|
||||||
|
|
||||||
// Fetch registration counts for all events on this page
|
// Fetch registration counts for all events on this page
|
||||||
const eventIds = events.data.map((e: Record<string, unknown>) =>
|
const eventIds = events.data.map((eventItem: Record<string, unknown>) =>
|
||||||
String(e.id),
|
String(eventItem.id),
|
||||||
);
|
);
|
||||||
const registrationCounts = await api.getRegistrationCounts(eventIds);
|
const registrationCounts = await api.getRegistrationCounts(eventIds);
|
||||||
|
|
||||||
// Pre-compute stats before rendering
|
// Pre-compute stats before rendering
|
||||||
const uniqueLocationCount = new Set(
|
const uniqueLocationCount = new Set(
|
||||||
events.data.map((e: Record<string, unknown>) => e.location).filter(Boolean),
|
events.data
|
||||||
|
.map((eventItem: Record<string, unknown>) => eventItem.location)
|
||||||
|
.filter(Boolean),
|
||||||
).size;
|
).size;
|
||||||
|
|
||||||
const totalCapacity = events.data.reduce(
|
const totalCapacity = events.data.reduce(
|
||||||
(sum: number, e: Record<string, unknown>) =>
|
(sum: number, eventItem: Record<string, unknown>) =>
|
||||||
sum + (Number(e.capacity) || 0),
|
sum + (Number(eventItem.capacity) || 0),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ export default async function EventsPage({ params, searchParams }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={`/home/${account}/events/new`}>
|
<Link href={`/home/${account}/events/new`}>
|
||||||
<Button>
|
<Button data-test="events-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
{t('newEvent')}
|
{t('newEvent')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default async function InvoiceDetailPage({ params }: PageProps) {
|
|||||||
const api = createFinanceApi(client);
|
const api = createFinanceApi(client);
|
||||||
const invoice = await api.getInvoiceWithItems(id);
|
const invoice = await api.getInvoiceWithItems(id);
|
||||||
|
|
||||||
if (!invoice) return <div>Rechnung nicht gefunden</div>;
|
if (!invoice) return <AccountNotFound />;
|
||||||
|
|
||||||
const status = String(invoice.status);
|
const status = String(invoice.status);
|
||||||
const items = (invoice.items ?? []) as Array<Record<string, unknown>>;
|
const items = (invoice.items ?? []) as Array<Record<string, unknown>>;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default async function SepaBatchDetailPage({ params }: PageProps) {
|
|||||||
api.getBatchItems(batchId),
|
api.getBatchItems(batchId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!batch) return <div>Einzug nicht gefunden</div>;
|
if (!batch) return <AccountNotFound />;
|
||||||
|
|
||||||
const status = String(batch.status);
|
const status = String(batch.status);
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
|
|||||||
api.getRecipients(campaignId),
|
api.getRecipients(campaignId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!newsletter) return <div>Newsletter nicht gefunden</div>;
|
if (!newsletter) return <AccountNotFound />;
|
||||||
|
|
||||||
const status = String(newsletter.status);
|
const status = String(newsletter.status);
|
||||||
const sentCount = recipients.filter(
|
const sentCount = recipients.filter(
|
||||||
@@ -60,6 +60,7 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
|
|||||||
<Link
|
<Link
|
||||||
href={`/home/${account}/newsletter`}
|
href={`/home/${account}/newsletter`}
|
||||||
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
|
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
|
||||||
|
data-test="newsletter-back-link"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||||
Zurück zu Newsletter
|
Zurück zu Newsletter
|
||||||
@@ -98,7 +99,7 @@ export default async function NewsletterDetailPage({ params }: PageProps) {
|
|||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
{status === 'draft' && (
|
{status === 'draft' && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Button>
|
<Button data-test="newsletter-send-btn">
|
||||||
<Send className="mr-2 h-4 w-4" />
|
<Send className="mr-2 h-4 w-4" />
|
||||||
Newsletter versenden
|
Newsletter versenden
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default async function NewsletterPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={`/home/${account}/newsletter/new`}>
|
<Link href={`/home/${account}/newsletter/new`}>
|
||||||
<Button>
|
<Button data-test="newsletter-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neuer Newsletter
|
Neuer Newsletter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default async function NewsletterTemplatesPage({ params }: PageProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button>
|
<Button data-test="newsletter-templates-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neue Vorlage
|
Neue Vorlage
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default async function EditPageRoute({ params }: Props) {
|
|||||||
|
|
||||||
const api = createSiteBuilderApi(client);
|
const api = createSiteBuilderApi(client);
|
||||||
const page = await api.getPage(pageId);
|
const page = await api.getPage(pageId);
|
||||||
if (!page) return <div>Seite nicht gefunden</div>;
|
if (!page) return <AccountNotFound />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteEditor
|
<SiteEditor
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default async function SiteBuilderDashboard({ params }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/home/${account}/site-builder/new`}>
|
<Link href={`/home/${account}/site-builder/new`}>
|
||||||
<Button>
|
<Button data-test="site-new-page-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neue Seite
|
Neue Seite
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default async function PostsManagerPage({ params }: Props) {
|
|||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button>
|
<Button data-test="site-new-post-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neuer Beitrag
|
Neuer Beitrag
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
71
apps/web/i18n/messages/de/bookings.json
Normal file
71
apps/web/i18n/messages/de/bookings.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"bookings": "Buchungen",
|
||||||
|
"calendar": "Kalender",
|
||||||
|
"rooms": "Zimmer",
|
||||||
|
"guests": "Gäste",
|
||||||
|
"newBooking": "Neue Buchung"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"title": "Buchungen",
|
||||||
|
"searchPlaceholder": "Gast oder Zimmer suchen...",
|
||||||
|
"newBooking": "Neue Buchung",
|
||||||
|
"noBookings": "Keine Buchungen vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihre erste Buchung.",
|
||||||
|
"activeBookings": "Aktive Buchungen",
|
||||||
|
"guest": "Gast",
|
||||||
|
"room": "Zimmer",
|
||||||
|
"checkIn": "Check-in",
|
||||||
|
"checkOut": "Check-out",
|
||||||
|
"nights": "Nächte",
|
||||||
|
"price": "Preis"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"notFound": "Buchung nicht gefunden",
|
||||||
|
"guestInfo": "Gastinformationen",
|
||||||
|
"roomInfo": "Zimmerinformationen",
|
||||||
|
"bookingDetails": "Buchungsdetails",
|
||||||
|
"extras": "Extras"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"room": "Zimmer *",
|
||||||
|
"selectRoom": "— Zimmer wählen —",
|
||||||
|
"guest": "Gast",
|
||||||
|
"checkIn": "Check-in *",
|
||||||
|
"checkOut": "Check-out *",
|
||||||
|
"adults": "Erwachsene",
|
||||||
|
"children": "Kinder",
|
||||||
|
"notes": "Notizen",
|
||||||
|
"created": "Buchung erfolgreich erstellt",
|
||||||
|
"errorCreating": "Fehler beim Erstellen"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"confirmed": "Bestätigt",
|
||||||
|
"checked_in": "Eingecheckt",
|
||||||
|
"checked_out": "Ausgecheckt",
|
||||||
|
"cancelled": "Storniert",
|
||||||
|
"no_show": "Nicht erschienen"
|
||||||
|
},
|
||||||
|
"rooms": {
|
||||||
|
"title": "Zimmer",
|
||||||
|
"newRoom": "Neues Zimmer",
|
||||||
|
"noRooms": "Keine Zimmer vorhanden",
|
||||||
|
"name": "Name",
|
||||||
|
"type": "Typ",
|
||||||
|
"capacity": "Kapazität",
|
||||||
|
"price": "Preis/Nacht"
|
||||||
|
},
|
||||||
|
"guests": {
|
||||||
|
"title": "Gäste",
|
||||||
|
"newGuest": "Neuer Gast",
|
||||||
|
"noGuests": "Keine Gäste vorhanden",
|
||||||
|
"name": "Name",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"phone": "Telefon",
|
||||||
|
"bookings": "Buchungen"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"title": "Belegungskalender"
|
||||||
|
}
|
||||||
|
}
|
||||||
120
apps/web/i18n/messages/de/courses.json
Normal file
120
apps/web/i18n/messages/de/courses.json
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"courses": "Kurse",
|
||||||
|
"newCourse": "Neuer Kurs",
|
||||||
|
"calendar": "Kalender",
|
||||||
|
"categories": "Kategorien",
|
||||||
|
"instructors": "Kursleiter",
|
||||||
|
"locations": "Standorte",
|
||||||
|
"statistics": "Statistiken"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"coursesTitle": "Kurse",
|
||||||
|
"newCourseTitle": "Neuer Kurs",
|
||||||
|
"calendarTitle": "Kurskalender",
|
||||||
|
"categoriesTitle": "Kurskategorien",
|
||||||
|
"instructorsTitle": "Kursleiter",
|
||||||
|
"locationsTitle": "Standorte",
|
||||||
|
"statisticsTitle": "Kurs-Statistiken"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"searchPlaceholder": "Kurs suchen...",
|
||||||
|
"title": "Kurse ({count})",
|
||||||
|
"noCourses": "Keine Kurse vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihren ersten Kurs, um loszulegen.",
|
||||||
|
"courseNumber": "Kursnr.",
|
||||||
|
"courseName": "Kursname",
|
||||||
|
"startDate": "Beginn",
|
||||||
|
"endDate": "Ende",
|
||||||
|
"participants": "Teilnehmer",
|
||||||
|
"fee": "Gebühr"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"notFound": "Kurs nicht gefunden",
|
||||||
|
"participants": "Teilnehmer",
|
||||||
|
"sessions": "Termine",
|
||||||
|
"viewAllParticipants": "Alle Teilnehmer anzeigen",
|
||||||
|
"viewAttendance": "Anwesenheit anzeigen",
|
||||||
|
"noParticipants": "Noch keine Teilnehmer.",
|
||||||
|
"noSessions": "Noch keine Termine.",
|
||||||
|
"addParticipant": "Teilnehmer hinzufügen"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"basicData": "Grunddaten",
|
||||||
|
"courseNumber": "Kursnummer",
|
||||||
|
"courseName": "Kursname *",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"schedule": "Zeitplan",
|
||||||
|
"startDate": "Startdatum",
|
||||||
|
"endDate": "Enddatum",
|
||||||
|
"registrationDeadline": "Anmeldeschluss",
|
||||||
|
"capacity": "Kapazität",
|
||||||
|
"maxParticipants": "Max. Teilnehmer",
|
||||||
|
"minParticipants": "Min. Teilnehmer",
|
||||||
|
"fee": "Gebühr (€)",
|
||||||
|
"reducedFee": "Ermäßigte Gebühr (€)",
|
||||||
|
"statusSection": "Status",
|
||||||
|
"courseStatus": "Kursstatus",
|
||||||
|
"created": "Kurs erfolgreich erstellt",
|
||||||
|
"updated": "Kurs aktualisiert",
|
||||||
|
"errorCreating": "Fehler beim Erstellen des Kurses",
|
||||||
|
"errorUpdating": "Fehler beim Aktualisieren"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"planned": "Geplant",
|
||||||
|
"open": "Offen",
|
||||||
|
"running": "Laufend",
|
||||||
|
"completed": "Abgeschlossen",
|
||||||
|
"cancelled": "Abgesagt"
|
||||||
|
},
|
||||||
|
"enrollment": {
|
||||||
|
"enrolled": "Eingeschrieben",
|
||||||
|
"waitlisted": "Warteliste",
|
||||||
|
"cancelled": "Storniert",
|
||||||
|
"completed": "Abgeschlossen",
|
||||||
|
"enrolledAt": "Eingeschrieben am"
|
||||||
|
},
|
||||||
|
"attendance": {
|
||||||
|
"title": "Anwesenheit",
|
||||||
|
"present": "Anwesend",
|
||||||
|
"absent": "Abwesend",
|
||||||
|
"excused": "Entschuldigt",
|
||||||
|
"session": "Termin"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"title": "Kurskalender",
|
||||||
|
"courseDay": "Kurstag",
|
||||||
|
"free": "Frei",
|
||||||
|
"today": "Heute",
|
||||||
|
"weekdays": ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"],
|
||||||
|
"months": [
|
||||||
|
"Januar",
|
||||||
|
"Februar",
|
||||||
|
"März",
|
||||||
|
"April",
|
||||||
|
"Mai",
|
||||||
|
"Juni",
|
||||||
|
"Juli",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"Oktober",
|
||||||
|
"November",
|
||||||
|
"Dezember"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"title": "Kategorien",
|
||||||
|
"newCategory": "Neue Kategorie",
|
||||||
|
"noCategories": "Keine Kategorien vorhanden."
|
||||||
|
},
|
||||||
|
"instructors": {
|
||||||
|
"title": "Kursleiter",
|
||||||
|
"newInstructor": "Neuer Kursleiter",
|
||||||
|
"noInstructors": "Keine Kursleiter vorhanden."
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"title": "Standorte",
|
||||||
|
"newLocation": "Neuer Standort",
|
||||||
|
"noLocations": "Keine Standorte vorhanden."
|
||||||
|
}
|
||||||
|
}
|
||||||
81
apps/web/i18n/messages/de/documents.json
Normal file
81
apps/web/i18n/messages/de/documents.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"documents": "Dokumente",
|
||||||
|
"generate": "Generieren",
|
||||||
|
"templates": "Vorlagen"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "Dokumente",
|
||||||
|
"subtitle": "Dokumente generieren und Vorlagen verwalten",
|
||||||
|
"generate": "Erstellen",
|
||||||
|
"manageTemplates": "Vorlagen verwalten"
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"member_card": "Mitgliedsausweis",
|
||||||
|
"invoice": "Rechnung",
|
||||||
|
"label": "Etiketten",
|
||||||
|
"report": "Bericht",
|
||||||
|
"letter": "Brief",
|
||||||
|
"certificate": "Zertifikat"
|
||||||
|
},
|
||||||
|
"typeDescriptions": {
|
||||||
|
"member_card": "Mitgliedsausweise mit Foto und QR-Code generieren",
|
||||||
|
"invoice": "Professionelle Rechnungen für Mitglieder und Dienstleistungen",
|
||||||
|
"label": "Adressetiketten für Serienbriefe und Versand",
|
||||||
|
"report": "Statistische Auswertungen und Übersichten",
|
||||||
|
"letter": "Serienbriefe und individuelle Schreiben",
|
||||||
|
"certificate": "Teilnahmebescheinigungen und Urkunden"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"title": "Dokument generieren",
|
||||||
|
"documentType": "Dokumenttyp",
|
||||||
|
"titleLabel": "Titel / Bezeichnung",
|
||||||
|
"format": "Format",
|
||||||
|
"orientation": "Ausrichtung",
|
||||||
|
"portrait": "Hochformat",
|
||||||
|
"landscape": "Querformat",
|
||||||
|
"comingSoon": "Demnächst verfügbar",
|
||||||
|
"generating": "Wird generiert...",
|
||||||
|
"generateButton": "Generieren",
|
||||||
|
"success": "Dokument erfolgreich erstellt!",
|
||||||
|
"downloaded": "Datei heruntergeladen",
|
||||||
|
"downloadAgain": "Erneut herunterladen",
|
||||||
|
"error": "Fehler bei der Generierung",
|
||||||
|
"backToDocuments": "Zurück zu Dokumente",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"hint": "Hinweis",
|
||||||
|
"memberCardHint": "Es werden Mitgliedsausweise für alle aktiven Mitglieder generiert.",
|
||||||
|
"labelHint": "Es werden Adressetiketten für alle aktiven Mitglieder generiert.",
|
||||||
|
"reportHint": "Es wird ein Mitgliederbericht mit aktuellen Daten generiert."
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"title": "Dokumentvorlagen",
|
||||||
|
"subtitle": "Vorlagen für Mitgliedsausweise, Rechnungen, Etiketten und mehr",
|
||||||
|
"newTemplate": "Neue Vorlage",
|
||||||
|
"noTemplates": "Keine Vorlagen vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihre erste Dokumentvorlage.",
|
||||||
|
"allTemplates": "Alle Vorlagen ({count})",
|
||||||
|
"name": "Name",
|
||||||
|
"type": "Typ",
|
||||||
|
"description": "Beschreibung"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"title": "MITGLIEDSAUSWEIS",
|
||||||
|
"memberSince": "Mitglied seit",
|
||||||
|
"dateOfBirth": "Geb.-Datum",
|
||||||
|
"address": "Adresse",
|
||||||
|
"validUntil": "Gültig",
|
||||||
|
"issuedOn": "Ausgestellt"
|
||||||
|
},
|
||||||
|
"report": {
|
||||||
|
"title": "Mitgliederbericht",
|
||||||
|
"total": "Gesamt:"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"accountNotFound": "Konto nicht gefunden",
|
||||||
|
"unknownType": "Unbekannter Dokumenttyp",
|
||||||
|
"underDevelopment": "ist noch in Entwicklung",
|
||||||
|
"dbError": "DB-Fehler",
|
||||||
|
"noActiveMembers": "Keine aktiven Mitglieder"
|
||||||
|
}
|
||||||
|
}
|
||||||
67
apps/web/i18n/messages/de/events.json
Normal file
67
apps/web/i18n/messages/de/events.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"events": "Veranstaltungen",
|
||||||
|
"newEvent": "Neue Veranstaltung",
|
||||||
|
"registrations": "Anmeldungen",
|
||||||
|
"holidayPasses": "Ferienpässe"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"title": "Veranstaltungen",
|
||||||
|
"newEvent": "Neue Veranstaltung",
|
||||||
|
"noEvents": "Keine Veranstaltungen vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihre erste Veranstaltung.",
|
||||||
|
"upcoming": "Bevorstehend",
|
||||||
|
"totalRegistrations": "Anmeldungen gesamt"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"notFound": "Veranstaltung nicht gefunden",
|
||||||
|
"date": "Datum",
|
||||||
|
"time": "Uhrzeit",
|
||||||
|
"location": "Ort",
|
||||||
|
"registrations": "Anmeldungen",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"noRegistrations": "Noch keine Anmeldungen",
|
||||||
|
"parentName": "Elternteil"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"basicData": "Grunddaten",
|
||||||
|
"name": "Name der Veranstaltung *",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"dateTime": "Datum & Zeit",
|
||||||
|
"eventDate": "Datum *",
|
||||||
|
"startTime": "Startzeit",
|
||||||
|
"endDate": "Enddatum",
|
||||||
|
"location": "Ort",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"contactName": "Ansprechpartner",
|
||||||
|
"contactEmail": "E-Mail",
|
||||||
|
"contactPhone": "Telefon",
|
||||||
|
"participantsAndFees": "Teilnehmer & Kosten",
|
||||||
|
"capacity": "Max. Teilnehmer",
|
||||||
|
"minAge": "Mindestalter",
|
||||||
|
"maxAge": "Höchstalter",
|
||||||
|
"fee": "Gebühr (€)",
|
||||||
|
"registrationDeadline": "Anmeldeschluss",
|
||||||
|
"status": "Status",
|
||||||
|
"created": "Veranstaltung erfolgreich erstellt",
|
||||||
|
"errorCreating": "Fehler beim Erstellen der Veranstaltung"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"planned": "Geplant",
|
||||||
|
"open": "Offen",
|
||||||
|
"full": "Ausgebucht",
|
||||||
|
"running": "Laufend",
|
||||||
|
"completed": "Abgeschlossen",
|
||||||
|
"cancelled": "Abgesagt"
|
||||||
|
},
|
||||||
|
"registrationStatus": {
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"confirmed": "Bestätigt",
|
||||||
|
"waitlisted": "Warteliste",
|
||||||
|
"cancelled": "Storniert"
|
||||||
|
},
|
||||||
|
"holidayPasses": {
|
||||||
|
"title": "Ferienpässe",
|
||||||
|
"noPasses": "Keine Ferienpässe vorhanden"
|
||||||
|
}
|
||||||
|
}
|
||||||
113
apps/web/i18n/messages/de/finance.json
Normal file
113
apps/web/i18n/messages/de/finance.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"overview": "Finanzen",
|
||||||
|
"invoices": "Rechnungen",
|
||||||
|
"sepa": "SEPA-Einzüge",
|
||||||
|
"payments": "Zahlungen",
|
||||||
|
"newInvoice": "Neue Rechnung",
|
||||||
|
"newBatch": "Neuer Einzug"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Finanzen – Übersicht",
|
||||||
|
"subtitle": "Rechnungen, SEPA-Einzüge und Zahlungen verwalten",
|
||||||
|
"openInvoices": "Offene Rechnungen",
|
||||||
|
"overdueInvoices": "Überfällige Rechnungen",
|
||||||
|
"totalRevenue": "Gesamteinnahmen",
|
||||||
|
"sepaBatches": "SEPA-Einzüge"
|
||||||
|
},
|
||||||
|
"invoices": {
|
||||||
|
"title": "Rechnungen",
|
||||||
|
"newInvoice": "Neue Rechnung",
|
||||||
|
"noInvoices": "Keine Rechnungen vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihre erste Rechnung.",
|
||||||
|
"invoiceNumber": "Rechnungsnr.",
|
||||||
|
"recipient": "Empfänger",
|
||||||
|
"issueDate": "Rechnungsdatum",
|
||||||
|
"dueDate": "Fälligkeitsdatum",
|
||||||
|
"amount": "Betrag",
|
||||||
|
"notFound": "Rechnung nicht gefunden"
|
||||||
|
},
|
||||||
|
"invoiceForm": {
|
||||||
|
"title": "Rechnungsdaten",
|
||||||
|
"invoiceNumber": "Rechnungsnummer *",
|
||||||
|
"issueDate": "Rechnungsdatum",
|
||||||
|
"dueDate": "Fälligkeitsdatum *",
|
||||||
|
"recipient": "Empfänger",
|
||||||
|
"recipientName": "Name *",
|
||||||
|
"recipientAddress": "Adresse",
|
||||||
|
"lineItems": "Positionen",
|
||||||
|
"addLineItem": "+ Position hinzufügen",
|
||||||
|
"itemDescription": "Beschreibung *",
|
||||||
|
"quantity": "Menge",
|
||||||
|
"unitPrice": "Einzelpreis (€)",
|
||||||
|
"removeItem": "Position entfernen",
|
||||||
|
"amounts": "Beträge",
|
||||||
|
"taxRate": "MwSt.-Satz (%)",
|
||||||
|
"subtotal": "Zwischensumme (netto)",
|
||||||
|
"tax": "MwSt. ({rate}%)",
|
||||||
|
"total": "Gesamtbetrag",
|
||||||
|
"notes": "Anmerkungen",
|
||||||
|
"created": "Rechnung erfolgreich erstellt",
|
||||||
|
"errorCreating": "Fehler beim Erstellen der Rechnung"
|
||||||
|
},
|
||||||
|
"invoiceStatus": {
|
||||||
|
"draft": "Entwurf",
|
||||||
|
"sent": "Versendet",
|
||||||
|
"paid": "Bezahlt",
|
||||||
|
"overdue": "Überfällig",
|
||||||
|
"cancelled": "Storniert",
|
||||||
|
"credited": "Gutgeschrieben"
|
||||||
|
},
|
||||||
|
"sepa": {
|
||||||
|
"title": "SEPA-Einzüge",
|
||||||
|
"newBatch": "Neuer Einzug",
|
||||||
|
"noBatches": "Keine SEPA-Einzüge vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihren ersten SEPA-Einzug.",
|
||||||
|
"directDebit": "Lastschrift",
|
||||||
|
"creditTransfer": "Überweisung",
|
||||||
|
"executionDate": "Ausführungsdatum",
|
||||||
|
"totalAmount": "Gesamtbetrag",
|
||||||
|
"itemCount": "Positionen",
|
||||||
|
"downloadXml": "XML herunterladen",
|
||||||
|
"notFound": "Einzug nicht gefunden"
|
||||||
|
},
|
||||||
|
"sepaBatchForm": {
|
||||||
|
"title": "SEPA-Einzug erstellen",
|
||||||
|
"batchType": "Typ",
|
||||||
|
"directDebit": "Lastschrift (SEPA Core)",
|
||||||
|
"creditTransfer": "Überweisung",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"descriptionPlaceholder": "z.B. Mitgliedsbeiträge Q1 2026",
|
||||||
|
"executionDate": "Ausführungsdatum *",
|
||||||
|
"executionDateRequired": "Ausführungsdatum ist erforderlich",
|
||||||
|
"created": "SEPA-Einzug erstellt",
|
||||||
|
"errorCreating": "Fehler beim Erstellen"
|
||||||
|
},
|
||||||
|
"sepaBatchStatus": {
|
||||||
|
"draft": "Entwurf",
|
||||||
|
"ready": "Bereit",
|
||||||
|
"submitted": "Eingereicht",
|
||||||
|
"executed": "Abgeschlossen",
|
||||||
|
"failed": "Fehlgeschlagen",
|
||||||
|
"cancelled": "Abgebrochen"
|
||||||
|
},
|
||||||
|
"sepaItemStatus": {
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"success": "Verarbeitet",
|
||||||
|
"failed": "Fehlgeschlagen",
|
||||||
|
"rejected": "Abgelehnt"
|
||||||
|
},
|
||||||
|
"payments": {
|
||||||
|
"title": "Zahlungsübersicht",
|
||||||
|
"paidInvoices": "Bezahlte Rechnungen",
|
||||||
|
"openInvoices": "Offene Rechnungen",
|
||||||
|
"overdueInvoices": "Überfällige Rechnungen",
|
||||||
|
"sepaBatches": "SEPA-Einzüge"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"creating": "Wird erstellt...",
|
||||||
|
"membershipFee": "Mitgliedsbeitrag",
|
||||||
|
"sepaDirectDebit": "SEPA Einzug"
|
||||||
|
}
|
||||||
|
}
|
||||||
80
apps/web/i18n/messages/de/meetings.json
Normal file
80
apps/web/i18n/messages/de/meetings.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"protocols": "Protokolle",
|
||||||
|
"tasks": "Aufgaben",
|
||||||
|
"newProtocol": "Neues Protokoll"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"overviewTitle": "Sitzungsprotokolle",
|
||||||
|
"protocolsTitle": "Sitzungsprotokolle - Protokolle",
|
||||||
|
"tasksTitle": "Sitzungsprotokolle - Aufgaben"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Sitzungsprotokolle – Übersicht",
|
||||||
|
"subtitle": "Protokolle, Tagesordnungspunkte und Aufgaben verwalten",
|
||||||
|
"totalProtocols": "Protokolle gesamt",
|
||||||
|
"publishedProtocols": "Veröffentlicht",
|
||||||
|
"draftProtocols": "Entwürfe",
|
||||||
|
"overdueTasks": "Überfällige Aufgaben",
|
||||||
|
"recentProtocols": "Neueste Protokolle",
|
||||||
|
"noRecentProtocols": "Noch keine Protokolle vorhanden."
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"title": "Titel *",
|
||||||
|
"meetingDate": "Sitzungsdatum *",
|
||||||
|
"meetingType": "Sitzungsart",
|
||||||
|
"location": "Ort",
|
||||||
|
"created": "Protokoll erfolgreich erstellt",
|
||||||
|
"updated": "Protokoll aktualisiert",
|
||||||
|
"published": "Protokoll veröffentlicht",
|
||||||
|
"errorSaving": "Fehler beim Speichern des Protokolls",
|
||||||
|
"notFound": "Protokoll nicht gefunden",
|
||||||
|
"noProtocols": "Keine Protokolle vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihr erstes Sitzungsprotokoll, um loszulegen."
|
||||||
|
},
|
||||||
|
"protocolList": {
|
||||||
|
"searchPlaceholder": "Protokoll suchen...",
|
||||||
|
"title": "Protokolle ({count})",
|
||||||
|
"date": "Datum",
|
||||||
|
"titleColumn": "Titel",
|
||||||
|
"type": "Sitzungsart",
|
||||||
|
"locationColumn": "Ort",
|
||||||
|
"statusColumn": "Status",
|
||||||
|
"published": "Veröffentlicht",
|
||||||
|
"draft": "Entwurf"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"agendaItems": "Tagesordnungspunkte",
|
||||||
|
"noItems": "Keine Tagesordnungspunkte vorhanden.",
|
||||||
|
"addItem": "Punkt hinzufügen",
|
||||||
|
"statusUpdated": "Status aktualisiert",
|
||||||
|
"deleted": "Tagesordnungspunkt gelöscht",
|
||||||
|
"deleteConfirm": "Tagesordnungspunkt wirklich löschen?",
|
||||||
|
"errorUpdating": "Fehler beim Aktualisieren",
|
||||||
|
"errorDeleting": "Fehler beim Löschen",
|
||||||
|
"dueDate": "Fällig: {date}",
|
||||||
|
"responsible": "Verantwortlich: {name}",
|
||||||
|
"status": {
|
||||||
|
"open": "Offen",
|
||||||
|
"in_progress": "In Bearbeitung",
|
||||||
|
"done": "Erledigt",
|
||||||
|
"cancelled": "Abgebrochen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"title": "Offene Aufgaben ({count})",
|
||||||
|
"noTasks": "Keine offenen Aufgaben",
|
||||||
|
"allTasksDone": "Alle Aufgaben sind erledigt.",
|
||||||
|
"overdue": "Überfällig",
|
||||||
|
"fromProtocol": "aus Protokoll"
|
||||||
|
},
|
||||||
|
"meetingTypes": {
|
||||||
|
"regular": "Ordentliche Sitzung",
|
||||||
|
"extraordinary": "Außerordentliche Sitzung",
|
||||||
|
"board": "Vorstandssitzung",
|
||||||
|
"general_assembly": "Mitgliederversammlung",
|
||||||
|
"committee": "Ausschusssitzung",
|
||||||
|
"other": "Sonstige"
|
||||||
|
}
|
||||||
|
}
|
||||||
76
apps/web/i18n/messages/de/newsletter.json
Normal file
76
apps/web/i18n/messages/de/newsletter.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"newsletters": "Newsletter",
|
||||||
|
"templates": "Vorlagen",
|
||||||
|
"newNewsletter": "Neuer Newsletter"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"title": "Newsletter",
|
||||||
|
"subtitle": "Newsletter erstellen und versenden",
|
||||||
|
"newNewsletter": "Neuer Newsletter",
|
||||||
|
"noNewsletters": "Keine Newsletter vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihren ersten Newsletter, um loszulegen.",
|
||||||
|
"subject": "Betreff",
|
||||||
|
"noSubject": "(Kein Betreff)",
|
||||||
|
"recipients": "Empfänger",
|
||||||
|
"created": "Erstellt",
|
||||||
|
"sent": "Gesendet",
|
||||||
|
"totalSent": "Gesendet",
|
||||||
|
"totalRecipients": "Empfänger gesamt"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"title": "Newsletter Details",
|
||||||
|
"backToList": "Zurück zu Newsletter",
|
||||||
|
"sendNewsletter": "Newsletter versenden",
|
||||||
|
"recipientsSection": "Empfänger",
|
||||||
|
"sentCount": "Gesendet",
|
||||||
|
"failedCount": "Fehlgeschlagen",
|
||||||
|
"noRecipients": "Keine Empfänger hinzugefügt. Fügen Sie Empfänger aus Ihrer Mitgliederliste hinzu.",
|
||||||
|
"recipientName": "Name",
|
||||||
|
"recipientEmail": "E-Mail",
|
||||||
|
"recipientStatus": "Status",
|
||||||
|
"notFound": "Newsletter nicht gefunden"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"content": "Newsletter-Inhalt",
|
||||||
|
"subject": "Betreff *",
|
||||||
|
"bodyHtml": "Inhalt (HTML) *",
|
||||||
|
"bodyHtmlPlaceholder": "<h1>Hallo!</h1><p>Ihr Newsletter-Inhalt...</p>",
|
||||||
|
"bodyText": "Nur-Text-Version",
|
||||||
|
"bodyTextHelp": "Nur-Text-Fallback für E-Mail-Clients ohne HTML-Unterstützung",
|
||||||
|
"schedule": "Zeitplan",
|
||||||
|
"scheduledDate": "Geplanter Versand (optional)",
|
||||||
|
"scheduleHelp": "Leer lassen, um den Newsletter als Entwurf zu speichern.",
|
||||||
|
"created": "Newsletter erfolgreich erstellt",
|
||||||
|
"errorCreating": "Fehler beim Erstellen des Newsletters"
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"title": "Newsletter-Vorlagen",
|
||||||
|
"subtitle": "Wiederverwendbare Vorlagen für Newsletter",
|
||||||
|
"newTemplate": "Neue Vorlage",
|
||||||
|
"noTemplates": "Keine Vorlagen vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihre erste Newsletter-Vorlage, um sie in Kampagnen wiederzuverwenden.",
|
||||||
|
"allTemplates": "Alle Vorlagen ({count})",
|
||||||
|
"name": "Name",
|
||||||
|
"subject": "Betreff",
|
||||||
|
"variables": "Variablen"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"draft": "Entwurf",
|
||||||
|
"scheduled": "Geplant",
|
||||||
|
"sending": "Wird versendet",
|
||||||
|
"sent": "Gesendet",
|
||||||
|
"failed": "Fehlgeschlagen"
|
||||||
|
},
|
||||||
|
"recipientStatus": {
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"sent": "Gesendet",
|
||||||
|
"failed": "Fehlgeschlagen",
|
||||||
|
"bounced": "Unzustellbar"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"creating": "Wird erstellt...",
|
||||||
|
"create": "Newsletter erstellen"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
apps/web/i18n/messages/de/siteBuilder.json
Normal file
53
apps/web/i18n/messages/de/siteBuilder.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"pages": "Seiten",
|
||||||
|
"posts": "Beiträge",
|
||||||
|
"settings": "Einstellungen"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"title": "Seiten",
|
||||||
|
"newPage": "Neue Seite",
|
||||||
|
"noPages": "Keine Seiten vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihre erste Seite.",
|
||||||
|
"pageTitle": "Seitentitel *",
|
||||||
|
"slug": "URL-Pfad",
|
||||||
|
"slugPlaceholder": "Leer lassen für automatische Generierung...",
|
||||||
|
"membersOnly": "Nur für Mitglieder",
|
||||||
|
"createAndEdit": "Seite erstellen & Editor öffnen",
|
||||||
|
"pageCreated": "Seite erstellt — Editor wird geöffnet",
|
||||||
|
"errorCreating": "Fehler beim Erstellen",
|
||||||
|
"notFound": "Seite nicht gefunden",
|
||||||
|
"published": "Seite veröffentlicht",
|
||||||
|
"error": "Fehler"
|
||||||
|
},
|
||||||
|
"posts": {
|
||||||
|
"title": "Beiträge",
|
||||||
|
"newPost": "Neuer Beitrag",
|
||||||
|
"noPosts": "Keine Beiträge vorhanden",
|
||||||
|
"createFirst": "Erstellen Sie Ihren ersten Beitrag.",
|
||||||
|
"postTitle": "Titel *",
|
||||||
|
"content": "Beitragsinhalt (HTML erlaubt)...",
|
||||||
|
"excerpt": "Kurzfassung",
|
||||||
|
"postCreated": "Beitrag erstellt",
|
||||||
|
"errorCreating": "Fehler"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Einstellungen",
|
||||||
|
"saved": "Einstellungen gespeichert",
|
||||||
|
"error": "Fehler"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"publish": "Veröffentlichen",
|
||||||
|
"unpublish": "Zurückziehen"
|
||||||
|
},
|
||||||
|
"portal": {
|
||||||
|
"loginSuccess": "Erfolgreich angemeldet",
|
||||||
|
"membersArea": "Mitgliederbereich"
|
||||||
|
},
|
||||||
|
"blocks": {
|
||||||
|
"news": "Neuigkeiten",
|
||||||
|
"events": "Veranstaltungen",
|
||||||
|
"loginError": "Fehler bei der Anmeldung",
|
||||||
|
"connectionError": "Verbindungsfehler"
|
||||||
|
}
|
||||||
|
}
|
||||||
71
apps/web/i18n/messages/en/bookings.json
Normal file
71
apps/web/i18n/messages/en/bookings.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"bookings": "Bookings",
|
||||||
|
"calendar": "Calendar",
|
||||||
|
"rooms": "Rooms",
|
||||||
|
"guests": "Guests",
|
||||||
|
"newBooking": "New Booking"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"title": "Bookings",
|
||||||
|
"searchPlaceholder": "Search guest or room...",
|
||||||
|
"newBooking": "New Booking",
|
||||||
|
"noBookings": "No bookings found",
|
||||||
|
"createFirst": "Create your first booking.",
|
||||||
|
"activeBookings": "Active Bookings",
|
||||||
|
"guest": "Guest",
|
||||||
|
"room": "Room",
|
||||||
|
"checkIn": "Check-in",
|
||||||
|
"checkOut": "Check-out",
|
||||||
|
"nights": "Nights",
|
||||||
|
"price": "Price"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"notFound": "Booking not found",
|
||||||
|
"guestInfo": "Guest Information",
|
||||||
|
"roomInfo": "Room Information",
|
||||||
|
"bookingDetails": "Booking Details",
|
||||||
|
"extras": "Extras"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"room": "Room *",
|
||||||
|
"selectRoom": "— Select room —",
|
||||||
|
"guest": "Guest",
|
||||||
|
"checkIn": "Check-in *",
|
||||||
|
"checkOut": "Check-out *",
|
||||||
|
"adults": "Adults",
|
||||||
|
"children": "Children",
|
||||||
|
"notes": "Notes",
|
||||||
|
"created": "Booking created successfully",
|
||||||
|
"errorCreating": "Error creating booking"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"confirmed": "Confirmed",
|
||||||
|
"checked_in": "Checked In",
|
||||||
|
"checked_out": "Checked Out",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
|
"no_show": "No Show"
|
||||||
|
},
|
||||||
|
"rooms": {
|
||||||
|
"title": "Rooms",
|
||||||
|
"newRoom": "New Room",
|
||||||
|
"noRooms": "No rooms found",
|
||||||
|
"name": "Name",
|
||||||
|
"type": "Type",
|
||||||
|
"capacity": "Capacity",
|
||||||
|
"price": "Price/Night"
|
||||||
|
},
|
||||||
|
"guests": {
|
||||||
|
"title": "Guests",
|
||||||
|
"newGuest": "New Guest",
|
||||||
|
"noGuests": "No guests found",
|
||||||
|
"name": "Name",
|
||||||
|
"email": "Email",
|
||||||
|
"phone": "Phone",
|
||||||
|
"bookings": "Bookings"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"title": "Availability Calendar"
|
||||||
|
}
|
||||||
|
}
|
||||||
120
apps/web/i18n/messages/en/courses.json
Normal file
120
apps/web/i18n/messages/en/courses.json
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"courses": "Courses",
|
||||||
|
"newCourse": "New Course",
|
||||||
|
"calendar": "Calendar",
|
||||||
|
"categories": "Categories",
|
||||||
|
"instructors": "Instructors",
|
||||||
|
"locations": "Locations",
|
||||||
|
"statistics": "Statistics"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"coursesTitle": "Courses",
|
||||||
|
"newCourseTitle": "New Course",
|
||||||
|
"calendarTitle": "Course Calendar",
|
||||||
|
"categoriesTitle": "Course Categories",
|
||||||
|
"instructorsTitle": "Instructors",
|
||||||
|
"locationsTitle": "Locations",
|
||||||
|
"statisticsTitle": "Course Statistics"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"searchPlaceholder": "Search courses...",
|
||||||
|
"title": "Courses ({count})",
|
||||||
|
"noCourses": "No courses found",
|
||||||
|
"createFirst": "Create your first course to get started.",
|
||||||
|
"courseNumber": "Course No.",
|
||||||
|
"courseName": "Course Name",
|
||||||
|
"startDate": "Start",
|
||||||
|
"endDate": "End",
|
||||||
|
"participants": "Participants",
|
||||||
|
"fee": "Fee"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"notFound": "Course not found",
|
||||||
|
"participants": "Participants",
|
||||||
|
"sessions": "Sessions",
|
||||||
|
"viewAllParticipants": "View all participants",
|
||||||
|
"viewAttendance": "View attendance",
|
||||||
|
"noParticipants": "No participants yet.",
|
||||||
|
"noSessions": "No sessions yet.",
|
||||||
|
"addParticipant": "Add Participant"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"basicData": "Basic Data",
|
||||||
|
"courseNumber": "Course Number",
|
||||||
|
"courseName": "Course Name *",
|
||||||
|
"description": "Description",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"startDate": "Start Date",
|
||||||
|
"endDate": "End Date",
|
||||||
|
"registrationDeadline": "Registration Deadline",
|
||||||
|
"capacity": "Capacity",
|
||||||
|
"maxParticipants": "Max Participants",
|
||||||
|
"minParticipants": "Min Participants",
|
||||||
|
"fee": "Fee (€)",
|
||||||
|
"reducedFee": "Reduced Fee (€)",
|
||||||
|
"statusSection": "Status",
|
||||||
|
"courseStatus": "Course Status",
|
||||||
|
"created": "Course created successfully",
|
||||||
|
"updated": "Course updated",
|
||||||
|
"errorCreating": "Error creating course",
|
||||||
|
"errorUpdating": "Error updating course"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"planned": "Planned",
|
||||||
|
"open": "Open",
|
||||||
|
"running": "Running",
|
||||||
|
"completed": "Completed",
|
||||||
|
"cancelled": "Cancelled"
|
||||||
|
},
|
||||||
|
"enrollment": {
|
||||||
|
"enrolled": "Enrolled",
|
||||||
|
"waitlisted": "Waitlisted",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
|
"completed": "Completed",
|
||||||
|
"enrolledAt": "Enrolled on"
|
||||||
|
},
|
||||||
|
"attendance": {
|
||||||
|
"title": "Attendance",
|
||||||
|
"present": "Present",
|
||||||
|
"absent": "Absent",
|
||||||
|
"excused": "Excused",
|
||||||
|
"session": "Session"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"title": "Course Calendar",
|
||||||
|
"courseDay": "Course Day",
|
||||||
|
"free": "Free",
|
||||||
|
"today": "Today",
|
||||||
|
"weekdays": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||||
|
"months": [
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"title": "Categories",
|
||||||
|
"newCategory": "New Category",
|
||||||
|
"noCategories": "No categories found."
|
||||||
|
},
|
||||||
|
"instructors": {
|
||||||
|
"title": "Instructors",
|
||||||
|
"newInstructor": "New Instructor",
|
||||||
|
"noInstructors": "No instructors found."
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"title": "Locations",
|
||||||
|
"newLocation": "New Location",
|
||||||
|
"noLocations": "No locations found."
|
||||||
|
}
|
||||||
|
}
|
||||||
81
apps/web/i18n/messages/en/documents.json
Normal file
81
apps/web/i18n/messages/en/documents.json
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"documents": "Documents",
|
||||||
|
"generate": "Generate",
|
||||||
|
"templates": "Templates"
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"title": "Documents",
|
||||||
|
"subtitle": "Generate documents and manage templates",
|
||||||
|
"generate": "Generate",
|
||||||
|
"manageTemplates": "Manage Templates"
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"member_card": "Member Card",
|
||||||
|
"invoice": "Invoice",
|
||||||
|
"label": "Labels",
|
||||||
|
"report": "Report",
|
||||||
|
"letter": "Letter",
|
||||||
|
"certificate": "Certificate"
|
||||||
|
},
|
||||||
|
"typeDescriptions": {
|
||||||
|
"member_card": "Generate member cards with photo and QR code",
|
||||||
|
"invoice": "Professional invoices for members and services",
|
||||||
|
"label": "Address labels for mailings and shipping",
|
||||||
|
"report": "Statistical reports and summaries",
|
||||||
|
"letter": "Form letters and individual correspondence",
|
||||||
|
"certificate": "Participation certificates and awards"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"title": "Generate Document",
|
||||||
|
"documentType": "Document Type",
|
||||||
|
"titleLabel": "Title / Label",
|
||||||
|
"format": "Format",
|
||||||
|
"orientation": "Orientation",
|
||||||
|
"portrait": "Portrait",
|
||||||
|
"landscape": "Landscape",
|
||||||
|
"comingSoon": "Coming Soon",
|
||||||
|
"generating": "Generating...",
|
||||||
|
"generateButton": "Generate",
|
||||||
|
"success": "Document created successfully!",
|
||||||
|
"downloaded": "File downloaded",
|
||||||
|
"downloadAgain": "Download Again",
|
||||||
|
"error": "Error generating document",
|
||||||
|
"backToDocuments": "Back to Documents",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"hint": "Note",
|
||||||
|
"memberCardHint": "Member cards will be generated for all active members.",
|
||||||
|
"labelHint": "Address labels will be generated for all active members.",
|
||||||
|
"reportHint": "A member report with current data will be generated."
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"title": "Document Templates",
|
||||||
|
"subtitle": "Templates for member cards, invoices, labels, and more",
|
||||||
|
"newTemplate": "New Template",
|
||||||
|
"noTemplates": "No templates found",
|
||||||
|
"createFirst": "Create your first document template.",
|
||||||
|
"allTemplates": "All Templates ({count})",
|
||||||
|
"name": "Name",
|
||||||
|
"type": "Type",
|
||||||
|
"description": "Description"
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"title": "MEMBER CARD",
|
||||||
|
"memberSince": "Member since",
|
||||||
|
"dateOfBirth": "Date of Birth",
|
||||||
|
"address": "Address",
|
||||||
|
"validUntil": "Valid until",
|
||||||
|
"issuedOn": "Issued on"
|
||||||
|
},
|
||||||
|
"report": {
|
||||||
|
"title": "Member Report",
|
||||||
|
"total": "Total:"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"accountNotFound": "Account not found",
|
||||||
|
"unknownType": "Unknown document type",
|
||||||
|
"underDevelopment": "is still under development",
|
||||||
|
"dbError": "Database error",
|
||||||
|
"noActiveMembers": "No active members"
|
||||||
|
}
|
||||||
|
}
|
||||||
67
apps/web/i18n/messages/en/events.json
Normal file
67
apps/web/i18n/messages/en/events.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"events": "Events",
|
||||||
|
"newEvent": "New Event",
|
||||||
|
"registrations": "Registrations",
|
||||||
|
"holidayPasses": "Holiday Passes"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"title": "Events",
|
||||||
|
"newEvent": "New Event",
|
||||||
|
"noEvents": "No events found",
|
||||||
|
"createFirst": "Create your first event.",
|
||||||
|
"upcoming": "Upcoming",
|
||||||
|
"totalRegistrations": "Total Registrations"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"notFound": "Event not found",
|
||||||
|
"date": "Date",
|
||||||
|
"time": "Time",
|
||||||
|
"location": "Location",
|
||||||
|
"registrations": "Registrations",
|
||||||
|
"description": "Description",
|
||||||
|
"noRegistrations": "No registrations yet",
|
||||||
|
"parentName": "Parent"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"basicData": "Basic Data",
|
||||||
|
"name": "Event Name *",
|
||||||
|
"description": "Description",
|
||||||
|
"dateTime": "Date & Time",
|
||||||
|
"eventDate": "Date *",
|
||||||
|
"startTime": "Start Time",
|
||||||
|
"endDate": "End Date",
|
||||||
|
"location": "Location",
|
||||||
|
"contact": "Contact",
|
||||||
|
"contactName": "Contact Person",
|
||||||
|
"contactEmail": "Email",
|
||||||
|
"contactPhone": "Phone",
|
||||||
|
"participantsAndFees": "Participants & Fees",
|
||||||
|
"capacity": "Max Participants",
|
||||||
|
"minAge": "Minimum Age",
|
||||||
|
"maxAge": "Maximum Age",
|
||||||
|
"fee": "Fee (€)",
|
||||||
|
"registrationDeadline": "Registration Deadline",
|
||||||
|
"status": "Status",
|
||||||
|
"created": "Event created successfully",
|
||||||
|
"errorCreating": "Error creating event"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"planned": "Planned",
|
||||||
|
"open": "Open",
|
||||||
|
"full": "Full",
|
||||||
|
"running": "Running",
|
||||||
|
"completed": "Completed",
|
||||||
|
"cancelled": "Cancelled"
|
||||||
|
},
|
||||||
|
"registrationStatus": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"confirmed": "Confirmed",
|
||||||
|
"waitlisted": "Waitlisted",
|
||||||
|
"cancelled": "Cancelled"
|
||||||
|
},
|
||||||
|
"holidayPasses": {
|
||||||
|
"title": "Holiday Passes",
|
||||||
|
"noPasses": "No holiday passes found"
|
||||||
|
}
|
||||||
|
}
|
||||||
113
apps/web/i18n/messages/en/finance.json
Normal file
113
apps/web/i18n/messages/en/finance.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"overview": "Finance",
|
||||||
|
"invoices": "Invoices",
|
||||||
|
"sepa": "SEPA Batches",
|
||||||
|
"payments": "Payments",
|
||||||
|
"newInvoice": "New Invoice",
|
||||||
|
"newBatch": "New Batch"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Finance – Overview",
|
||||||
|
"subtitle": "Manage invoices, SEPA batches, and payments",
|
||||||
|
"openInvoices": "Open Invoices",
|
||||||
|
"overdueInvoices": "Overdue Invoices",
|
||||||
|
"totalRevenue": "Total Revenue",
|
||||||
|
"sepaBatches": "SEPA Batches"
|
||||||
|
},
|
||||||
|
"invoices": {
|
||||||
|
"title": "Invoices",
|
||||||
|
"newInvoice": "New Invoice",
|
||||||
|
"noInvoices": "No invoices found",
|
||||||
|
"createFirst": "Create your first invoice.",
|
||||||
|
"invoiceNumber": "Invoice No.",
|
||||||
|
"recipient": "Recipient",
|
||||||
|
"issueDate": "Issue Date",
|
||||||
|
"dueDate": "Due Date",
|
||||||
|
"amount": "Amount",
|
||||||
|
"notFound": "Invoice not found"
|
||||||
|
},
|
||||||
|
"invoiceForm": {
|
||||||
|
"title": "Invoice Details",
|
||||||
|
"invoiceNumber": "Invoice Number *",
|
||||||
|
"issueDate": "Issue Date",
|
||||||
|
"dueDate": "Due Date *",
|
||||||
|
"recipient": "Recipient",
|
||||||
|
"recipientName": "Name *",
|
||||||
|
"recipientAddress": "Address",
|
||||||
|
"lineItems": "Line Items",
|
||||||
|
"addLineItem": "+ Add Line Item",
|
||||||
|
"itemDescription": "Description *",
|
||||||
|
"quantity": "Quantity",
|
||||||
|
"unitPrice": "Unit Price (€)",
|
||||||
|
"removeItem": "Remove Item",
|
||||||
|
"amounts": "Amounts",
|
||||||
|
"taxRate": "Tax Rate (%)",
|
||||||
|
"subtotal": "Subtotal (net)",
|
||||||
|
"tax": "Tax ({rate}%)",
|
||||||
|
"total": "Total Amount",
|
||||||
|
"notes": "Notes",
|
||||||
|
"created": "Invoice created successfully",
|
||||||
|
"errorCreating": "Error creating invoice"
|
||||||
|
},
|
||||||
|
"invoiceStatus": {
|
||||||
|
"draft": "Draft",
|
||||||
|
"sent": "Sent",
|
||||||
|
"paid": "Paid",
|
||||||
|
"overdue": "Overdue",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
|
"credited": "Credited"
|
||||||
|
},
|
||||||
|
"sepa": {
|
||||||
|
"title": "SEPA Batches",
|
||||||
|
"newBatch": "New Batch",
|
||||||
|
"noBatches": "No SEPA batches found",
|
||||||
|
"createFirst": "Create your first SEPA batch.",
|
||||||
|
"directDebit": "Direct Debit",
|
||||||
|
"creditTransfer": "Credit Transfer",
|
||||||
|
"executionDate": "Execution Date",
|
||||||
|
"totalAmount": "Total Amount",
|
||||||
|
"itemCount": "Items",
|
||||||
|
"downloadXml": "Download XML",
|
||||||
|
"notFound": "Batch not found"
|
||||||
|
},
|
||||||
|
"sepaBatchForm": {
|
||||||
|
"title": "Create SEPA Batch",
|
||||||
|
"batchType": "Type",
|
||||||
|
"directDebit": "Direct Debit (SEPA Core)",
|
||||||
|
"creditTransfer": "Credit Transfer",
|
||||||
|
"description": "Description",
|
||||||
|
"descriptionPlaceholder": "e.g. Membership fees Q1 2026",
|
||||||
|
"executionDate": "Execution Date *",
|
||||||
|
"executionDateRequired": "Execution date is required",
|
||||||
|
"created": "SEPA batch created",
|
||||||
|
"errorCreating": "Error creating batch"
|
||||||
|
},
|
||||||
|
"sepaBatchStatus": {
|
||||||
|
"draft": "Draft",
|
||||||
|
"ready": "Ready",
|
||||||
|
"submitted": "Submitted",
|
||||||
|
"executed": "Executed",
|
||||||
|
"failed": "Failed",
|
||||||
|
"cancelled": "Cancelled"
|
||||||
|
},
|
||||||
|
"sepaItemStatus": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"success": "Processed",
|
||||||
|
"failed": "Failed",
|
||||||
|
"rejected": "Rejected"
|
||||||
|
},
|
||||||
|
"payments": {
|
||||||
|
"title": "Payment Overview",
|
||||||
|
"paidInvoices": "Paid Invoices",
|
||||||
|
"openInvoices": "Open Invoices",
|
||||||
|
"overdueInvoices": "Overdue Invoices",
|
||||||
|
"sepaBatches": "SEPA Batches"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"creating": "Creating...",
|
||||||
|
"membershipFee": "Membership Fee",
|
||||||
|
"sepaDirectDebit": "SEPA Direct Debit"
|
||||||
|
}
|
||||||
|
}
|
||||||
80
apps/web/i18n/messages/en/meetings.json
Normal file
80
apps/web/i18n/messages/en/meetings.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"overview": "Overview",
|
||||||
|
"protocols": "Protocols",
|
||||||
|
"tasks": "Tasks",
|
||||||
|
"newProtocol": "New Protocol"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"overviewTitle": "Meeting Protocols",
|
||||||
|
"protocolsTitle": "Meeting Protocols - Protocols",
|
||||||
|
"tasksTitle": "Meeting Protocols - Tasks"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Meeting Protocols – Overview",
|
||||||
|
"subtitle": "Manage protocols, agenda items, and tasks",
|
||||||
|
"totalProtocols": "Total Protocols",
|
||||||
|
"publishedProtocols": "Published",
|
||||||
|
"draftProtocols": "Drafts",
|
||||||
|
"overdueTasks": "Overdue Tasks",
|
||||||
|
"recentProtocols": "Recent Protocols",
|
||||||
|
"noRecentProtocols": "No protocols yet."
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"title": "Title *",
|
||||||
|
"meetingDate": "Meeting Date *",
|
||||||
|
"meetingType": "Meeting Type",
|
||||||
|
"location": "Location",
|
||||||
|
"created": "Protocol created successfully",
|
||||||
|
"updated": "Protocol updated",
|
||||||
|
"published": "Protocol published",
|
||||||
|
"errorSaving": "Error saving protocol",
|
||||||
|
"notFound": "Protocol not found",
|
||||||
|
"noProtocols": "No protocols found",
|
||||||
|
"createFirst": "Create your first meeting protocol to get started."
|
||||||
|
},
|
||||||
|
"protocolList": {
|
||||||
|
"searchPlaceholder": "Search protocols...",
|
||||||
|
"title": "Protocols ({count})",
|
||||||
|
"date": "Date",
|
||||||
|
"titleColumn": "Title",
|
||||||
|
"type": "Meeting Type",
|
||||||
|
"locationColumn": "Location",
|
||||||
|
"statusColumn": "Status",
|
||||||
|
"published": "Published",
|
||||||
|
"draft": "Draft"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"agendaItems": "Agenda Items",
|
||||||
|
"noItems": "No agenda items found.",
|
||||||
|
"addItem": "Add Item",
|
||||||
|
"statusUpdated": "Status updated",
|
||||||
|
"deleted": "Agenda item deleted",
|
||||||
|
"deleteConfirm": "Delete this agenda item?",
|
||||||
|
"errorUpdating": "Error updating",
|
||||||
|
"errorDeleting": "Error deleting",
|
||||||
|
"dueDate": "Due: {date}",
|
||||||
|
"responsible": "Responsible: {name}",
|
||||||
|
"status": {
|
||||||
|
"open": "Open",
|
||||||
|
"in_progress": "In Progress",
|
||||||
|
"done": "Done",
|
||||||
|
"cancelled": "Cancelled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"title": "Open Tasks ({count})",
|
||||||
|
"noTasks": "No open tasks",
|
||||||
|
"allTasksDone": "All tasks are completed.",
|
||||||
|
"overdue": "Overdue",
|
||||||
|
"fromProtocol": "from protocol"
|
||||||
|
},
|
||||||
|
"meetingTypes": {
|
||||||
|
"regular": "Regular Meeting",
|
||||||
|
"extraordinary": "Extraordinary Meeting",
|
||||||
|
"board": "Board Meeting",
|
||||||
|
"general_assembly": "General Assembly",
|
||||||
|
"committee": "Committee Meeting",
|
||||||
|
"other": "Other"
|
||||||
|
}
|
||||||
|
}
|
||||||
76
apps/web/i18n/messages/en/newsletter.json
Normal file
76
apps/web/i18n/messages/en/newsletter.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"newsletters": "Newsletters",
|
||||||
|
"templates": "Templates",
|
||||||
|
"newNewsletter": "New Newsletter"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"title": "Newsletters",
|
||||||
|
"subtitle": "Create and send newsletters",
|
||||||
|
"newNewsletter": "New Newsletter",
|
||||||
|
"noNewsletters": "No newsletters found",
|
||||||
|
"createFirst": "Create your first newsletter to get started.",
|
||||||
|
"subject": "Subject",
|
||||||
|
"noSubject": "(No Subject)",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"created": "Created",
|
||||||
|
"sent": "Sent",
|
||||||
|
"totalSent": "Sent",
|
||||||
|
"totalRecipients": "Total Recipients"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"title": "Newsletter Details",
|
||||||
|
"backToList": "Back to Newsletters",
|
||||||
|
"sendNewsletter": "Send Newsletter",
|
||||||
|
"recipientsSection": "Recipients",
|
||||||
|
"sentCount": "Sent",
|
||||||
|
"failedCount": "Failed",
|
||||||
|
"noRecipients": "No recipients added. Add recipients from your member list.",
|
||||||
|
"recipientName": "Name",
|
||||||
|
"recipientEmail": "Email",
|
||||||
|
"recipientStatus": "Status",
|
||||||
|
"notFound": "Newsletter not found"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"content": "Newsletter Content",
|
||||||
|
"subject": "Subject *",
|
||||||
|
"bodyHtml": "Content (HTML) *",
|
||||||
|
"bodyHtmlPlaceholder": "<h1>Hello!</h1><p>Your newsletter content...</p>",
|
||||||
|
"bodyText": "Plain Text Version",
|
||||||
|
"bodyTextHelp": "Plain text fallback for email clients without HTML support",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"scheduledDate": "Scheduled Send (optional)",
|
||||||
|
"scheduleHelp": "Leave empty to save the newsletter as a draft.",
|
||||||
|
"created": "Newsletter created successfully",
|
||||||
|
"errorCreating": "Error creating newsletter"
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"title": "Newsletter Templates",
|
||||||
|
"subtitle": "Reusable templates for newsletters",
|
||||||
|
"newTemplate": "New Template",
|
||||||
|
"noTemplates": "No templates found",
|
||||||
|
"createFirst": "Create your first newsletter template for reuse in campaigns.",
|
||||||
|
"allTemplates": "All Templates ({count})",
|
||||||
|
"name": "Name",
|
||||||
|
"subject": "Subject",
|
||||||
|
"variables": "Variables"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"draft": "Draft",
|
||||||
|
"scheduled": "Scheduled",
|
||||||
|
"sending": "Sending",
|
||||||
|
"sent": "Sent",
|
||||||
|
"failed": "Failed"
|
||||||
|
},
|
||||||
|
"recipientStatus": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"sent": "Sent",
|
||||||
|
"failed": "Failed",
|
||||||
|
"bounced": "Bounced"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"creating": "Creating...",
|
||||||
|
"create": "Create Newsletter"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
apps/web/i18n/messages/en/siteBuilder.json
Normal file
53
apps/web/i18n/messages/en/siteBuilder.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"pages": "Pages",
|
||||||
|
"posts": "Posts",
|
||||||
|
"settings": "Settings"
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"title": "Pages",
|
||||||
|
"newPage": "New Page",
|
||||||
|
"noPages": "No pages found",
|
||||||
|
"createFirst": "Create your first page.",
|
||||||
|
"pageTitle": "Page Title *",
|
||||||
|
"slug": "URL Path",
|
||||||
|
"slugPlaceholder": "Leave empty for auto-generation...",
|
||||||
|
"membersOnly": "Members Only",
|
||||||
|
"createAndEdit": "Create Page & Open Editor",
|
||||||
|
"pageCreated": "Page created — opening editor",
|
||||||
|
"errorCreating": "Error creating page",
|
||||||
|
"notFound": "Page not found",
|
||||||
|
"published": "Page published",
|
||||||
|
"error": "Error"
|
||||||
|
},
|
||||||
|
"posts": {
|
||||||
|
"title": "Posts",
|
||||||
|
"newPost": "New Post",
|
||||||
|
"noPosts": "No posts found",
|
||||||
|
"createFirst": "Create your first post.",
|
||||||
|
"postTitle": "Title *",
|
||||||
|
"content": "Post content (HTML allowed)...",
|
||||||
|
"excerpt": "Excerpt",
|
||||||
|
"postCreated": "Post created",
|
||||||
|
"errorCreating": "Error"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"saved": "Settings saved",
|
||||||
|
"error": "Error"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"publish": "Publish",
|
||||||
|
"unpublish": "Unpublish"
|
||||||
|
},
|
||||||
|
"portal": {
|
||||||
|
"loginSuccess": "Logged in successfully",
|
||||||
|
"membersArea": "Members Area"
|
||||||
|
},
|
||||||
|
"blocks": {
|
||||||
|
"news": "News",
|
||||||
|
"events": "Events",
|
||||||
|
"loginError": "Login error",
|
||||||
|
"connectionError": "Connection error"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,14 @@ const namespaces = [
|
|||||||
'verband',
|
'verband',
|
||||||
'members',
|
'members',
|
||||||
'fischerei',
|
'fischerei',
|
||||||
|
'meetings',
|
||||||
|
'courses',
|
||||||
|
'finance',
|
||||||
|
'newsletter',
|
||||||
|
'siteBuilder',
|
||||||
|
'events',
|
||||||
|
'documents',
|
||||||
|
'bookings',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ export function CreateBookingForm({ accountId, account, rooms }: Props) {
|
|||||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">— Zimmer wählen —</option>
|
<option value="">— Zimmer wählen —</option>
|
||||||
{rooms.map((r) => (
|
{rooms.map((room) => (
|
||||||
<option key={r.id} value={r.id}>
|
<option key={room.id} value={room.id}>
|
||||||
{r.roomNumber}
|
{room.roomNumber}
|
||||||
{r.name ? ` – ${r.name}` : ''} ({r.pricePerNight}{' '}
|
{room.name ? ` – ${room.name}` : ''} (
|
||||||
€/Nacht)
|
{room.pricePerNight} €/Nacht)
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -240,10 +240,19 @@ export function CreateBookingForm({ accountId, account, rooms }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
data-test="booking-cancel-btn"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
data-test="booking-submit-btn"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Buchung erstellen'}
|
{isPending ? 'Wird erstellt...' : 'Buchung erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -296,10 +296,19 @@ export function CreateCourseForm({ accountId, account }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
data-test="course-cancel-btn"
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
data-test="course-submit-btn"
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Kurs erstellen'}
|
{isPending ? 'Wird erstellt...' : 'Kurs erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -352,10 +352,19 @@ export function CreateEventForm({ accountId, account }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
data-test="event-cancel-btn"
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
data-test="event-submit-btn"
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Veranstaltung erstellen'}
|
{isPending ? 'Wird erstellt...' : 'Veranstaltung erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
onSuccess: ({ data }) => {
|
onSuccess: ({ data }) => {
|
||||||
if (data?.success) {
|
if (data?.success) {
|
||||||
toast.success('Rechnung erfolgreich erstellt');
|
toast.success('Rechnung erfolgreich erstellt');
|
||||||
router.push(`/home/${account}/finance-cms`);
|
router.push(`/home/${account}/finance/invoices`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: ({ error }) => {
|
onError: ({ error }) => {
|
||||||
@@ -98,7 +98,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Rechnungsnummer *</FormLabel>
|
<FormLabel>Rechnungsnummer *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input data-test="invoice-number-input" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -111,7 +111,11 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Rechnungsdatum</FormLabel>
|
<FormLabel>Rechnungsdatum</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="date" {...field} />
|
<Input
|
||||||
|
type="date"
|
||||||
|
data-test="invoice-issue-date-input"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -124,7 +128,11 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Fälligkeitsdatum *</FormLabel>
|
<FormLabel>Fälligkeitsdatum *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="date" {...field} />
|
<Input
|
||||||
|
type="date"
|
||||||
|
data-test="invoice-due-date-input"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -145,7 +153,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name *</FormLabel>
|
<FormLabel>Name *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input data-test="invoice-recipient-input" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -178,6 +186,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
data-test="invoice-add-item-btn"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
append({ description: '', quantity: 1, unitPrice: 0 })
|
append({ description: '', quantity: 1, unitPrice: 0 })
|
||||||
}
|
}
|
||||||
@@ -287,6 +296,7 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
min={0}
|
min={0}
|
||||||
step="0.5"
|
step="0.5"
|
||||||
className="max-w-[120px]"
|
className="max-w-[120px]"
|
||||||
|
data-test="invoice-tax-rate-input"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
@@ -329,10 +339,19 @@ export function CreateInvoiceForm({ accountId, account }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
data-test="invoice-cancel-btn"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
data-test="invoice-submit-btn"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Rechnung erstellen'}
|
{isPending ? 'Wird erstellt...' : 'Rechnung erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<select
|
<select
|
||||||
{...field}
|
{...field}
|
||||||
|
data-test="sepa-batch-type-select"
|
||||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||||
>
|
>
|
||||||
<option value="direct_debit">
|
<option value="direct_debit">
|
||||||
@@ -104,6 +105,7 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="z.B. Mitgliedsbeiträge Q1 2026"
|
placeholder="z.B. Mitgliedsbeiträge Q1 2026"
|
||||||
|
data-test="sepa-batch-description-input"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -119,7 +121,11 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Ausführungsdatum *</FormLabel>
|
<FormLabel>Ausführungsdatum *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="date" {...field} />
|
<Input
|
||||||
|
type="date"
|
||||||
|
data-test="sepa-batch-date-input"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -129,10 +135,19 @@ export function CreateSepaBatchForm({ accountId, account }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
data-test="sepa-batch-cancel-btn"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
data-test="sepa-batch-submit-btn"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Einzug erstellen'}
|
{isPending ? 'Wird erstellt...' : 'Einzug erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Betreff *</FormLabel>
|
<FormLabel>Betreff *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} data-test="newsletter-subject-input" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -88,6 +88,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
|||||||
rows={12}
|
rows={12}
|
||||||
className="border-input bg-background flex min-h-[200px] w-full rounded-md border px-3 py-2 font-mono text-sm"
|
className="border-input bg-background flex min-h-[200px] w-full rounded-md border px-3 py-2 font-mono text-sm"
|
||||||
placeholder="<h1>Hallo!</h1><p>Ihr Newsletter-Inhalt...</p>"
|
placeholder="<h1>Hallo!</h1><p>Ihr Newsletter-Inhalt...</p>"
|
||||||
|
data-test="newsletter-body-input"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -106,6 +107,7 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
|||||||
rows={4}
|
rows={4}
|
||||||
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
|
className="border-input bg-background flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm"
|
||||||
placeholder="Nur-Text-Fallback für E-Mail-Clients ohne HTML-Unterstützung"
|
placeholder="Nur-Text-Fallback für E-Mail-Clients ohne HTML-Unterstützung"
|
||||||
|
data-test="newsletter-text-input"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -127,7 +129,11 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Geplanter Versand (optional)</FormLabel>
|
<FormLabel>Geplanter Versand (optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="datetime-local" {...field} />
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
{...field}
|
||||||
|
data-test="newsletter-schedule-input"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Leer lassen, um den Newsletter als Entwurf zu speichern.
|
Leer lassen, um den Newsletter als Entwurf zu speichern.
|
||||||
@@ -140,10 +146,19 @@ export function CreateNewsletterForm({ accountId, account }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
data-test="newsletter-cancel-btn"
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
data-test="newsletter-submit-btn"
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Newsletter erstellen'}
|
{isPending ? 'Wird erstellt...' : 'Newsletter erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -148,10 +148,19 @@ export function CreatePageForm({ accountId, account }: Props) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
data-test="page-cancel-btn"
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
data-test="page-submit-btn"
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Seite erstellen & Editor öffnen'}
|
{isPending ? 'Wird erstellt...' : 'Seite erstellen & Editor öffnen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,10 +159,19 @@ export function CreatePostForm({ accountId, account }: Props) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
data-test="post-cancel-btn"
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
data-test="post-submit-btn"
|
||||||
|
>
|
||||||
{isPending ? 'Wird erstellt...' : 'Beitrag erstellen'}
|
{isPending ? 'Wird erstellt...' : 'Beitrag erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -465,15 +465,17 @@ const EventListBlock = ({
|
|||||||
<h2 className="mb-6 text-2xl font-bold">Veranstaltungen</h2>
|
<h2 className="mb-6 text-2xl font-bold">Veranstaltungen</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{items.map((event) => {
|
{items.map((event) => {
|
||||||
const d = new Date(event.event_date);
|
const eventDate = new Date(event.event_date);
|
||||||
const isExpanded = expandedId === event.id;
|
const isExpanded = expandedId === event.id;
|
||||||
const isSuccess = successId === event.id;
|
const isSuccess = successId === event.id;
|
||||||
return (
|
return (
|
||||||
<div key={event.id} className="overflow-hidden rounded-lg border">
|
<div key={event.id} className="overflow-hidden rounded-lg border">
|
||||||
<div className="flex items-center gap-4 p-4">
|
<div className="flex items-center gap-4 p-4">
|
||||||
<div className="bg-primary/10 text-primary flex h-14 w-14 shrink-0 flex-col items-center justify-center rounded-lg">
|
<div className="bg-primary/10 text-primary flex h-14 w-14 shrink-0 flex-col items-center justify-center rounded-lg">
|
||||||
<span className="text-lg font-bold">{d.getDate()}</span>
|
<span className="text-lg font-bold">
|
||||||
<span className="text-xs">{formatMonthShort(d)}</span>
|
{eventDate.getDate()}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs">{formatMonthShort(eventDate)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h3 className="font-semibold">{event.name}</h3>
|
<h3 className="font-semibold">{event.name}</h3>
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export function CreateProtocolForm({
|
|||||||
<FormLabel>Titel *</FormLabel>
|
<FormLabel>Titel *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
data-test="protocol-title-input"
|
||||||
placeholder="z.B. Vorstandssitzung März 2026"
|
placeholder="z.B. Vorstandssitzung März 2026"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -100,7 +101,11 @@ export function CreateProtocolForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Sitzungsdatum *</FormLabel>
|
<FormLabel>Sitzungsdatum *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="date" {...field} />
|
<Input
|
||||||
|
data-test="protocol-date-input"
|
||||||
|
type="date"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -115,6 +120,7 @@ export function CreateProtocolForm({
|
|||||||
<FormLabel>Sitzungsart *</FormLabel>
|
<FormLabel>Sitzungsart *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<select
|
<select
|
||||||
|
data-test="protocol-type-select"
|
||||||
{...field}
|
{...field}
|
||||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||||
>
|
>
|
||||||
@@ -136,7 +142,11 @@ export function CreateProtocolForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Ort</FormLabel>
|
<FormLabel>Ort</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="z.B. Vereinsheim" {...field} />
|
<Input
|
||||||
|
data-test="protocol-location-input"
|
||||||
|
placeholder="z.B. Vereinsheim"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -223,10 +233,19 @@ export function CreateProtocolForm({
|
|||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
data-test="protocol-cancel-btn"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
data-test="protocol-submit-btn"
|
||||||
|
>
|
||||||
{isPending ? 'Wird gespeichert...' : 'Protokoll erstellen'}
|
{isPending ? 'Wird gespeichert...' : 'Protokoll erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
interface MeetingsTabNavigationProps {
|
interface MeetingsTabNavigationProps {
|
||||||
@@ -10,9 +11,9 @@ interface MeetingsTabNavigationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'overview', label: 'Übersicht', path: '' },
|
{ id: 'overview', i18nKey: 'meetings:nav.overview', path: '' },
|
||||||
{ id: 'protocols', label: 'Protokolle', path: '/protocols' },
|
{ id: 'protocols', i18nKey: 'meetings:nav.protocols', path: '/protocols' },
|
||||||
{ id: 'tasks', label: 'Offene Aufgaben', path: '/tasks' },
|
{ id: 'tasks', i18nKey: 'meetings:nav.tasks', path: '/tasks' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function MeetingsTabNavigation({
|
export function MeetingsTabNavigation({
|
||||||
@@ -25,7 +26,7 @@ export function MeetingsTabNavigation({
|
|||||||
<div className="mb-6 border-b">
|
<div className="mb-6 border-b">
|
||||||
<nav
|
<nav
|
||||||
className="-mb-px flex space-x-1 overflow-x-auto"
|
className="-mb-px flex space-x-1 overflow-x-auto"
|
||||||
aria-label="Sitzungsprotokolle Navigation"
|
aria-label="Meeting Protocols Navigation"
|
||||||
>
|
>
|
||||||
{TABS.map((tab) => {
|
{TABS.map((tab) => {
|
||||||
const isActive = tab.id === activeTab;
|
const isActive = tab.id === activeTab;
|
||||||
@@ -34,6 +35,7 @@ export function MeetingsTabNavigation({
|
|||||||
<Link
|
<Link
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
href={`${basePath}${tab.path}`}
|
href={`${basePath}${tab.path}`}
|
||||||
|
data-test={`meetings-tab-${tab.id}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-b-2 px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
|
'border-b-2 px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
|
||||||
isActive
|
isActive
|
||||||
@@ -41,7 +43,7 @@ export function MeetingsTabNavigation({
|
|||||||
: 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent',
|
: 'text-muted-foreground hover:border-muted-foreground/30 hover:text-foreground border-transparent',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tab.label}
|
<Trans i18nKey={tab.i18nKey} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export function OpenTasksView({
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
data-test="tasks-prev-btn"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={page <= 1}
|
disabled={page <= 1}
|
||||||
@@ -182,6 +183,7 @@ export function OpenTasksView({
|
|||||||
Zurück
|
Zurück
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
data-test="tasks-next-btn"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={page >= totalPages}
|
disabled={page >= totalPages}
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export function ProtocolItemsList({
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-center">
|
<td className="p-3 text-center">
|
||||||
<Badge
|
<Badge
|
||||||
|
data-test="item-status-toggle"
|
||||||
variant={
|
variant={
|
||||||
(ITEM_STATUS_COLORS[item.status] as
|
(ITEM_STATUS_COLORS[item.status] as
|
||||||
| 'default'
|
| 'default'
|
||||||
@@ -156,6 +157,7 @@ export function ProtocolItemsList({
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-right">
|
<td className="p-3 text-right">
|
||||||
<Button
|
<Button
|
||||||
|
data-test="item-delete-btn"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
|
|||||||
@@ -69,16 +69,16 @@ export function ProtocolsDataTable({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSearch = useCallback(
|
const handleSearch = useCallback(
|
||||||
(e: React.FormEvent) => {
|
(formEvent: React.FormEvent) => {
|
||||||
e.preventDefault();
|
formEvent.preventDefault();
|
||||||
updateParams({ q: form.getValues('search') });
|
updateParams({ q: form.getValues('search') });
|
||||||
},
|
},
|
||||||
[form, updateParams],
|
[form, updateParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTypeChange = useCallback(
|
const handleTypeChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
(changeEvent: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
updateParams({ type: e.target.value });
|
updateParams({ type: changeEvent.target.value });
|
||||||
},
|
},
|
||||||
[updateParams],
|
[updateParams],
|
||||||
);
|
);
|
||||||
@@ -96,17 +96,24 @@ export function ProtocolsDataTable({
|
|||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<form onSubmit={handleSearch} className="flex gap-2">
|
<form onSubmit={handleSearch} className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
data-test="protocols-search-input"
|
||||||
placeholder="Protokoll suchen..."
|
placeholder="Protokoll suchen..."
|
||||||
className="w-64"
|
className="w-64"
|
||||||
{...form.register('search')}
|
{...form.register('search')}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" variant="outline" size="sm">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
data-test="protocols-search-btn"
|
||||||
|
>
|
||||||
Suchen
|
Suchen
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<select
|
<select
|
||||||
|
data-test="protocols-type-filter"
|
||||||
value={currentType}
|
value={currentType}
|
||||||
onChange={handleTypeChange}
|
onChange={handleTypeChange}
|
||||||
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
|
className="border-input bg-background flex h-9 rounded-md border px-3 py-1 text-sm shadow-sm"
|
||||||
@@ -119,7 +126,7 @@ export function ProtocolsDataTable({
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<Link href={`/home/${account}/meetings/protocols/new`}>
|
<Link href={`/home/${account}/meetings/protocols/new`}>
|
||||||
<Button size="sm">
|
<Button size="sm" data-test="protocols-new-btn">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Neues Protokoll
|
Neues Protokoll
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user