feat: MyEasyCMS v2 — Full SaaS rebuild
Complete rebuild of 22-year-old PHP CMS as modern SaaS: Database (15 migrations, 42+ tables): - Foundation: account_settings, audit_log, GDPR register, cms_files - Module Engine: modules, fields, records, permissions, relations + RPC - Members: 45+ field member profiles, departments, roles, honors, SEPA mandates - Courses: courses, sessions, categories, instructors, locations, attendance - Bookings: rooms, guests, bookings with availability - Events: events, registrations, holiday passes - Finance: SEPA batches/items (pain.008/001 XML), invoices - Newsletter: campaigns, templates, recipients, subscriptions - Site Builder: site_pages (Puck JSON), site_settings, cms_posts - Portal Auth: member_portal_invitations, user linking Feature Packages (9): - @kit/module-builder — dynamic low-code CRUD engine - @kit/member-management — 31 API methods, 21 actions, 8 components - @kit/course-management, @kit/booking-management, @kit/event-management - @kit/finance — SEPA XML generator + IBAN validator - @kit/newsletter — campaigns + dispatch - @kit/document-generator — PDF/Excel/Word - @kit/site-builder — Puck visual editor, 15 blocks, public rendering Pages (60+): - Dashboard with real stats from all APIs - Full CRUD for all 8 domains with react-hook-form + Zod - Recharts statistics - German i18n throughout - Member portal with auth + invitation system - Public club websites via Puck at /club/[slug] Infrastructure: - Dockerfile (multi-stage, standalone output) - docker-compose.yml (Supabase self-hosted + Next.js) - Kong API gateway config - .env.production.example
This commit is contained in:
@@ -1,235 +1,28 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft, BedDouble } from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
|
||||
import { createBookingManagementApi } from '@kit/booking-management/api';
|
||||
|
||||
import { CreateBookingForm } from '@kit/booking-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function NewBookingPage({ params }: PageProps) {
|
||||
export default async function NewBookingPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createBookingManagementApi(client);
|
||||
|
||||
const [rooms, guests] = await Promise.all([
|
||||
api.listRooms(acct.id),
|
||||
api.listGuests(acct.id),
|
||||
]);
|
||||
const rooms = await api.listRooms(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neue Buchung">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href={`/home/${account}/bookings`}>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Neue Buchung</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Buchung für ein Zimmer erstellen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="flex flex-col gap-6">
|
||||
{/* Zimmer & Gast */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BedDouble className="h-5 w-5" />
|
||||
Zimmer & Gast
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Wählen Sie Zimmer und Gast für die Buchung aus
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor="room_id"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Zimmer
|
||||
</label>
|
||||
<select
|
||||
id="room_id"
|
||||
name="room_id"
|
||||
required
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<option value="">Zimmer wählen…</option>
|
||||
{rooms.map((room: Record<string, unknown>) => (
|
||||
<option key={String(room.id)} value={String(room.id)}>
|
||||
{String(room.room_number)} – {String(room.name ?? room.room_type ?? '')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor="guest_id"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Gast
|
||||
</label>
|
||||
<select
|
||||
id="guest_id"
|
||||
name="guest_id"
|
||||
required
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<option value="">Gast wählen…</option>
|
||||
{guests.map((guest: Record<string, unknown>) => (
|
||||
<option key={String(guest.id)} value={String(guest.id)}>
|
||||
{String(guest.last_name)}, {String(guest.first_name)}
|
||||
{guest.email ? ` (${String(guest.email)})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Aufenthalt */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Aufenthalt</CardTitle>
|
||||
<CardDescription>
|
||||
Reisedaten und Personenanzahl
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="check_in" className="text-sm font-medium">
|
||||
Anreise
|
||||
</label>
|
||||
<input
|
||||
id="check_in"
|
||||
name="check_in"
|
||||
type="date"
|
||||
required
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="check_out" className="text-sm font-medium">
|
||||
Abreise
|
||||
</label>
|
||||
<input
|
||||
id="check_out"
|
||||
name="check_out"
|
||||
type="date"
|
||||
required
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="adults" className="text-sm font-medium">
|
||||
Erwachsene
|
||||
</label>
|
||||
<input
|
||||
id="adults"
|
||||
name="adults"
|
||||
type="number"
|
||||
min={1}
|
||||
defaultValue={1}
|
||||
required
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="children" className="text-sm font-medium">
|
||||
Kinder
|
||||
</label>
|
||||
<input
|
||||
id="children"
|
||||
name="children"
|
||||
type="number"
|
||||
min={0}
|
||||
defaultValue={0}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Preis & Notizen */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Preis & Notizen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5 md:w-1/2">
|
||||
<label htmlFor="total_price" className="text-sm font-medium">
|
||||
Preis (€)
|
||||
</label>
|
||||
<input
|
||||
id="total_price"
|
||||
name="total_price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min={0}
|
||||
placeholder="0.00"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="notes" className="text-sm font-medium">
|
||||
Notizen
|
||||
</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
name="notes"
|
||||
rows={3}
|
||||
placeholder="Zusätzliche Informationen zur Buchung…"
|
||||
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Link href={`/home/${account}/bookings`}>
|
||||
<Button type="button" variant="outline">
|
||||
Abbrechen
|
||||
</Button>
|
||||
</Link>
|
||||
<Button type="submit">Buchung erstellen</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Neue Buchung" description="Buchung erstellen">
|
||||
<CreateBookingForm
|
||||
accountId={acct.id}
|
||||
account={account}
|
||||
rooms={(rooms ?? []).map((r: Record<string, unknown>) => ({
|
||||
id: String(r.id), roomNumber: String(r.room_number), name: String(r.name ?? ''), pricePerNight: Number(r.price_per_night ?? 0)
|
||||
}))}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,191 +1,18 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Textarea } from '@kit/ui/textarea';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { CreateCourseForm } from '@kit/course-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function NewCoursePage({ params }: PageProps) {
|
||||
export default async function NewCoursePage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neuer Kurs">
|
||||
<div className="flex w-full max-w-3xl flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href={`/home/${account}/courses`}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Zurück
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Neuer Kurs</h1>
|
||||
<p className="text-muted-foreground">Kurs anlegen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="flex flex-col gap-6">
|
||||
{/* Grunddaten */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Grunddaten</CardTitle>
|
||||
<CardDescription>
|
||||
Allgemeine Informationen zum Kurs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="courseNumber">Kursnummer</Label>
|
||||
<Input
|
||||
id="courseNumber"
|
||||
name="courseNumber"
|
||||
placeholder="z.B. K-2025-001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Kursname</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="z.B. Töpfern für Anfänger"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Beschreibung</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Kursbeschreibung…"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Zeitplan */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Zeitplan</CardTitle>
|
||||
<CardDescription>Beginn und Ende des Kurses</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="startDate">Beginn</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
name="startDate"
|
||||
type="date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="endDate">Ende</Label>
|
||||
<Input id="endDate" name="endDate" type="date" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Kapazität */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kapazität</CardTitle>
|
||||
<CardDescription>
|
||||
Teilnehmer und Gebühren
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="capacity">Max. Teilnehmer</Label>
|
||||
<Input
|
||||
id="capacity"
|
||||
name="capacity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="minParticipants">Min. Teilnehmer</Label>
|
||||
<Input
|
||||
id="minParticipants"
|
||||
name="minParticipants"
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="fee">Gebühr (€)</Label>
|
||||
<Input
|
||||
id="fee"
|
||||
name="fee"
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Zuordnung */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Zuordnung</CardTitle>
|
||||
<CardDescription>Status des Kurses</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
defaultValue="planned"
|
||||
>
|
||||
<option value="planned">Geplant</option>
|
||||
<option value="open">Offen</option>
|
||||
<option value="running">Laufend</option>
|
||||
<option value="completed">Abgeschlossen</option>
|
||||
<option value="cancelled">Abgesagt</option>
|
||||
</select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Link href={`/home/${account}/courses`}>
|
||||
<Button variant="outline" type="button">
|
||||
Abbrechen
|
||||
</Button>
|
||||
</Link>
|
||||
<Button type="submit">Kurs erstellen</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Neuer Kurs" description="Kurs anlegen">
|
||||
<CreateCourseForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
GraduationCap,
|
||||
Users,
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
BarChart3,
|
||||
} from 'lucide-react';
|
||||
import { GraduationCap, Users, Calendar, TrendingUp, BarChart3 } from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
@@ -13,6 +7,7 @@ import { createCourseManagementApi } from '@kit/course-management/api';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { StatsCard } from '~/components/stats-card';
|
||||
import { StatsBarChart, StatsPieChart } from '~/components/stats-charts';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -22,77 +17,53 @@ export default async function CourseStatisticsPage({ params }: PageProps) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createCourseManagementApi(client);
|
||||
const stats = await api.getStatistics(acct.id);
|
||||
|
||||
const statusChartData = [
|
||||
{ name: 'Aktiv', value: stats.openCourses },
|
||||
{ name: 'Abgeschlossen', value: stats.completedCourses },
|
||||
{ name: 'Gesamt', value: stats.totalCourses },
|
||||
];
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Kurs-Statistiken">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Statistiken</h1>
|
||||
<p className="text-muted-foreground">Übersicht über das Kursangebot</p>
|
||||
</div>
|
||||
|
||||
{/* Stat Cards */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatsCard
|
||||
title="Kurse gesamt"
|
||||
value={stats.totalCourses}
|
||||
icon={<GraduationCap className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Aktive Kurse"
|
||||
value={stats.openCourses}
|
||||
icon={<Calendar className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Teilnehmer gesamt"
|
||||
value={stats.totalParticipants}
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Abgeschlossen"
|
||||
value={stats.completedCourses}
|
||||
icon={<TrendingUp className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard title="Kurse gesamt" value={stats.totalCourses} icon={<GraduationCap className="h-5 w-5" />} />
|
||||
<StatsCard title="Aktive Kurse" value={stats.openCourses} icon={<Calendar className="h-5 w-5" />} />
|
||||
<StatsCard title="Teilnehmer" value={stats.totalParticipants} icon={<Users className="h-5 w-5" />} />
|
||||
<StatsCard title="Abgeschlossen" value={stats.completedCourses} icon={<TrendingUp className="h-5 w-5" />} />
|
||||
</div>
|
||||
|
||||
{/* Chart Placeholder */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
Kursauslastung
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-64 items-center justify-center rounded-md border border-dashed text-muted-foreground">
|
||||
Diagramm wird hier angezeigt
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
Kursauslastung
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StatsBarChart data={statusChartData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
Anmeldungen pro Monat
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-64 items-center justify-center rounded-md border border-dashed text-muted-foreground">
|
||||
Diagramm wird hier angezeigt
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
Verteilung
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StatsPieChart data={statusChartData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
);
|
||||
|
||||
@@ -1,301 +1,18 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarDays,
|
||||
Clock,
|
||||
MapPin,
|
||||
Phone,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
|
||||
import { CreateEventForm } from '@kit/event-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function NewEventPage({ params }: PageProps) {
|
||||
export default async function NewEventPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neue Veranstaltung">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href={`/home/${account}/events`}>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Neue Veranstaltung</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Veranstaltung oder Ferienprogramm anlegen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="flex flex-col gap-6">
|
||||
{/* Grunddaten */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CalendarDays className="h-5 w-5" />
|
||||
Grunddaten
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Name und Beschreibung der Veranstaltung
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="name" className="text-sm font-medium">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="z.B. Sommerfest 2025"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="description" className="text-sm font-medium">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
rows={4}
|
||||
placeholder="Beschreiben Sie die Veranstaltung…"
|
||||
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Datum & Ort */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
Datum & Ort
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Zeitraum und Veranstaltungsort
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="event_date" className="text-sm font-medium">
|
||||
Veranstaltungsdatum
|
||||
</label>
|
||||
<input
|
||||
id="event_date"
|
||||
name="event_date"
|
||||
type="date"
|
||||
required
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="event_time" className="text-sm font-medium">
|
||||
Uhrzeit
|
||||
</label>
|
||||
<input
|
||||
id="event_time"
|
||||
name="event_time"
|
||||
type="time"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="end_date" className="text-sm font-medium">
|
||||
Enddatum
|
||||
</label>
|
||||
<input
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
type="date"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="location" className="text-sm font-medium">
|
||||
Ort
|
||||
</label>
|
||||
<input
|
||||
id="location"
|
||||
name="location"
|
||||
type="text"
|
||||
placeholder="z.B. Gemeindehaus, Turnhalle"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Teilnehmer & Kosten */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5" />
|
||||
Teilnehmer & Kosten
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Kapazität, Alter und Teilnahmegebühr
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="capacity" className="text-sm font-medium">
|
||||
Kapazität
|
||||
</label>
|
||||
<input
|
||||
id="capacity"
|
||||
name="capacity"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Max. Teilnehmer"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="min_age" className="text-sm font-medium">
|
||||
Mindestalter
|
||||
</label>
|
||||
<input
|
||||
id="min_age"
|
||||
name="min_age"
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="z.B. 6"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="max_age" className="text-sm font-medium">
|
||||
Höchstalter
|
||||
</label>
|
||||
<input
|
||||
id="max_age"
|
||||
name="max_age"
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="z.B. 16"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="fee" className="text-sm font-medium">
|
||||
Gebühr (€)
|
||||
</label>
|
||||
<input
|
||||
id="fee"
|
||||
name="fee"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min={0}
|
||||
placeholder="0.00"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Kontakt */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Phone className="h-5 w-5" />
|
||||
Kontakt
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Ansprechpartner für die Veranstaltung
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="contact_name" className="text-sm font-medium">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="contact_name"
|
||||
name="contact_name"
|
||||
type="text"
|
||||
placeholder="Vorname Nachname"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="contact_email" className="text-sm font-medium">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
id="contact_email"
|
||||
name="contact_email"
|
||||
type="email"
|
||||
placeholder="name@example.de"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="contact_phone" className="text-sm font-medium">
|
||||
Telefon
|
||||
</label>
|
||||
<input
|
||||
id="contact_phone"
|
||||
name="contact_phone"
|
||||
type="tel"
|
||||
placeholder="+49 …"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Link href={`/home/${account}/events`}>
|
||||
<Button type="button" variant="outline">
|
||||
Abbrechen
|
||||
</Button>
|
||||
</Link>
|
||||
<Button type="submit">Veranstaltung erstellen</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Neue Veranstaltung" description="Veranstaltung oder Ferienprogramm anlegen">
|
||||
<CreateEventForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,212 +1,18 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Textarea } from '@kit/ui/textarea';
|
||||
|
||||
import { CreateInvoiceForm } from '@kit/finance/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function NewInvoicePage({ params }: PageProps) {
|
||||
export default async function NewInvoicePage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neue Rechnung">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Back link */}
|
||||
<div>
|
||||
<Link
|
||||
href={`/home/${account}/finance/invoices`}
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
|
||||
>
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
Zurück zu Rechnungen
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="max-w-3xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Neue Rechnung</CardTitle>
|
||||
<CardDescription>
|
||||
Erstellen Sie eine neue Rechnung mit Positionen.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-6">
|
||||
{/* Basic Info */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="invoiceNumber">Rechnungsnummer</Label>
|
||||
<Input
|
||||
id="invoiceNumber"
|
||||
name="invoiceNumber"
|
||||
placeholder="RE-2026-001"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="recipientName">Empfänger</Label>
|
||||
<Input
|
||||
id="recipientName"
|
||||
name="recipientName"
|
||||
placeholder="Max Mustermann"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recipient Address */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="recipientAddress">Empfängeradresse</Label>
|
||||
<Textarea
|
||||
id="recipientAddress"
|
||||
name="recipientAddress"
|
||||
placeholder="Musterstraße 1 12345 Musterstadt"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dates + Tax */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="issueDate">Rechnungsdatum</Label>
|
||||
<Input
|
||||
id="issueDate"
|
||||
name="issueDate"
|
||||
type="date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="dueDate">Fälligkeitsdatum</Label>
|
||||
<Input
|
||||
id="dueDate"
|
||||
name="dueDate"
|
||||
type="date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="taxRate">Steuersatz (%)</Label>
|
||||
<Input
|
||||
id="taxRate"
|
||||
name="taxRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
defaultValue="19"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Line Items */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Positionen</Label>
|
||||
<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">
|
||||
Beschreibung
|
||||
</th>
|
||||
<th className="p-3 text-right font-medium">Menge</th>
|
||||
<th className="p-3 text-right font-medium">
|
||||
Einzelpreis (€)
|
||||
</th>
|
||||
<th className="p-3 text-right font-medium">Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[0, 1, 2].map((idx) => (
|
||||
<tr key={idx} className="border-b">
|
||||
<td className="p-2">
|
||||
<Input
|
||||
name={`items[${idx}].description`}
|
||||
placeholder="Leistungsbeschreibung"
|
||||
className="border-0 shadow-none"
|
||||
/>
|
||||
</td>
|
||||
<td className="w-24 p-2">
|
||||
<Input
|
||||
name={`items[${idx}].quantity`}
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
defaultValue="1"
|
||||
className="border-0 text-right shadow-none"
|
||||
/>
|
||||
</td>
|
||||
<td className="w-32 p-2">
|
||||
<Input
|
||||
name={`items[${idx}].unitPrice`}
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
defaultValue="0.00"
|
||||
className="border-0 text-right shadow-none"
|
||||
/>
|
||||
</td>
|
||||
<td className="w-32 p-3 text-right text-muted-foreground">
|
||||
0,00 €
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="ml-auto flex w-64 flex-col gap-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Zwischensumme</span>
|
||||
<span>0,00 €</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">MwSt. (19%)</span>
|
||||
<span>0,00 €</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t pt-1 font-semibold">
|
||||
<span>Gesamt</span>
|
||||
<span>0,00 €</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex justify-between">
|
||||
<Link href={`/home/${account}/finance/invoices`}>
|
||||
<Button variant="outline">Abbrechen</Button>
|
||||
</Link>
|
||||
<Button type="submit">Rechnung erstellen</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Neue Rechnung" description="Rechnung mit Positionen erstellen">
|
||||
<CreateInvoiceForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
|
||||
import { CreateSepaBatchForm } from '@kit/finance/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function NewSepaPage({ params }: PageProps) {
|
||||
export default async function NewSepaBatchPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
@@ -34,84 +20,8 @@ export default async function NewSepaPage({ params }: PageProps) {
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neuer SEPA-Einzug">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Back link */}
|
||||
<div>
|
||||
<Link
|
||||
href={`/home/${account}/finance/sepa`}
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
|
||||
>
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
Zurück zu SEPA-Lastschriften
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Neuer SEPA-Einzug</CardTitle>
|
||||
<CardDescription>
|
||||
Erstellen Sie einen neuen Lastschrifteinzug oder eine Überweisung.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-5">
|
||||
{/* Typ */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="batchType">Typ</Label>
|
||||
<select
|
||||
id="batchType"
|
||||
name="batchType"
|
||||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
defaultValue="direct_debit"
|
||||
>
|
||||
<option value="direct_debit">Lastschrift</option>
|
||||
<option value="credit_transfer">Überweisung</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Beschreibung */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="description">Beschreibung</Label>
|
||||
<Input
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="z.B. Mitgliedsbeiträge Q1 2026"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Ausführungsdatum */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="executionDate">Ausführungsdatum</Label>
|
||||
<Input
|
||||
id="executionDate"
|
||||
name="executionDate"
|
||||
type="date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* PAIN Format Info */}
|
||||
<div className="rounded-md bg-muted/50 p-4 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<strong>Hinweis:</strong> Nach dem Erstellen können Sie
|
||||
einzelne Positionen hinzufügen und anschließend die
|
||||
SEPA-XML-Datei generieren.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex justify-between">
|
||||
<Link href={`/home/${account}/finance/sepa`}>
|
||||
<Button variant="outline">Abbrechen</Button>
|
||||
</Link>
|
||||
<Button type="submit">Einzug erstellen</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Neuer SEPA-Einzug" description="SEPA-Lastschrifteinzug erstellen">
|
||||
<CreateSepaBatchForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { EditMemberForm } from '@kit/member-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string; memberId: string }>;
|
||||
}
|
||||
|
||||
export default async function EditMemberPage({ params }: Props) {
|
||||
const { account, memberId } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const member = await api.getMember(memberId);
|
||||
if (!member) return <div>Mitglied nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title={`${String(member.first_name)} ${String(member.last_name)} bearbeiten`}>
|
||||
<EditMemberForm member={member} account={account} accountId={acct.id} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -1,173 +1,25 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { User, Mail, Phone, MapPin, CreditCard, Pencil, Ban } 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 { createMemberManagementApi } from '@kit/member-management/api';
|
||||
|
||||
import { MemberDetailView } from '@kit/member-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
interface Props {
|
||||
params: Promise<{ account: string; memberId: string }>;
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
active: 'Aktiv',
|
||||
inactive: 'Inaktiv',
|
||||
pending: 'Ausstehend',
|
||||
cancelled: 'Gekündigt',
|
||||
};
|
||||
|
||||
const STATUS_VARIANT: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
active: 'default',
|
||||
inactive: 'secondary',
|
||||
pending: 'outline',
|
||||
cancelled: 'destructive',
|
||||
};
|
||||
|
||||
function DetailRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<span className="text-sm font-medium">{value || '—'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function MemberDetailPage({ params }: PageProps) {
|
||||
export default async function MemberDetailPage({ params }: Props) {
|
||||
const { account, memberId } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
|
||||
const member = await api.getMember(memberId);
|
||||
|
||||
if (!member) return <div>Mitglied nicht gefunden</div>;
|
||||
|
||||
const m = member as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title={`${String(m.first_name)} ${String(m.last_name)}`}>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">
|
||||
{String(m.first_name)} {String(m.last_name)}
|
||||
</h1>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Badge variant={STATUS_VARIANT[String(m.status)] ?? 'secondary'}>
|
||||
{STATUS_LABEL[String(m.status)] ?? String(m.status)}
|
||||
</Badge>
|
||||
{m.member_number ? (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Nr. {String(m.member_number)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
<Ban className="mr-2 h-4 w-4" />
|
||||
Kündigen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{/* Persönliche Daten */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-4 w-4" />
|
||||
Persönliche Daten
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4">
|
||||
<DetailRow label="Vorname" value={String(m.first_name ?? '')} />
|
||||
<DetailRow label="Nachname" value={String(m.last_name ?? '')} />
|
||||
<DetailRow
|
||||
label="Geburtsdatum"
|
||||
value={m.date_of_birth ? new Date(String(m.date_of_birth)).toLocaleDateString('de-DE') : ''}
|
||||
/>
|
||||
<DetailRow label="Geschlecht" value={String(m.gender ?? '')} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Kontakt */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mail className="h-4 w-4" />
|
||||
Kontakt
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4">
|
||||
<DetailRow label="E-Mail" value={String(m.email ?? '')} />
|
||||
<DetailRow label="Telefon" value={String(m.phone ?? '')} />
|
||||
<DetailRow label="Mobil" value={String(m.mobile ?? '')} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Adresse */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4" />
|
||||
Adresse
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4">
|
||||
<DetailRow label="Straße" value={`${String(m.street ?? '')} ${String(m.house_number ?? '')}`} />
|
||||
<DetailRow label="PLZ / Ort" value={`${String(m.postal_code ?? '')} ${String(m.city ?? '')}`} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Mitgliedschaft */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-4 w-4" />
|
||||
Mitgliedschaft
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4">
|
||||
<DetailRow label="Mitgliedsnr." value={String(m.member_number ?? '')} />
|
||||
<DetailRow label="Status" value={STATUS_LABEL[String(m.status)] ?? String(m.status ?? '')} />
|
||||
<DetailRow
|
||||
label="Eintrittsdatum"
|
||||
value={m.entry_date ? new Date(String(m.entry_date)).toLocaleDateString('de-DE') : ''}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Austrittsdatum"
|
||||
value={m.exit_date ? new Date(String(m.exit_date)).toLocaleDateString('de-DE') : ''}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEPA */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CreditCard className="h-4 w-4" />
|
||||
SEPA-Bankverbindung
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4 sm:grid-cols-3">
|
||||
<DetailRow label="IBAN" value={String(m.iban ?? '')} />
|
||||
<DetailRow label="BIC" value={String(m.bic ?? '')} />
|
||||
<DetailRow label="Kontoinhaber" value={String(m.account_holder ?? '')} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<CmsPageShell account={account} title={`${String(member.first_name)} ${String(member.last_name)}`}>
|
||||
<MemberDetailView member={member} account={account} accountId={acct.id} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,117 +1,24 @@
|
||||
import { UserCheck, UserX, FileText } 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 { createMemberManagementApi } from '@kit/member-management/api';
|
||||
|
||||
import { ApplicationWorkflow } from '@kit/member-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
|
||||
interface PageProps {
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
const STATUS_VARIANT: Record<string, 'secondary' | 'default' | 'info' | 'destructive'> = {
|
||||
pending: 'secondary',
|
||||
approved: 'default',
|
||||
rejected: 'destructive',
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
pending: 'Ausstehend',
|
||||
approved: 'Genehmigt',
|
||||
rejected: 'Abgelehnt',
|
||||
};
|
||||
|
||||
export default async function ApplicationsPage({ params }: PageProps) {
|
||||
export default async function ApplicationsPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const applications = await api.listApplications(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Anträge">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Mitgliedsanträge</h1>
|
||||
<p className="text-muted-foreground">Eingehende Anträge prüfen und bearbeiten</p>
|
||||
</div>
|
||||
|
||||
{applications.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<FileText className="h-8 w-8" />}
|
||||
title="Keine Anträge"
|
||||
description="Es liegen derzeit keine Mitgliedsanträge vor."
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Alle Anträge ({applications.length})</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">Name</th>
|
||||
<th className="p-3 text-left font-medium">E-Mail</th>
|
||||
<th className="p-3 text-left font-medium">Datum</th>
|
||||
<th className="p-3 text-left font-medium">Status</th>
|
||||
<th className="p-3 text-right font-medium">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{applications.map((app: Record<string, unknown>) => (
|
||||
<tr key={String(app.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3 font-medium">
|
||||
{String(app.last_name ?? '')}, {String(app.first_name ?? '')}
|
||||
</td>
|
||||
<td className="p-3">{String(app.email ?? '—')}</td>
|
||||
<td className="p-3">
|
||||
{app.created_at
|
||||
? new Date(String(app.created_at)).toLocaleDateString('de-DE')
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<Badge variant={STATUS_VARIANT[String(app.status)] ?? 'secondary'}>
|
||||
{STATUS_LABEL[String(app.status)] ?? String(app.status)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
{String(app.status) === 'pending' && (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button size="sm" variant="default">
|
||||
<UserCheck className="mr-1 h-3 w-3" />
|
||||
Genehmigen
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive">
|
||||
<UserX className="mr-1 h-3 w-3" />
|
||||
Ablehnen
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Aufnahmeanträge" description="Mitgliedsanträge bearbeiten">
|
||||
<ApplicationWorkflow applications={applications} accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { CreditCard, Download } from 'lucide-react';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function MemberCardsPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const result = await api.listMembers(acct.id, { status: 'active', pageSize: 100 });
|
||||
const members = result.data;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Mitgliedsausweise" description="Ausweise erstellen und verwalten">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">{members.length} aktive Mitglieder</p>
|
||||
<Button disabled>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Alle Ausweise generieren (PDF)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{members.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<CreditCard className="h-8 w-8" />}
|
||||
title="Keine aktiven Mitglieder"
|
||||
description="Erstellen Sie zuerst Mitglieder, um Ausweise zu generieren."
|
||||
actionLabel="Mitglieder verwalten"
|
||||
actionHref={`/home/${account}/members-cms`}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{members.map((m: Record<string, unknown>) => (
|
||||
<Card key={String(m.id)}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-semibold">{String(m.last_name)}, {String(m.first_name)}</p>
|
||||
<p className="text-xs text-muted-foreground">Nr. {String(m.member_number ?? '—')}</p>
|
||||
</div>
|
||||
<Badge variant="default">Aktiv</Badge>
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Button size="sm" variant="outline" disabled>
|
||||
<CreditCard className="mr-1 h-3 w-3" />
|
||||
Ausweis
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>PDF-Generierung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Die PDF-Generierung erfordert die Installation von <code>@react-pdf/renderer</code>.
|
||||
Nach der Installation können Mitgliedsausweise einzeln oder als Stapel erstellt werden.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
import { Users } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function DepartmentsPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const departments = await api.listDepartments(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Abteilungen" description="Sparten und Abteilungen verwalten">
|
||||
{departments.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Users className="h-8 w-8" />}
|
||||
title="Keine Abteilungen vorhanden"
|
||||
description="Erstellen Sie Ihre erste Abteilung."
|
||||
actionLabel="Neue Abteilung"
|
||||
/>
|
||||
) : (
|
||||
<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">Name</th>
|
||||
<th className="p-3 text-left font-medium">Beschreibung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{departments.map((dept: Record<string, unknown>) => (
|
||||
<tr key={String(dept.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3 font-medium">{String(dept.name)}</td>
|
||||
<td className="p-3 text-muted-foreground">{String(dept.description ?? '—')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +1,24 @@
|
||||
import { Euro, Plus } 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 { createMemberManagementApi } from '@kit/member-management/api';
|
||||
|
||||
import { DuesCategoryManager } from '@kit/member-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
|
||||
interface PageProps {
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function DuesPage({ params }: PageProps) {
|
||||
export default async function DuesPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const categories = await api.listDuesCategories(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Beitragskategorien">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Beitragskategorien</h1>
|
||||
<p className="text-muted-foreground">Beiträge und Gebühren verwalten</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Neue Kategorie
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{categories.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Euro className="h-8 w-8" />}
|
||||
title="Keine Beitragskategorien"
|
||||
description="Legen Sie Ihre erste Beitragskategorie an."
|
||||
actionLabel="Neue Kategorie"
|
||||
/>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Alle Kategorien ({categories.length})</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">Name</th>
|
||||
<th className="p-3 text-left font-medium">Beschreibung</th>
|
||||
<th className="p-3 text-right font-medium">Betrag (€)</th>
|
||||
<th className="p-3 text-left font-medium">Intervall</th>
|
||||
<th className="p-3 text-center font-medium">Standard</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{categories.map((cat: Record<string, unknown>) => (
|
||||
<tr key={String(cat.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3 font-medium">{String(cat.name)}</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
{String(cat.description ?? '—')}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
{cat.amount != null ? `${Number(cat.amount).toFixed(2)}` : '—'}
|
||||
</td>
|
||||
<td className="p-3">{String(cat.interval ?? '—')}</td>
|
||||
<td className="p-3 text-center">
|
||||
{cat.is_default ? '✓' : '✗'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Beitragskategorien" description="Mitgliedsbeiträge verwalten">
|
||||
<DuesCategoryManager categories={categories} accountId={acct.id} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { MemberImportWizard } from '@kit/member-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function MemberImportPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Mitglieder importieren" description="CSV-Datei importieren">
|
||||
<MemberImportWizard accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -1,183 +1,28 @@
|
||||
import { UserPlus } from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { CreateMemberForm } from '@kit/member-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function NewMemberPage({ params }: PageProps) {
|
||||
export default async function NewMemberPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
const duesCategories = await api.listDuesCategories(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neues Mitglied">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Neues Mitglied</h1>
|
||||
<p className="text-muted-foreground">Mitglied manuell anlegen</p>
|
||||
</div>
|
||||
|
||||
<form className="flex flex-col gap-6">
|
||||
{/* Persönliche Daten */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Persönliche Daten</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">Vorname</Label>
|
||||
<Input id="firstName" name="firstName" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Nachname</Label>
|
||||
<Input id="lastName" name="lastName" required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dateOfBirth">Geburtsdatum</Label>
|
||||
<Input id="dateOfBirth" name="dateOfBirth" type="date" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gender">Geschlecht</Label>
|
||||
<select
|
||||
id="gender"
|
||||
name="gender"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— Bitte wählen —</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
</select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Kontakt */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kontakt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-Mail</Label>
|
||||
<Input id="email" name="email" type="email" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Telefon</Label>
|
||||
<Input id="phone" name="phone" type="tel" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mobile">Mobil</Label>
|
||||
<Input id="mobile" name="mobile" type="tel" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Adresse */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Adresse</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2 sm:col-span-1">
|
||||
<Label htmlFor="street">Straße</Label>
|
||||
<Input id="street" name="street" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="houseNumber">Hausnummer</Label>
|
||||
<Input id="houseNumber" name="houseNumber" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postalCode">PLZ</Label>
|
||||
<Input id="postalCode" name="postalCode" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">Ort</Label>
|
||||
<Input id="city" name="city" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Mitgliedschaft */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Mitgliedschaft</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberNumber">Mitgliedsnr.</Label>
|
||||
<Input id="memberNumber" name="memberNumber" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="pending">Ausstehend</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="entryDate">Eintrittsdatum</Label>
|
||||
<Input id="entryDate" name="entryDate" type="date" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEPA */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>SEPA-Bankverbindung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="iban">IBAN</Label>
|
||||
<Input id="iban" name="iban" placeholder="DE89 3704 0044 0532 0130 00" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bic">BIC</Label>
|
||||
<Input id="bic" name="bic" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="accountHolder">Kontoinhaber</Label>
|
||||
<Input id="accountHolder" name="accountHolder" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notizen */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notizen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<textarea
|
||||
name="notes"
|
||||
rows={4}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
placeholder="Zusätzliche Anmerkungen…"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" size="lg">
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Mitglied erstellen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Neues Mitglied" description="Mitglied manuell anlegen">
|
||||
<CreateMemberForm
|
||||
accountId={acct.id}
|
||||
account={account}
|
||||
duesCategories={(duesCategories ?? []).map((c: Record<string, unknown>) => ({
|
||||
id: String(c.id), name: String(c.name), amount: Number(c.amount ?? 0)
|
||||
}))}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,42 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
import { MembersDataTable } from '@kit/member-management/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface MembersPageProps {
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}
|
||||
|
||||
export default async function MembersPage({ params, searchParams }: MembersPageProps) {
|
||||
export default async function MembersPage({ params, searchParams }: Props) {
|
||||
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 <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createMemberManagementApi(client);
|
||||
|
||||
const { data: accountData } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
if (!accountData) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const page = Number(search.page) || 1;
|
||||
const result = await api.listMembers(accountData.id, {
|
||||
const result = await api.listMembers(acct.id, {
|
||||
search: search.q as string,
|
||||
status: search.status as string,
|
||||
page,
|
||||
pageSize: 25,
|
||||
});
|
||||
const duesCategories = await api.listDuesCategories(acct.id);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Mitglieder</h1>
|
||||
<p className="text-muted-foreground">{result.total} Mitglieder</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Nr.</th>
|
||||
<th className="p-3 text-left">Name</th>
|
||||
<th className="p-3 text-left">E-Mail</th>
|
||||
<th className="p-3 text-left">Ort</th>
|
||||
<th className="p-3 text-left">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.data.map((member: Record<string, unknown>) => (
|
||||
<tr key={String(member.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3">{String(member.member_number ?? '—')}</td>
|
||||
<td className="p-3 font-medium">{String(member.last_name)}, {String(member.first_name)}</td>
|
||||
<td className="p-3">{String(member.email ?? '—')}</td>
|
||||
<td className="p-3">{String(member.postal_code ?? '')} {String(member.city ?? '')}</td>
|
||||
<td className="p-3">{String(member.status)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Mitglieder" description={`${result.total} Mitglieder`}>
|
||||
<MembersDataTable
|
||||
data={result.data}
|
||||
total={result.total}
|
||||
page={page}
|
||||
pageSize={25}
|
||||
account={account}
|
||||
duesCategories={(duesCategories ?? []).map((c: Record<string, unknown>) => ({
|
||||
id: String(c.id), name: String(c.name),
|
||||
}))}
|
||||
/>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createMemberManagementApi } from '@kit/member-management/api';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { StatsCard } from '~/components/stats-card';
|
||||
import { StatsBarChart, StatsPieChart } from '~/components/stats-charts';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -27,65 +28,48 @@ export default async function MemberStatisticsPage({ params }: PageProps) {
|
||||
const api = createMemberManagementApi(client);
|
||||
const stats = await api.getMemberStatistics(acct.id);
|
||||
|
||||
const statusChartData = [
|
||||
{ name: 'Aktiv', value: stats.active ?? 0 },
|
||||
{ name: 'Inaktiv', value: stats.inactive ?? 0 },
|
||||
{ name: 'Ausstehend', value: stats.pending ?? 0 },
|
||||
{ name: 'Ausgetreten', value: stats.resigned ?? 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Mitglieder-Statistiken">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Mitglieder-Statistiken</h1>
|
||||
<p className="text-muted-foreground">Übersicht über Ihre Mitglieder</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatsCard
|
||||
title="Gesamt"
|
||||
value={stats.total ?? 0}
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Aktiv"
|
||||
value={stats.active ?? 0}
|
||||
icon={<UserCheck className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Inaktiv"
|
||||
value={stats.inactive ?? 0}
|
||||
icon={<UserMinus className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Ausstehend"
|
||||
value={stats.pending ?? 0}
|
||||
icon={<Clock className="h-5 w-5" />}
|
||||
/>
|
||||
<StatsCard title="Gesamt" value={stats.total ?? 0} icon={<Users className="h-5 w-5" />} />
|
||||
<StatsCard title="Aktiv" value={stats.active ?? 0} icon={<UserCheck className="h-5 w-5" />} />
|
||||
<StatsCard title="Inaktiv" value={stats.inactive ?? 0} icon={<UserMinus className="h-5 w-5" />} />
|
||||
<StatsCard title="Ausstehend" value={stats.pending ?? 0} icon={<Clock className="h-5 w-5" />} />
|
||||
</div>
|
||||
|
||||
{/* Chart Placeholders */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
Mitgliederentwicklung
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-64 items-center justify-center rounded-md border border-dashed text-muted-foreground">
|
||||
Diagramm wird hier angezeigt
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
Mitglieder nach Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StatsBarChart data={statusChartData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
Eintritte / Austritte pro Monat
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex h-64 items-center justify-center rounded-md border border-dashed text-muted-foreground">
|
||||
Diagramm wird hier angezeigt
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
Verteilung
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StatsPieChart data={statusChartData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
);
|
||||
|
||||
@@ -1,125 +1,18 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Textarea } from '@kit/ui/textarea';
|
||||
|
||||
import { CreateNewsletterForm } from '@kit/newsletter/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function NewNewsletterPage({ params }: PageProps) {
|
||||
export default async function NewNewsletterPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: acct } = await client
|
||||
.from('accounts')
|
||||
.select('id')
|
||||
.eq('slug', account)
|
||||
.single();
|
||||
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neuer Newsletter">
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Back link */}
|
||||
<div>
|
||||
<Link
|
||||
href={`/home/${account}/newsletter`}
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center text-sm"
|
||||
>
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
Zurück zu Newsletter
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>Neuer Newsletter</CardTitle>
|
||||
<CardDescription>
|
||||
Erstellen Sie eine neue Newsletter-Kampagne.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-5">
|
||||
{/* Betreff */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="subject">Betreff</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
name="subject"
|
||||
placeholder="z.B. Monatliche Vereinsnachrichten März 2026"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Body HTML */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="bodyHtml">Inhalt (HTML)</Label>
|
||||
<Textarea
|
||||
id="bodyHtml"
|
||||
name="bodyHtml"
|
||||
placeholder="<h1>Hallo {{first_name}},</h1> <p>Neuigkeiten aus dem Verein...</p>"
|
||||
rows={10}
|
||||
className="font-mono text-sm"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Verwenden Sie {'{{first_name}}'}, {'{{name}}'} und{' '}
|
||||
{'{{email}}'} als Platzhalter für die Personalisierung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Empfänger Info */}
|
||||
<div className="rounded-md bg-muted/50 p-4 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<strong>Empfänger-Auswahl:</strong> Nach dem Erstellen können
|
||||
Sie die Empfänger aus Ihrer Mitgliederliste auswählen. Es
|
||||
werden nur Mitglieder mit hinterlegter E-Mail-Adresse
|
||||
berücksichtigt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Vorlage Auswahl */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="templateId">
|
||||
Vorlage (optional)
|
||||
</Label>
|
||||
<select
|
||||
id="templateId"
|
||||
name="templateId"
|
||||
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<option value="">Keine Vorlage</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex justify-between">
|
||||
<Link href={`/home/${account}/newsletter`}>
|
||||
<Button variant="outline">Abbrechen</Button>
|
||||
</Link>
|
||||
<Button type="submit">Newsletter erstellen</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<CmsPageShell account={account} title="Neuer Newsletter" description="Newsletter-Kampagne erstellen">
|
||||
<CreateNewsletterForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createSiteBuilderApi } from '@kit/site-builder/api';
|
||||
import { SiteEditor } from '@kit/site-builder/components';
|
||||
|
||||
interface Props { params: Promise<{ account: string; pageId: string }> }
|
||||
|
||||
export default async function EditPageRoute({ params }: Props) {
|
||||
const { account, pageId } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createSiteBuilderApi(client);
|
||||
const page = await api.getPage(pageId);
|
||||
if (!page) return <div>Seite nicht gefunden</div>;
|
||||
|
||||
return <SiteEditor pageId={pageId} accountId={acct.id} initialData={(page.puck_data ?? {}) as Record<string, unknown>} />;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { CreatePageForm } from '@kit/site-builder/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ account: string }>;
|
||||
}
|
||||
|
||||
export default async function NewSitePage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neue Seite" description="Seite für Ihre Vereinswebsite erstellen">
|
||||
<CreatePageForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
95
apps/web/app/[locale]/home/[account]/site-builder/page.tsx
Normal file
95
apps/web/app/[locale]/home/[account]/site-builder/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createSiteBuilderApi } from '@kit/site-builder/api';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Plus, Globe, FileText, Settings, ExternalLink } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function SiteBuilderDashboard({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createSiteBuilderApi(client);
|
||||
const pages = await api.listPages(acct.id);
|
||||
const settings = await api.getSiteSettings(acct.id);
|
||||
const posts = await api.listPosts(acct.id);
|
||||
|
||||
const publishedCount = pages.filter((p: any) => p.is_published).length;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Website-Baukasten" description="Ihre Vereinswebseite verwalten">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/home/${account}/site-builder/settings`}>
|
||||
<Button variant="outline" size="sm"><Settings className="mr-2 h-4 w-4" />Einstellungen</Button>
|
||||
</Link>
|
||||
<Link href={`/home/${account}/site-builder/posts`}>
|
||||
<Button variant="outline" size="sm"><FileText className="mr-2 h-4 w-4" />Beiträge ({posts.length})</Button>
|
||||
</Link>
|
||||
{settings?.is_public && (
|
||||
<a href={`/club/${account}`} target="_blank" rel="noopener">
|
||||
<Button variant="outline" size="sm"><ExternalLink className="mr-2 h-4 w-4" />Website ansehen</Button>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Link href={`/home/${account}/site-builder/new`}>
|
||||
<Button><Plus className="mr-2 h-4 w-4" />Neue Seite</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<Card><CardContent className="p-6"><p className="text-sm text-muted-foreground">Seiten</p><p className="text-2xl font-bold">{pages.length}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-6"><p className="text-sm text-muted-foreground">Veröffentlicht</p><p className="text-2xl font-bold">{publishedCount}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-6"><p className="text-sm text-muted-foreground">Status</p><p className="text-2xl font-bold">{settings?.is_public ? '🟢 Online' : '🔴 Offline'}</p></CardContent></Card>
|
||||
</div>
|
||||
|
||||
{pages.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Globe className="h-8 w-8" />}
|
||||
title="Noch keine Seiten"
|
||||
description="Erstellen Sie Ihre erste Seite mit dem visuellen Editor."
|
||||
actionLabel="Erste Seite erstellen"
|
||||
actionHref={`/home/${account}/site-builder/new`}
|
||||
/>
|
||||
) : (
|
||||
<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">Titel</th>
|
||||
<th className="p-3 text-left font-medium">URL</th>
|
||||
<th className="p-3 text-left font-medium">Status</th>
|
||||
<th className="p-3 text-left font-medium">Startseite</th>
|
||||
<th className="p-3 text-left font-medium">Aktualisiert</th>
|
||||
<th className="p-3 text-left font-medium">Aktionen</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{pages.map((page: Record<string, unknown>) => (
|
||||
<tr key={String(page.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3 font-medium">{String(page.title)}</td>
|
||||
<td className="p-3 text-muted-foreground font-mono text-xs">/{String(page.slug)}</td>
|
||||
<td className="p-3"><Badge variant={page.is_published ? 'default' : 'secondary'}>{page.is_published ? 'Veröffentlicht' : 'Entwurf'}</Badge></td>
|
||||
<td className="p-3">{page.is_homepage ? '⭐' : '—'}</td>
|
||||
<td className="p-3 text-xs text-muted-foreground">{page.updated_at ? new Date(String(page.updated_at)).toLocaleDateString('de-DE') : '—'}</td>
|
||||
<td className="p-3">
|
||||
<Link href={`/home/${account}/site-builder/${String(page.id)}/edit`}>
|
||||
<Button size="sm" variant="outline">Bearbeiten</Button>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { CreatePostForm } from '@kit/site-builder/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function NewPostPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Neuer Beitrag" description="Beitrag erstellen">
|
||||
<CreatePostForm accountId={acct.id} account={account} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createSiteBuilderApi } from '@kit/site-builder/api';
|
||||
import { Card, CardContent } from '@kit/ui/card';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function PostsManagerPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createSiteBuilderApi(client);
|
||||
const posts = await api.listPosts(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Beiträge" description="Neuigkeiten und Artikel verwalten">
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-end">
|
||||
<Button><Plus className="mr-2 h-4 w-4" />Neuer Beitrag</Button>
|
||||
</div>
|
||||
{posts.length === 0 ? (
|
||||
<EmptyState title="Keine Beiträge" description="Erstellen Sie Ihren ersten Beitrag." actionLabel="Beitrag erstellen" />
|
||||
) : (
|
||||
<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">Titel</th>
|
||||
<th className="p-3 text-left">Status</th>
|
||||
<th className="p-3 text-left">Erstellt</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{posts.map((post: Record<string, unknown>) => (
|
||||
<tr key={String(post.id)} className="border-b hover:bg-muted/30">
|
||||
<td className="p-3 font-medium">{String(post.title)}</td>
|
||||
<td className="p-3"><Badge variant={post.status === 'published' ? 'default' : 'secondary'}>{String(post.status)}</Badge></td>
|
||||
<td className="p-3 text-xs text-muted-foreground">{post.created_at ? new Date(String(post.created_at)).toLocaleDateString('de-DE') : '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createSiteBuilderApi } from '@kit/site-builder/api';
|
||||
import { SiteSettingsForm } from '@kit/site-builder/components';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface Props { params: Promise<{ account: string }> }
|
||||
|
||||
export default async function SiteSettingsPage({ params }: Props) {
|
||||
const { account } = await params;
|
||||
const client = getSupabaseServerClient();
|
||||
const { data: acct } = await client.from('accounts').select('id').eq('slug', account).single();
|
||||
if (!acct) return <div>Konto nicht gefunden</div>;
|
||||
|
||||
const api = createSiteBuilderApi(client);
|
||||
const settings = await api.getSiteSettings(acct.id);
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Website-Einstellungen" description="Design und Kontaktdaten">
|
||||
<SiteSettingsForm accountId={acct.id} account={account} settings={settings} />
|
||||
</CmsPageShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user