Files
myeasycms-v2/apps/web/app/[locale]/home/[account]/newsletter/page.tsx
Zaid Marzguioui ebd0fd4638
Some checks failed
Workflow / ʦ TypeScript (push) Failing after 6m26s
Workflow / ⚫️ Test (push) Has been skipped
feat: complete CMS v2 with Docker, Fischerei, Meetings, Verband modules + UX audit fixes
Major changes:
- Docker Compose: full Supabase stack (11 services) equivalent to supabase CLI
- Fischerei module: 16 DB tables, waters/species/stocking/catch books/competitions
- Sitzungsprotokolle module: meeting protocols, agenda items, task tracking
- Verbandsverwaltung module: federation management, member clubs, contacts, fees
- Per-account module activation via Modules page toggle
- Site Builder: live CMS data in Puck blocks (courses, events, membership registration)
- Public registration APIs: course signup, event registration, membership application
- Document generation: PDF member cards, Excel reports, HTML labels
- Landing page: real Com.BISS content (no filler text)
- UX audit fixes: AccountNotFound component, shared status badges, confirm dialog,
  pagination, duplicate heading removal, emoji→badge replacement, a11y fixes
- QA: healthcheck fix, API auth fix, enum mismatch fix, password required attribute
2026-03-31 16:35:46 +02:00

215 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from 'next/link';
import { ChevronLeft, ChevronRight, Mail, Plus, Send, Users } from 'lucide-react';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
import { createNewsletterApi } from '@kit/newsletter/api';
import { CmsPageShell } from '~/components/cms-page-shell';
import { EmptyState } from '~/components/empty-state';
import { StatsCard } from '~/components/stats-card';
import { AccountNotFound } from '~/components/account-not-found';
import {
NEWSLETTER_STATUS_VARIANT,
NEWSLETTER_STATUS_LABEL,
} from '~/lib/status-badges';
const PAGE_SIZE = 25;
interface PageProps {
params: Promise<{ account: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}
export default async function NewsletterPage({ params, searchParams }: PageProps) {
const { account } = await params;
const search = await searchParams;
const client = getSupabaseServerClient();
const { data: acct } = await client
.from('accounts')
.select('id')
.eq('slug', account)
.single();
if (!acct) return <AccountNotFound />;
const api = createNewsletterApi(client);
const allNewsletters = await api.listNewsletters(acct.id);
const sentCount = allNewsletters.filter(
(n: Record<string, unknown>) => n.status === 'sent',
).length;
const totalRecipients = allNewsletters.reduce(
(sum: number, n: Record<string, unknown>) =>
sum + (Number(n.total_recipients) || 0),
0,
);
// Pagination
const currentPage = Math.max(1, Number(search.page) || 1);
const totalItems = allNewsletters.length;
const totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE));
const safePage = Math.min(currentPage, totalPages);
const startIdx = (safePage - 1) * PAGE_SIZE;
const newsletters = allNewsletters.slice(startIdx, startIdx + PAGE_SIZE);
return (
<CmsPageShell account={account} title="Newsletter">
<div className="flex w-full flex-col gap-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Newsletter</h1>
<p className="text-muted-foreground">
Newsletter erstellen und versenden
</p>
</div>
<Link href={`/home/${account}/newsletter/new`}>
<Button>
<Plus className="mr-2 h-4 w-4" />
Neuer Newsletter
</Button>
</Link>
</div>
{/* Stats */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<StatsCard
title="Newsletter"
value={totalItems}
icon={<Mail className="h-5 w-5" />}
/>
<StatsCard
title="Gesendet"
value={sentCount}
icon={<Send className="h-5 w-5" />}
/>
<StatsCard
title="Empfänger gesamt"
value={totalRecipients}
icon={<Users className="h-5 w-5" />}
/>
</div>
{/* Table or Empty State */}
{totalItems === 0 ? (
<EmptyState
icon={<Mail className="h-8 w-8" />}
title="Keine Newsletter vorhanden"
description="Erstellen Sie Ihren ersten Newsletter, um loszulegen."
actionLabel="Neuer Newsletter"
actionHref={`/home/${account}/newsletter/new`}
/>
) : (
<Card>
<CardHeader>
<CardTitle>Alle Newsletter ({totalItems})</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="p-3 text-left font-medium">Betreff</th>
<th className="p-3 text-left font-medium">Status</th>
<th className="p-3 text-right font-medium">Empfänger</th>
<th className="p-3 text-left font-medium">Erstellt</th>
<th className="p-3 text-left font-medium">Gesendet</th>
</tr>
</thead>
<tbody>
{newsletters.map((nl: Record<string, unknown>) => (
<tr
key={String(nl.id)}
className="border-b hover:bg-muted/30"
>
<td className="p-3 font-medium">
<Link
href={`/home/${account}/newsletter/${String(nl.id)}`}
className="hover:underline"
>
{String(nl.subject ?? '(Kein Betreff)')}
</Link>
</td>
<td className="p-3">
<Badge
variant={
NEWSLETTER_STATUS_VARIANT[String(nl.status)] ?? 'secondary'
}
>
{NEWSLETTER_STATUS_LABEL[String(nl.status)] ?? String(nl.status)}
</Badge>
</td>
<td className="p-3 text-right">
{nl.total_recipients != null
? String(nl.total_recipients)
: '—'}
</td>
<td className="p-3">
{nl.created_at
? new Date(String(nl.created_at)).toLocaleDateString('de-DE')
: '—'}
</td>
<td className="p-3">
{nl.sent_at
? new Date(String(nl.sent_at)).toLocaleDateString('de-DE')
: '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between pt-4">
<p className="text-sm text-muted-foreground">
{startIdx + 1}{Math.min(startIdx + PAGE_SIZE, totalItems)} von {totalItems}
</p>
<div className="flex items-center gap-1">
{safePage > 1 ? (
<Link href={`/home/${account}/newsletter?page=${safePage - 1}`}>
<Button variant="outline" size="sm">
<ChevronLeft className="h-4 w-4" />
</Button>
</Link>
) : (
<Button variant="outline" size="sm" disabled>
<ChevronLeft className="h-4 w-4" />
</Button>
)}
<span className="px-3 text-sm font-medium">
{safePage} / {totalPages}
</span>
{safePage < totalPages ? (
<Link href={`/home/${account}/newsletter?page=${safePage + 1}`}>
<Button variant="outline" size="sm">
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
) : (
<Button variant="outline" size="sm" disabled>
<ChevronRight className="h-4 w-4" />
</Button>
)}
</div>
</div>
)}
</CardContent>
</Card>
)}
</div>
</CmsPageShell>
);
}