feat: Firmendaten um Geschäftsführer, Amtsgericht, Handelsregister erweitert

- Backend: CompanySettings Interface + GET/POST um managingDirector,
  localCourt, commercialRegister ergänzt (Redis-Storage)
- Frontend AdminCompanyPage: neue Sektion „Rechtliche Angaben" mit 3
  Feldern, Footer-Vorschau zeigt alle Angaben inkl. HR-Nummer
- Export-Service: PDF- und DOCX-Fußzeile enthält jetzt Geschäftsführer
  und Handelsregistereintrag (HRB + Amtsgericht kombiniert)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-15 09:28:06 +01:00
parent 0f5d01df2a
commit 69305a0b0b
3 changed files with 81 additions and 16 deletions

View file

@ -245,12 +245,17 @@ export class ProfileExportService {
if (companyRaw) { if (companyRaw) {
const c = JSON.parse(companyRaw) as Record<string, unknown>; const c = JSON.parse(companyRaw) as Record<string, unknown>;
const address = [c['street'], [c['postalCode'], c['city']].filter(Boolean).join(' ')].filter(Boolean).join(', '); const address = [c['street'], [c['postalCode'], c['city']].filter(Boolean).join(' ')].filter(Boolean).join(', ');
const hrEntry = c['commercialRegister'] && c['localCourt']
? `${c['commercialRegister']} ${c['localCourt']}`
: (c['commercialRegister'] || c['localCourt'] || null);
companyFooterText = [ companyFooterText = [
c['name'], c['name'],
address || null, address || null,
c['phone'] ? `Tel: ${c['phone']}` : null, c['phone'] ? `Tel: ${c['phone']}` : null,
c['email'], c['email'],
c['website'], c['website'],
c['managingDirector'] ? `Geschäftsführer: ${c['managingDirector']}` : null,
hrEntry,
].filter(Boolean).join(' | '); ].filter(Boolean).join(' | ');
} }
if (brandingRaw) { if (brandingRaw) {
@ -713,6 +718,7 @@ export class ProfileExportService {
const c = JSON.parse(companyRaw) as { const c = JSON.parse(companyRaw) as {
name?: string | null; street?: string | null; postalCode?: string | null; name?: string | null; street?: string | null; postalCode?: string | null;
city?: string | null; phone?: string | null; email?: string | null; website?: string | null; city?: string | null; phone?: string | null; email?: string | null; website?: string | null;
managingDirector?: string | null; localCourt?: string | null; commercialRegister?: string | null;
}; };
const parts: string[] = []; const parts: string[] = [];
if (c.name) parts.push(c.name); if (c.name) parts.push(c.name);
@ -721,6 +727,11 @@ export class ProfileExportService {
if (c.phone) parts.push(`Tel: ${c.phone}`); if (c.phone) parts.push(`Tel: ${c.phone}`);
if (c.email) parts.push(c.email); if (c.email) parts.push(c.email);
if (c.website) parts.push(c.website); if (c.website) parts.push(c.website);
if (c.managingDirector) parts.push(`Geschäftsführer: ${c.managingDirector}`);
const hrEntry = c.commercialRegister && c.localCourt
? `${c.commercialRegister} ${c.localCourt}`
: (c.commercialRegister || c.localCourt || null);
if (hrEntry) parts.push(hrEntry);
if (parts.length) companyFooterText = parts.join(' | '); if (parts.length) companyFooterText = parts.join(' | ');
} catch { /* ignore */ } } catch { /* ignore */ }
} }

View file

@ -44,6 +44,9 @@ interface CompanySettings {
phone: string | null; phone: string | null;
email: string | null; email: string | null;
website: string | null; website: string | null;
managingDirector: string | null;
localCourt: string | null;
commercialRegister: string | null;
} }
/** /**
@ -428,19 +431,23 @@ export class SettingsController {
const empty: CompanySettings = { const empty: CompanySettings = {
name: null, street: null, postalCode: null, city: null, name: null, street: null, postalCode: null, city: null,
phone: null, email: null, website: null, phone: null, email: null, website: null,
managingDirector: null, localCourt: null, commercialRegister: null,
}; };
const raw = await this.redis.get(COMPANY_SETTINGS_KEY); const raw = await this.redis.get(COMPANY_SETTINGS_KEY);
if (!raw) return empty; if (!raw) return empty;
try { try {
const data = JSON.parse(raw) as Record<string, unknown>; const data = JSON.parse(raw) as Record<string, unknown>;
return { return {
name: typeof data.name === 'string' ? data.name : null, name: typeof data.name === 'string' ? data.name : null,
street: typeof data.street === 'string' ? data.street : null, street: typeof data.street === 'string' ? data.street : null,
postalCode: typeof data.postalCode === 'string' ? data.postalCode : null, postalCode: typeof data.postalCode === 'string' ? data.postalCode : null,
city: typeof data.city === 'string' ? data.city : null, city: typeof data.city === 'string' ? data.city : null,
phone: typeof data.phone === 'string' ? data.phone : null, phone: typeof data.phone === 'string' ? data.phone : null,
email: typeof data.email === 'string' ? data.email : null, email: typeof data.email === 'string' ? data.email : null,
website: typeof data.website === 'string' ? data.website : null, website: typeof data.website === 'string' ? data.website : null,
managingDirector: typeof data.managingDirector === 'string' ? data.managingDirector : null,
localCourt: typeof data.localCourt === 'string' ? data.localCourt : null,
commercialRegister: typeof data.commercialRegister === 'string' ? data.commercialRegister : null,
}; };
} catch { } catch {
return empty; return empty;
@ -459,13 +466,16 @@ export class SettingsController {
@Body() body: CompanySettings, @Body() body: CompanySettings,
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
const data: CompanySettings = { const data: CompanySettings = {
name: body.name?.trim() || null, name: body.name?.trim() || null,
street: body.street?.trim() || null, street: body.street?.trim() || null,
postalCode: body.postalCode?.trim() || null, postalCode: body.postalCode?.trim() || null,
city: body.city?.trim() || null, city: body.city?.trim() || null,
phone: body.phone?.trim() || null, phone: body.phone?.trim() || null,
email: body.email?.trim() || null, email: body.email?.trim() || null,
website: body.website?.trim() || null, website: body.website?.trim() || null,
managingDirector: body.managingDirector?.trim() || null,
localCourt: body.localCourt?.trim() || null,
commercialRegister: body.commercialRegister?.trim() || null,
}; };
await this.redis.set(COMPANY_SETTINGS_KEY, JSON.stringify(data)); await this.redis.set(COMPANY_SETTINGS_KEY, JSON.stringify(data));
this.logger.log('Firmendaten aktualisiert'); this.logger.log('Firmendaten aktualisiert');

View file

@ -10,11 +10,15 @@ interface CompanySettings {
phone: string | null; phone: string | null;
email: string | null; email: string | null;
website: string | null; website: string | null;
managingDirector: string | null;
localCourt: string | null;
commercialRegister: string | null;
} }
const empty: CompanySettings = { const empty: CompanySettings = {
name: null, street: null, postalCode: null, city: null, name: null, street: null, postalCode: null, city: null,
phone: null, email: null, website: null, phone: null, email: null, website: null,
managingDirector: null, localCourt: null, commercialRegister: null,
}; };
const cardStyle: React.CSSProperties = { const cardStyle: React.CSSProperties = {
@ -163,7 +167,7 @@ export function AdminCompanyPage() {
</div> </div>
</div> </div>
<div style={{ marginBottom: '1.5rem' }}> <div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>Website</label> <label style={labelStyle}>Website</label>
<input <input
style={inputStyle} style={inputStyle}
@ -173,6 +177,42 @@ export function AdminCompanyPage() {
/> />
</div> </div>
<hr style={{ border: 'none', borderTop: '1px solid var(--color-border)', margin: '1.25rem 0' }} />
<p style={{ margin: '0 0 1rem', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text-secondary)', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
Rechtliche Angaben
</p>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>Geschäftsführer</label>
<input
style={inputStyle}
value={form.managingDirector ?? ''}
onChange={(e) => set('managingDirector', e.target.value)}
placeholder="Max Mustermann"
/>
</div>
<div style={{ ...rowStyle, marginBottom: '1.5rem' }}>
<div>
<label style={labelStyle}>Amtsgericht</label>
<input
style={inputStyle}
value={form.localCourt ?? ''}
onChange={(e) => set('localCourt', e.target.value)}
placeholder="Amtsgericht Köln"
/>
</div>
<div>
<label style={labelStyle}>Handelsregisternummer</label>
<input
style={inputStyle}
value={form.commercialRegister ?? ''}
onChange={(e) => set('commercialRegister', e.target.value)}
placeholder="HRB 12345"
/>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<button <button
style={btnPrimary} style={btnPrimary}
@ -195,7 +235,7 @@ export function AdminCompanyPage() {
</div> </div>
{/* Vorschau Fußzeile */} {/* Vorschau Fußzeile */}
{(form.name || form.city || form.phone || form.email || form.website) && ( {(form.name || form.city || form.phone || form.email || form.website || form.managingDirector || form.commercialRegister) && (
<div style={cardStyle}> <div style={cardStyle}>
<h3 style={{ margin: '0 0 0.75rem', fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text-secondary)' }}> <h3 style={{ margin: '0 0 0.75rem', fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text-secondary)' }}>
VORSCHAU FUSSZEILE VORSCHAU FUSSZEILE
@ -213,6 +253,10 @@ export function AdminCompanyPage() {
form.phone ? `Tel: ${form.phone}` : null, form.phone ? `Tel: ${form.phone}` : null,
form.email, form.email,
form.website, form.website,
form.managingDirector ? `Geschäftsführer: ${form.managingDirector}` : null,
form.commercialRegister && form.localCourt
? `${form.commercialRegister} ${form.localCourt}`
: form.commercialRegister || (form.localCourt ? form.localCourt : null),
].filter(Boolean).join(' | ')} ].filter(Boolean).join(' | ')}
</div> </div>
</div> </div>