INSIGHT-MVP/packages/frontend/src/admin/AdminCompanyPage.tsx
Thomas Reitz 46ced98bf4 feat(core+frontend): Firmendaten im Admin + PDF-Fusszeile
Backend:
- GET/POST /settings/company (Redis-Key platform_company_settings)
  Felder: name, street, postalCode, city, phone, email, website
- ProfileExportService: RedisService injiziert, laedt Firmendaten vor PDF-Erzeugung
- PDF-Footer: Trennlinie + kompakte Zeile mit allen Firmendaten auf jeder Seite
  (bufferPages=true, switchToPage-Loop vor doc.end())

Frontend:
- AdminCompanyPage: Formular mit Vorschau der Fusszeile
- AdminLayout: neuer Tab 'Firmendaten'
- App.tsx: Route /admin/company

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:07:42 +01:00

222 lines
6.7 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../api/client';
interface CompanySettings {
name: string | null;
street: string | null;
postalCode: string | null;
city: string | null;
phone: string | null;
email: string | null;
website: string | null;
}
const empty: CompanySettings = {
name: null, street: null, postalCode: null, city: null,
phone: null, email: null, website: null,
};
const cardStyle: React.CSSProperties = {
background: 'var(--color-bg-card)',
borderRadius: 'var(--radius-md)',
boxShadow: 'var(--shadow-sm)',
border: '1px solid var(--color-border)',
padding: '1.5rem',
marginBottom: '1.5rem',
};
const btnPrimary: React.CSSProperties = {
padding: '0.5rem 1.25rem',
background: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
fontWeight: 600,
cursor: 'pointer',
};
const labelStyle: React.CSSProperties = {
display: 'block',
fontSize: '0.75rem',
fontWeight: 600,
color: 'var(--color-text-secondary)',
marginBottom: '0.25rem',
textTransform: 'uppercase',
letterSpacing: '0.04em',
};
const inputStyle: React.CSSProperties = {
width: '100%',
padding: '0.5rem 0.75rem',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
background: 'var(--color-bg)',
color: 'var(--color-text)',
boxSizing: 'border-box',
};
const rowStyle: React.CSSProperties = {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1rem',
};
export function AdminCompanyPage() {
const queryClient = useQueryClient();
const [form, setForm] = useState<CompanySettings>(empty);
const [saved, setSaved] = useState(false);
const { data, isLoading } = useQuery({
queryKey: ['company-settings'],
queryFn: () => api.get<CompanySettings>('/settings/company').then((r) => r.data),
});
useEffect(() => {
if (data) setForm(data);
}, [data]);
const mutation = useMutation({
mutationFn: (settings: CompanySettings) => api.post('/settings/company', settings),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['company-settings'] });
setSaved(true);
setTimeout(() => setSaved(false), 2500);
},
});
const set = (field: keyof CompanySettings, value: string) =>
setForm((prev) => ({ ...prev, [field]: value || null }));
if (isLoading) return <p>Lädt</p>;
return (
<div style={{ maxWidth: 640 }}>
<div style={cardStyle}>
<h2 style={{ margin: '0 0 0.25rem', fontSize: '1.125rem', fontWeight: 700 }}>Firmendaten</h2>
<p style={{ margin: '0 0 1.5rem', color: 'var(--color-text-secondary)', fontSize: '0.875rem' }}>
Diese Angaben erscheinen als Fußzeile in jedem PDF-Export.
</p>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>Firmenname</label>
<input
style={inputStyle}
value={form.name ?? ''}
onChange={(e) => set('name', e.target.value)}
placeholder="Muster GmbH"
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>Straße &amp; Hausnummer</label>
<input
style={inputStyle}
value={form.street ?? ''}
onChange={(e) => set('street', e.target.value)}
placeholder="Musterstraße 1"
/>
</div>
<div style={{ ...rowStyle, marginBottom: '1rem' }}>
<div>
<label style={labelStyle}>Postleitzahl</label>
<input
style={inputStyle}
value={form.postalCode ?? ''}
onChange={(e) => set('postalCode', e.target.value)}
placeholder="12345"
/>
</div>
<div>
<label style={labelStyle}>Stadt</label>
<input
style={inputStyle}
value={form.city ?? ''}
onChange={(e) => set('city', e.target.value)}
placeholder="Musterstadt"
/>
</div>
</div>
<div style={{ ...rowStyle, marginBottom: '1rem' }}>
<div>
<label style={labelStyle}>Telefon</label>
<input
style={inputStyle}
value={form.phone ?? ''}
onChange={(e) => set('phone', e.target.value)}
placeholder="+49 123 456789"
/>
</div>
<div>
<label style={labelStyle}>E-Mail</label>
<input
style={inputStyle}
type="email"
value={form.email ?? ''}
onChange={(e) => set('email', e.target.value)}
placeholder="info@muster.de"
/>
</div>
</div>
<div style={{ marginBottom: '1.5rem' }}>
<label style={labelStyle}>Website</label>
<input
style={inputStyle}
value={form.website ?? ''}
onChange={(e) => set('website', e.target.value)}
placeholder="https://www.muster.de"
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<button
style={btnPrimary}
onClick={() => mutation.mutate(form)}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Speichern…' : 'Speichern'}
</button>
{saved && (
<span style={{ color: 'var(--color-success, #16a34a)', fontSize: '0.875rem', fontWeight: 500 }}>
Gespeichert
</span>
)}
{mutation.isError && (
<span style={{ color: 'var(--color-danger, #dc2626)', fontSize: '0.875rem' }}>
Fehler beim Speichern
</span>
)}
</div>
</div>
{/* Vorschau Fußzeile */}
{(form.name || form.city || form.phone || form.email || form.website) && (
<div style={cardStyle}>
<h3 style={{ margin: '0 0 0.75rem', fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text-secondary)' }}>
VORSCHAU FUSSZEILE
</h3>
<div style={{
borderTop: '1px solid #ccc',
paddingTop: '0.5rem',
fontSize: '0.6875rem',
color: '#999',
textAlign: 'center',
}}>
{[
form.name,
[form.street, [form.postalCode, form.city].filter(Boolean).join(' ')].filter(Boolean).join(', '),
form.phone ? `Tel: ${form.phone}` : null,
form.email,
form.website,
].filter(Boolean).join(' | ')}
</div>
</div>
)}
</div>
);
}