7a5f1e48dd
[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
322 lines
14 KiB
TypeScript
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>
|
|
)
|
|
}
|