mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
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>
222 lines
6.7 KiB
TypeScript
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 & 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>
|
|
);
|
|
}
|