Files
breakpilot-compliance/admin-compliance/app/sdk/founding-wizard/_components/StepsSimpleConfig.tsx
T
Benjamin Admin 7a5f1e48dd feat(founding-wizard): Gründungs-Wizard für 2-Mann GmbH + 14 Notar-Templates
[migration-approved]

Templates (Migrations 123-136):
- 123 GO-GF (Geschäftsordnung Geschäftsführung)
- 124 SHA (Shareholders' Agreement, 56 Platzhalter)
- 125 Satzung (Articles of Association mit UG-Variante)
- 126 GF-Dienstvertrag (Trennungsprinzip Organ/Anstellung)
- 127 Arbeitsvertrag (AGG-neutral, NachwG, eAU)
- 128 Gesellschafterliste (§ 40 GmbHG)
- 129 GF-Bestellungsbeschluss (mit § 6 Abs. 2 Versicherung)
- 130 HRB-Anmeldung (§§ 7, 8, 39 GmbHG, § 12 HGB)
- 131 IP-Assignment Agreement (Gründer→GmbH)
- 132 Term Sheet (Pre-Seed/Seed VC-Standard)
- 133 Wandeldarlehensvertrag (Convertible Loan)
- 134 Beteiligungsvertrag (Subscription Agreement)
- 135 ESOP/VSOP-Plan (3 Varianten)
- 136 Cap Table

Kategorisierung (Migrations 137-138):
- ALTER TABLE compliance_legal_templates ADD lifecycle_stage TEXT[],
  functional_category TEXT (mit CHECK Constraints + GIN-Index)
- Backfill aller 105 Templates: lifecycle_stage (pre_founding|founding|
  startup|kmu|konzern) + functional_category (founding_legal|employment|
  investor_funding|...)

Backend Founding-Wizard Service:
- template_renderer.py: Handlebars-light ({{VAR}}, {{#IF FLAG}}...{{/IF}})
- wizard_to_context.py: Mapping Wizard-State → SCREAMING_SNAKE_CASE Vars
- markdown_to_docx.py: Markdown → DOCX via python-docx
- founding_wizard_routes.py: POST /v1/founding-wizard/generate
  → liefert base64-DOCX-Files für ausgewählte Templates

Frontend Founding-Wizard (/sdk/founding-wizard):
- 8-Step Wizard (Basics, Gesellschafter, GF, Kapital, Notar, SHA, GF-Verträge, Generate)
- useFoundingWizardForm Hook mit localStorage-Persistenz
- TypeScript Code-Registry (template-categories.ts) als Backup zur DB
- Word-Download via data:URLs (base64)

Tests:
- 20 Unit-Tests grün (Renderer, Context-Mapping, DOCX-Conversion)
- Playwright E2E-Test mit 2-Mann GmbH (Benjamin + Sharang) Test-Daten
2026-05-20 09:30:51 +02:00

322 lines
14 KiB
TypeScript

'use client'
/**
* Kombinierte einfache Steps: Geschäftsführer (3), Kapital (4), Notar (5), SHA (6).
* Jeder Sub-Step ist eine simple Form.
*/
import type { FoundingWizardState, GFContract } from '@/lib/sdk/founding/types'
interface PropsBase {
state: FoundingWizardState
update: <K extends keyof FoundingWizardState>(k: K, v: FoundingWizardState[K]) => void
}
export function StepGFAssignment({ state, update }: PropsBase) {
const founders = state.gesellschafter
const toggleGF = (id: string, val: boolean) => {
update('gesellschafter', state.gesellschafter.map(g => g.id === id ? { ...g, is_geschaeftsfuehrer: val } : g))
}
const setRole = (id: string, role: string) => {
update('gesellschafter', state.gesellschafter.map(g => g.id === id ? { ...g, internal_role: role } : g))
}
return (
<div className="space-y-4">
<p className="text-sm text-gray-600">
Wähle, welche Gesellschafter zu Geschäftsführern bestellt werden sollen. Standardmäßig sind alle Gründer auch GF.
</p>
{founders.length === 0 ? (
<p className="text-orange-600">Bitte zuerst Gesellschafter in Step 2 anlegen.</p>
) : (
<table className="w-full text-sm" data-testid="gf-assignment-table">
<thead className="bg-gray-100">
<tr>
<th className="px-3 py-2 text-left">Gesellschafter</th>
<th className="px-3 py-2 text-left">Interne Rolle (CEO, CTO, ...)</th>
<th className="px-3 py-2">GF?</th>
</tr>
</thead>
<tbody>
{founders.map(g => (
<tr key={g.id} className="border-t">
<td className="px-3 py-2 font-medium">{g.name}</td>
<td className="px-3 py-2">
<input
value={g.internal_role || ''}
onChange={e => setRole(g.id, e.target.value)}
className="px-2 py-1 border rounded w-48"
placeholder="CEO, CTO, COO..."
/>
</td>
<td className="px-3 py-2 text-center">
<input
type="checkbox"
data-testid={`gf-toggle-${g.anteil_nr}`}
checked={g.is_geschaeftsfuehrer}
onChange={e => toggleGF(g.id, e.target.checked)}
/>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)
}
export function StepCapital({ state, update }: PropsBase) {
const c = state.capital
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Stammkapital (EUR)</label>
<input
data-testid="stammkapital"
type="number" min={1} step={1}
value={c.stammkapital_eur}
onChange={e => update('capital', { ...c, stammkapital_eur: parseInt(e.target.value) || 0 })}
className="w-full px-3 py-2 border rounded-lg"
/>
<p className="mt-1 text-xs text-gray-500">GmbH: mind. 25.000 , UG: ab 1 </p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Einlage-Art</label>
<select
data-testid="einlage-method"
value={c.einlage_method}
onChange={e => update('capital', { ...c, einlage_method: e.target.value as typeof c.einlage_method })}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="Geld">Bargründung</option>
<option value="Sacheinlage">Sachgründung</option>
<option value="Geld und Sacheinlage">Misch-Gründung</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sofortige Einzahlung (%)
</label>
<input
data-testid="einlage-quote"
type="number" min={25} max={100}
value={c.einlage_quote_initial_pct}
onChange={e => update('capital', { ...c, einlage_quote_initial_pct: parseInt(e.target.value) || 50 })}
className="w-full px-3 py-2 border rounded-lg"
/>
<p className="mt-1 text-xs text-gray-500">Mind. 25% gem. § 7 Abs. 2 GmbHG, Standard 50%</p>
</div>
<div className="flex items-center gap-2 mt-7">
<input
type="checkbox"
id="has_sach"
data-testid="has-sacheinlage"
checked={c.has_sacheinlage}
onChange={e => update('capital', { ...c, has_sacheinlage: e.target.checked })}
/>
<label htmlFor="has_sach" className="text-sm">Sacheinlage-Klausel aktivieren</label>
</div>
</div>
</div>
)
}
export function StepNotar({ state, update }: PropsBase) {
const n = state.notar
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name des Notars</label>
<input
data-testid="notary-name"
value={n.notary_name}
onChange={e => update('notar', { ...n, notary_name: e.target.value })}
placeholder="z.B. Dr. Müller"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notarsitz</label>
<input
data-testid="notary-place"
value={n.notary_place}
onChange={e => update('notar', { ...n, notary_place: e.target.value })}
placeholder="z.B. Stuttgart"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Adresse</label>
<input
data-testid="notary-address"
value={n.notary_address || ''}
onChange={e => update('notar', { ...n, notary_address: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Geplanter Notartermin</label>
<input
data-testid="notarial-date"
type="date"
value={n.notarial_date || ''}
onChange={e => update('notar', { ...n, notarial_date: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm text-blue-900">
<strong>Hinweis:</strong> Die URNr. wird vom Notar beim Beurkundungstermin vergeben. Du kannst die generierte
HRB-Anmeldung als Vorbereitungsdokument zum Termin mitnehmen.
</div>
</div>
)
}
export function StepSHAConfig({ state, update }: PropsBase) {
const s = state.sha
const updateField = <K extends keyof typeof s>(k: K, v: typeof s[K]) => update('sha', { ...s, [k]: v })
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<input
type="checkbox"
data-testid="has-sha"
checked={s.has_sha}
onChange={e => updateField('has_sha', e.target.checked)}
/>
<label className="text-sm font-medium">SHA (Shareholders' Agreement) ist Teil des Notartermin-Pakets</label>
</div>
{s.has_sha && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-700 mb-1">Vesting-Dauer (Monate)</label>
<input data-testid="vesting-months" type="number" value={s.vesting_months}
onChange={e => updateField('vesting_months', parseInt(e.target.value) || 48)}
className="w-full px-3 py-2 border rounded-lg" />
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Cliff (Monate)</label>
<input data-testid="cliff-months" type="number" value={s.cliff_months}
onChange={e => updateField('cliff_months', parseInt(e.target.value) || 12)}
className="w-full px-3 py-2 border rounded-lg" />
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Drag-Along Schwelle (%)</label>
<input data-testid="drag-along-pct" type="number" value={s.drag_along_threshold_pct}
onChange={e => updateField('drag_along_threshold_pct', parseInt(e.target.value) || 75)}
className="w-full px-3 py-2 border rounded-lg" />
</div>
<div>
<label className="block text-sm text-gray-700 mb-1">Reserved-Matters Mehrheit (%)</label>
<input data-testid="reserved-matters-pct" type="number" value={s.reserved_matters_majority_pct}
onChange={e => updateField('reserved_matters_majority_pct', parseInt(e.target.value) || 75)}
className="w-full px-3 py-2 border rounded-lg" />
</div>
<div className="col-span-2 grid grid-cols-3 gap-3 mt-2">
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" data-testid="has-beirat" checked={s.has_beirat}
onChange={e => updateField('has_beirat', e.target.checked)} />
Beirat einrichten
</label>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" data-testid="has-texas" checked={s.has_texas_shootout}
onChange={e => updateField('has_texas_shootout', e.target.checked)} />
Texas Shoot-Out (Deadlock)
</label>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" data-testid="has-ceo" checked={s.has_ceo_designation}
onChange={e => updateField('has_ceo_designation', e.target.checked)} />
CEO mit Stichentscheid
</label>
</div>
</div>
)}
</div>
)
}
interface GFContractStepProps extends PropsBase {
gf_list: Array<{ id: string; name: string; internal_role?: string }>
upsertGFContract: (c: GFContract) => void
}
export function StepGFContracts({ state, gf_list, upsertGFContract }: GFContractStepProps) {
return (
<div className="space-y-4">
<p className="text-sm text-gray-600">
Für jeden Geschäftsführer wird ein Dienstvertrag generiert. Bitte Eckdaten ausfüllen.
</p>
{gf_list.length === 0 ? (
<p className="text-orange-600">Bitte zuerst in Step 2 mindestens einen GF anlegen.</p>
) : (
gf_list.map(gf => {
const c = state.gf_contracts.find(x => x.gesellschafter_id === gf.id) || {
gesellschafter_id: gf.id,
gross_annual_salary_eur: 84000,
has_bonus: false,
has_company_car: false,
has_bav: false,
vacation_days: 30,
kuendigungsfrist_gesellschaft_monate: 6,
kuendigungsfrist_gf_monate: 3,
para_181_release: true,
sv_status: 'sozialversicherungsfrei' as const,
}
const u = (patch: Partial<GFContract>) => upsertGFContract({ ...c, ...patch })
return (
<div key={gf.id} className="border rounded-lg p-4" data-testid={`contract-${gf.id}`}>
<h4 className="font-semibold mb-3">{gf.name} {gf.internal_role && `(${gf.internal_role})`}</h4>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs text-gray-700 mb-1">Jahresgehalt (EUR brutto)</label>
<input
data-testid={`salary-${gf.id}`}
type="number"
value={c.gross_annual_salary_eur}
onChange={e => u({ gross_annual_salary_eur: parseInt(e.target.value) || 0 })}
className="w-full px-2 py-1 border rounded"
/>
</div>
<div>
<label className="block text-xs text-gray-700 mb-1">Urlaubstage</label>
<input type="number" value={c.vacation_days}
onChange={e => u({ vacation_days: parseInt(e.target.value) || 30 })}
className="w-full px-2 py-1 border rounded" />
</div>
<div>
<label className="block text-xs text-gray-700 mb-1">SV-Status</label>
<select value={c.sv_status} onChange={e => u({ sv_status: e.target.value as GFContract['sv_status'] })}
className="w-full px-2 py-1 border rounded">
<option value="sozialversicherungsfrei">sv-frei (Standard für GF/Gesellschafter)</option>
<option value="sozialversicherungspflichtig">sv-pflichtig</option>
<option value="noch zu klären">noch zu klären</option>
</select>
</div>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={c.para_181_release}
onChange={e => u({ para_181_release: e.target.checked })} />
§ 181 BGB-Befreiung
</label>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={c.has_bonus}
onChange={e => u({ has_bonus: e.target.checked })} />
Bonus-Vereinbarung
</label>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={c.has_company_car}
onChange={e => u({ has_company_car: e.target.checked })} />
Firmenfahrzeug
</label>
</div>
</div>
)
})
)}
</div>
)
}