Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -73,7 +73,7 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
||||
setSelectedType(e.target.value);
|
||||
setResult(null);
|
||||
}}
|
||||
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"
|
||||
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:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
>
|
||||
<option value="member-card">Mitgliedsausweis</option>
|
||||
<option value="invoice">Rechnung</option>
|
||||
@@ -92,7 +92,8 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
||||
<p className="font-medium">Demnächst verfügbar</p>
|
||||
<p className="mt-1 text-amber-700 dark:text-amber-300">
|
||||
Die Generierung von “{DOCUMENT_LABELS[selectedType]}”
|
||||
befindet sich noch in Entwicklung und wird in Kürze verfügbar sein.
|
||||
befindet sich noch in Entwicklung und wird in Kürze verfügbar
|
||||
sein.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +119,7 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
||||
id="format"
|
||||
name="format"
|
||||
disabled={isPending}
|
||||
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 disabled:opacity-50"
|
||||
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:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:opacity-50"
|
||||
>
|
||||
<option value="A4">A4</option>
|
||||
<option value="A5">A5</option>
|
||||
@@ -131,7 +132,7 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
||||
id="orientation"
|
||||
name="orientation"
|
||||
disabled={isPending}
|
||||
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 disabled:opacity-50"
|
||||
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:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:opacity-50"
|
||||
>
|
||||
<option value="portrait">Hochformat</option>
|
||||
<option value="landscape">Querformat</option>
|
||||
@@ -140,7 +141,7 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Hint */}
|
||||
<div className="text-muted-foreground rounded-md bg-muted/50 p-4 text-sm">
|
||||
<div className="text-muted-foreground bg-muted/50 rounded-md p-4 text-sm">
|
||||
<p>
|
||||
<strong>Hinweis:</strong>{' '}
|
||||
{selectedType === 'member-card'
|
||||
@@ -211,11 +212,7 @@ export function GenerateDocumentForm({ accountSlug, initialType }: Props) {
|
||||
* Trigger a browser download from a base64 string.
|
||||
* Uses an anchor element with the download attribute set to the full filename.
|
||||
*/
|
||||
function downloadFile(
|
||||
base64Data: string,
|
||||
mimeType: string,
|
||||
fileName: string,
|
||||
) {
|
||||
function downloadFile(base64Data: string, mimeType: string, fileName: string) {
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { createDocumentGeneratorApi } from '@kit/document-generator/api';
|
||||
import { formatDate } from '@kit/shared/dates';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
export type GenerateDocumentInput = {
|
||||
accountSlug: string;
|
||||
@@ -55,7 +57,11 @@ export async function generateDocumentAction(
|
||||
return { success: false, error: 'Unbekannter Dokumenttyp.' };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Document generation error:', err);
|
||||
const logger = await getLogger();
|
||||
logger.error(
|
||||
{ error: err, context: 'document-generation' },
|
||||
'Document generation error',
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : 'Unbekannter Fehler.',
|
||||
@@ -73,8 +79,7 @@ const LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
function fmtDate(d: string | null): string {
|
||||
if (!d) return '–';
|
||||
try { return new Date(d).toLocaleDateString('de-DE'); } catch { return d; }
|
||||
return formatDate(d);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -88,16 +93,28 @@ async function generateMemberCards(
|
||||
): Promise<GenerateDocumentResult> {
|
||||
const { data: members, error } = await client
|
||||
.from('members')
|
||||
.select('id, member_number, first_name, last_name, entry_date, status, date_of_birth, street, house_number, postal_code, city, email')
|
||||
.select(
|
||||
'id, member_number, first_name, last_name, entry_date, status, date_of_birth, street, house_number, postal_code, city, email',
|
||||
)
|
||||
.eq('account_id', accountId)
|
||||
.eq('status', 'active')
|
||||
.order('last_name');
|
||||
|
||||
if (error) return { success: false, error: `DB-Fehler: ${error.message}` };
|
||||
if (!members?.length) return { success: false, error: 'Keine aktiven Mitglieder.' };
|
||||
if (!members?.length)
|
||||
return { success: false, error: 'Keine aktiven Mitglieder.' };
|
||||
|
||||
const { Document, Page, View, Text, StyleSheet, renderToBuffer, Svg, Rect, Circle } =
|
||||
await import('@react-pdf/renderer');
|
||||
const {
|
||||
Document,
|
||||
Page,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
renderToBuffer,
|
||||
Svg,
|
||||
Rect,
|
||||
Circle,
|
||||
} = await import('@react-pdf/renderer');
|
||||
|
||||
// — Brand colors (configurable later via account settings) —
|
||||
const PRIMARY = '#1e40af';
|
||||
@@ -107,7 +124,13 @@ async function generateMemberCards(
|
||||
const LIGHT_GRAY = '#f1f5f9';
|
||||
|
||||
const s = StyleSheet.create({
|
||||
page: { padding: 24, flexDirection: 'row', flexWrap: 'wrap', gap: 16, fontFamily: 'Helvetica' },
|
||||
page: {
|
||||
padding: 24,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 16,
|
||||
fontFamily: 'Helvetica',
|
||||
},
|
||||
|
||||
// ── Card shell ──
|
||||
card: {
|
||||
@@ -138,10 +161,22 @@ async function generateMemberCards(
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
badgeText: { fontSize: 6, color: PRIMARY, fontFamily: 'Helvetica-Bold', textTransform: 'uppercase' as const, letterSpacing: 0.8 },
|
||||
badgeText: {
|
||||
fontSize: 6,
|
||||
color: PRIMARY,
|
||||
fontFamily: 'Helvetica-Bold',
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
|
||||
// ── Main content ──
|
||||
body: { flexDirection: 'row', paddingHorizontal: 14, paddingTop: 8, gap: 12, flex: 1 },
|
||||
body: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: 8,
|
||||
gap: 12,
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
// Photo column
|
||||
photoCol: { width: 64, alignItems: 'center' },
|
||||
@@ -165,11 +200,22 @@ async function generateMemberCards(
|
||||
|
||||
// Info column
|
||||
infoCol: { flex: 1, justifyContent: 'center' },
|
||||
memberName: { fontSize: 14, fontFamily: 'Helvetica-Bold', color: DARK, marginBottom: 6 },
|
||||
memberName: {
|
||||
fontSize: 14,
|
||||
fontFamily: 'Helvetica-Bold',
|
||||
color: DARK,
|
||||
marginBottom: 6,
|
||||
},
|
||||
|
||||
fieldGroup: { flexDirection: 'row', flexWrap: 'wrap', gap: 4 },
|
||||
field: { width: '48%', marginBottom: 5 },
|
||||
fieldLabel: { fontSize: 6, color: GRAY, textTransform: 'uppercase' as const, letterSpacing: 0.6, marginBottom: 1 },
|
||||
fieldLabel: {
|
||||
fontSize: 6,
|
||||
color: GRAY,
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: 0.6,
|
||||
marginBottom: 1,
|
||||
},
|
||||
fieldValue: { fontSize: 8, color: DARK, fontFamily: 'Helvetica-Bold' },
|
||||
|
||||
// ── Footer ──
|
||||
@@ -184,10 +230,16 @@ async function generateMemberCards(
|
||||
},
|
||||
footerLeft: { fontSize: 6, color: GRAY },
|
||||
footerRight: { fontSize: 6, color: GRAY },
|
||||
validDot: { width: 5, height: 5, borderRadius: 2.5, backgroundColor: '#22c55e', marginRight: 3 },
|
||||
validDot: {
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: 2.5,
|
||||
backgroundColor: '#22c55e',
|
||||
marginRight: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const today = new Date().toLocaleDateString('de-DE');
|
||||
const today = formatDate(new Date());
|
||||
const year = new Date().getFullYear();
|
||||
const cardsPerPage = 4;
|
||||
const pages: React.ReactElement[] = [];
|
||||
@@ -198,51 +250,118 @@ async function generateMemberCards(
|
||||
pages.push(
|
||||
React.createElement(
|
||||
Page,
|
||||
{ key: `p${i}`, size: input.format === 'letter' ? 'LETTER' : (input.format.toUpperCase() as 'A4'|'A5'), orientation: input.orientation, style: s.page },
|
||||
{
|
||||
key: `p${i}`,
|
||||
size:
|
||||
input.format === 'letter'
|
||||
? 'LETTER'
|
||||
: (input.format.toUpperCase() as 'A4' | 'A5'),
|
||||
orientation: input.orientation,
|
||||
style: s.page,
|
||||
},
|
||||
...batch.map((m) =>
|
||||
React.createElement(View, { key: m.id, style: s.card },
|
||||
React.createElement(
|
||||
View,
|
||||
{ key: m.id, style: s.card },
|
||||
// Accent bar
|
||||
React.createElement(View, { style: s.accentBar }),
|
||||
|
||||
// Header
|
||||
React.createElement(View, { style: s.header },
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.header },
|
||||
React.createElement(Text, { style: s.clubName }, accountName),
|
||||
React.createElement(View, { style: s.badge },
|
||||
React.createElement(Text, { style: s.badgeText }, 'Mitgliedsausweis'),
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.badge },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.badgeText },
|
||||
'Mitgliedsausweis',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Body: photo + info
|
||||
React.createElement(View, { style: s.body },
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.body },
|
||||
// Photo column
|
||||
React.createElement(View, { style: s.photoCol },
|
||||
React.createElement(View, { style: s.photoFrame },
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.photoCol },
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.photoFrame },
|
||||
React.createElement(Text, { style: s.photoIcon }, '👤'),
|
||||
),
|
||||
React.createElement(Text, { style: s.memberNumber }, `Nr. ${m.member_number ?? '–'}`),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.memberNumber },
|
||||
`Nr. ${m.member_number ?? '–'}`,
|
||||
),
|
||||
),
|
||||
|
||||
// Info column
|
||||
React.createElement(View, { style: s.infoCol },
|
||||
React.createElement(Text, { style: s.memberName }, `${m.first_name} ${m.last_name}`),
|
||||
React.createElement(View, { style: s.fieldGroup },
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.infoCol },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.memberName },
|
||||
`${m.first_name} ${m.last_name}`,
|
||||
),
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.fieldGroup },
|
||||
// Entry date
|
||||
React.createElement(View, { style: s.field },
|
||||
React.createElement(Text, { style: s.fieldLabel }, 'Mitglied seit'),
|
||||
React.createElement(Text, { style: s.fieldValue }, fmtDate(m.entry_date)),
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.field },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.fieldLabel },
|
||||
'Mitglied seit',
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.fieldValue },
|
||||
fmtDate(m.entry_date),
|
||||
),
|
||||
),
|
||||
// Date of birth
|
||||
React.createElement(View, { style: s.field },
|
||||
React.createElement(Text, { style: s.fieldLabel }, 'Geb.-Datum'),
|
||||
React.createElement(Text, { style: s.fieldValue }, fmtDate(m.date_of_birth)),
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.field },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.fieldLabel },
|
||||
'Geb.-Datum',
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.fieldValue },
|
||||
fmtDate(m.date_of_birth),
|
||||
),
|
||||
),
|
||||
// Address
|
||||
React.createElement(View, { style: { ...s.field, width: '100%' } },
|
||||
React.createElement(Text, { style: s.fieldLabel }, 'Adresse'),
|
||||
React.createElement(Text, { style: s.fieldValue },
|
||||
[m.street, m.house_number].filter(Boolean).join(' ') || '–',
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: { ...s.field, width: '100%' } },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.fieldLabel },
|
||||
'Adresse',
|
||||
),
|
||||
React.createElement(Text, { style: { ...s.fieldValue, marginTop: 1 } },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.fieldValue },
|
||||
[m.street, m.house_number].filter(Boolean).join(' ') ||
|
||||
'–',
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: { ...s.fieldValue, marginTop: 1 } },
|
||||
[m.postal_code, m.city].filter(Boolean).join(' ') || '',
|
||||
),
|
||||
),
|
||||
@@ -251,12 +370,24 @@ async function generateMemberCards(
|
||||
),
|
||||
|
||||
// Footer
|
||||
React.createElement(View, { style: s.footer },
|
||||
React.createElement(View, { style: { flexDirection: 'row', alignItems: 'center' } },
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.footer },
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: { flexDirection: 'row', alignItems: 'center' } },
|
||||
React.createElement(View, { style: s.validDot }),
|
||||
React.createElement(Text, { style: s.footerLeft }, `Gültig ${year}/${year + 1}`),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.footerLeft },
|
||||
`Gültig ${year}/${year + 1}`,
|
||||
),
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ style: s.footerRight },
|
||||
`Ausgestellt ${today}`,
|
||||
),
|
||||
React.createElement(Text, { style: s.footerRight }, `Ausgestellt ${today}`),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -285,17 +416,22 @@ async function generateLabels(
|
||||
): Promise<GenerateDocumentResult> {
|
||||
const { data: members, error } = await client
|
||||
.from('members')
|
||||
.select('first_name, last_name, street, house_number, postal_code, city, salutation, title')
|
||||
.select(
|
||||
'first_name, last_name, street, house_number, postal_code, city, salutation, title',
|
||||
)
|
||||
.eq('account_id', accountId)
|
||||
.eq('status', 'active')
|
||||
.order('last_name');
|
||||
|
||||
if (error) return { success: false, error: `DB-Fehler: ${error.message}` };
|
||||
if (!members?.length) return { success: false, error: 'Keine aktiven Mitglieder.' };
|
||||
if (!members?.length)
|
||||
return { success: false, error: 'Keine aktiven Mitglieder.' };
|
||||
|
||||
const api = createDocumentGeneratorApi();
|
||||
const records = members.map((m) => ({
|
||||
line1: [m.salutation, m.title, m.first_name, m.last_name].filter(Boolean).join(' '),
|
||||
line1: [m.salutation, m.title, m.first_name, m.last_name]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
line2: [m.street, m.house_number].filter(Boolean).join(' ') || undefined,
|
||||
line3: [m.postal_code, m.city].filter(Boolean).join(' ') || undefined,
|
||||
}));
|
||||
@@ -320,7 +456,9 @@ async function generateMemberReport(
|
||||
): Promise<GenerateDocumentResult> {
|
||||
const { data: members, error } = await client
|
||||
.from('members')
|
||||
.select('member_number, last_name, first_name, email, postal_code, city, status, entry_date')
|
||||
.select(
|
||||
'member_number, last_name, first_name, email, postal_code, city, status, entry_date',
|
||||
)
|
||||
.eq('account_id', accountId)
|
||||
.order('last_name');
|
||||
|
||||
@@ -346,11 +484,21 @@ async function generateMemberReport(
|
||||
|
||||
const hdr = ws.getRow(1);
|
||||
hdr.font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
||||
hdr.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1E40AF' } };
|
||||
hdr.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FF1E40AF' },
|
||||
};
|
||||
hdr.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
hdr.height = 24;
|
||||
|
||||
const SL: Record<string, string> = { active: 'Aktiv', inactive: 'Inaktiv', pending: 'Ausstehend', resigned: 'Ausgetreten', excluded: 'Ausgeschlossen' };
|
||||
const SL: Record<string, string> = {
|
||||
active: 'Aktiv',
|
||||
inactive: 'Inaktiv',
|
||||
pending: 'Ausstehend',
|
||||
resigned: 'Ausgetreten',
|
||||
excluded: 'Ausgeschlossen',
|
||||
};
|
||||
|
||||
for (const m of members) {
|
||||
ws.addRow({
|
||||
@@ -361,12 +509,17 @@ async function generateMemberReport(
|
||||
plz: m.postal_code ?? '',
|
||||
ort: m.city ?? '',
|
||||
status: SL[m.status] ?? m.status,
|
||||
eintritt: m.entry_date ? new Date(m.entry_date).toLocaleDateString('de-DE') : '',
|
||||
eintritt: m.entry_date ? formatDate(m.entry_date) : '',
|
||||
});
|
||||
}
|
||||
|
||||
ws.eachRow((row, n) => {
|
||||
if (n > 1 && n % 2 === 0) row.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
||||
if (n > 1 && n % 2 === 0)
|
||||
row.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFF1F5F9' },
|
||||
};
|
||||
row.border = { bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } } };
|
||||
});
|
||||
|
||||
@@ -379,7 +532,8 @@ async function generateMemberReport(
|
||||
return {
|
||||
success: true,
|
||||
data: Buffer.from(buf as ArrayBuffer).toString('base64'),
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
mimeType:
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
fileName: `${input.title || 'Mitgliederbericht'}.xlsx`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
import { GenerateDocumentForm } from '../_components/generate-document-form';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
|
||||
@@ -13,8 +13,8 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -40,32 +40,28 @@ const DOCUMENT_TYPES = [
|
||||
{
|
||||
id: 'labels',
|
||||
title: 'Etiketten',
|
||||
description:
|
||||
'Adressetiketten für Serienbriefe im Avery-Format drucken.',
|
||||
description: 'Adressetiketten für Serienbriefe im Avery-Format drucken.',
|
||||
icon: Tag,
|
||||
color: 'text-orange-600 bg-orange-50',
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
title: 'Bericht',
|
||||
description:
|
||||
'Statistische Auswertungen und Berichte als PDF oder Excel.',
|
||||
description: 'Statistische Auswertungen und Berichte als PDF oder Excel.',
|
||||
icon: BarChart3,
|
||||
color: 'text-purple-600 bg-purple-50',
|
||||
},
|
||||
{
|
||||
id: 'letter',
|
||||
title: 'Brief',
|
||||
description:
|
||||
'Serienbriefe mit personalisierten Platzhaltern erstellen.',
|
||||
description: 'Serienbriefe mit personalisierten Platzhaltern erstellen.',
|
||||
icon: Mail,
|
||||
color: 'text-rose-600 bg-rose-50',
|
||||
},
|
||||
{
|
||||
id: 'certificate',
|
||||
title: 'Zertifikat',
|
||||
description:
|
||||
'Teilnahmebescheinigungen und Zertifikate mit Unterschrift.',
|
||||
description: 'Teilnahmebescheinigungen und Zertifikate mit Unterschrift.',
|
||||
icon: Award,
|
||||
color: 'text-amber-600 bg-amber-50',
|
||||
},
|
||||
@@ -84,7 +80,11 @@ export default async function DocumentsPage({ params }: PageProps) {
|
||||
if (!acct) return <AccountNotFound />;
|
||||
|
||||
return (
|
||||
<CmsPageShell account={account} title="Dokumente" description="Dokumente erstellen und verwalten">
|
||||
<CmsPageShell
|
||||
account={account}
|
||||
title="Dokumente"
|
||||
description="Dokumente erstellen und verwalten"
|
||||
>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end">
|
||||
@@ -108,7 +108,7 @@ export default async function DocumentsPage({ params }: PageProps) {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col justify-between gap-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{docType.description}
|
||||
</p>
|
||||
<Link
|
||||
|
||||
@@ -6,9 +6,9 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
import { CmsPageShell } from '~/components/cms-page-shell';
|
||||
import { EmptyState } from '~/components/empty-state';
|
||||
import { AccountNotFound } from '~/components/account-not-found';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -69,7 +69,7 @@ export default async function DocumentTemplatesPage({ params }: PageProps) {
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/50">
|
||||
<tr className="bg-muted/50 border-b">
|
||||
<th className="p-3 text-left font-medium">Name</th>
|
||||
<th className="p-3 text-left font-medium">Typ</th>
|
||||
<th className="p-3 text-left font-medium">
|
||||
@@ -81,11 +81,11 @@ export default async function DocumentTemplatesPage({ params }: PageProps) {
|
||||
{templates.map((template) => (
|
||||
<tr
|
||||
key={template.id}
|
||||
className="border-b hover:bg-muted/30"
|
||||
className="hover:bg-muted/30 border-b"
|
||||
>
|
||||
<td className="p-3 font-medium">{template.name}</td>
|
||||
<td className="p-3">{template.type}</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
<td className="text-muted-foreground p-3">
|
||||
{template.description}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user