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
188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
import {
|
|
defaultFoundingWizardState,
|
|
type FoundingWizardState,
|
|
type Gesellschafter,
|
|
type GFContract,
|
|
type GeneratedDocument,
|
|
} from '@/lib/sdk/founding/types'
|
|
|
|
const STORAGE_KEY = 'breakpilot:founding-wizard:state:v1'
|
|
|
|
export const FOUNDING_WIZARD_STEPS = [
|
|
{ id: 1, name: 'Stage & Basics', description: 'Unternehmensname, Sitz, Gegenstand' },
|
|
{ id: 2, name: 'Gesellschafter', description: 'Gründer und ihre Anteile' },
|
|
{ id: 3, name: 'Geschäftsführer', description: 'GF-Bestellung und Rollen' },
|
|
{ id: 4, name: 'Kapital', description: 'Stammkapital und Einzahlung' },
|
|
{ id: 5, name: 'Notar', description: 'Notartermin und Beurkundung' },
|
|
{ id: 6, name: 'SHA-Optionen', description: 'Vesting, Drag-Along, Reserved Matters' },
|
|
{ id: 7, name: 'GF-Verträge', description: 'Vergütung, D&O, Kündigungsfristen' },
|
|
{ id: 8, name: 'Dokumente generieren', description: 'Auswahl und Word-Export' },
|
|
]
|
|
|
|
export function useFoundingWizardForm() {
|
|
const [state, setState] = useState<FoundingWizardState>(defaultFoundingWizardState())
|
|
const [hydrated, setHydrated] = useState(false)
|
|
const [generating, setGenerating] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Hydrate from localStorage
|
|
useEffect(() => {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw)
|
|
setState({ ...defaultFoundingWizardState(), ...parsed })
|
|
}
|
|
} catch {
|
|
// ignore corrupted storage
|
|
}
|
|
setHydrated(true)
|
|
}, [])
|
|
|
|
// Persist on every change after hydration
|
|
useEffect(() => {
|
|
if (!hydrated) return
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
|
} catch {
|
|
// quota exceeded - ignore
|
|
}
|
|
}, [state, hydrated])
|
|
|
|
const update = useCallback(<K extends keyof FoundingWizardState>(
|
|
key: K,
|
|
value: FoundingWizardState[K] | ((prev: FoundingWizardState[K]) => FoundingWizardState[K])
|
|
) => {
|
|
setState(prev => ({
|
|
...prev,
|
|
[key]: typeof value === 'function' ? (value as Function)(prev[key]) : value,
|
|
}))
|
|
}, [])
|
|
|
|
const setStep = useCallback((step: number) => {
|
|
setState(prev => ({ ...prev, current_step: step }))
|
|
}, [])
|
|
|
|
const nextStep = useCallback(() => {
|
|
setState(prev => ({ ...prev, current_step: Math.min(prev.current_step + 1, FOUNDING_WIZARD_STEPS.length) }))
|
|
}, [])
|
|
|
|
const prevStep = useCallback(() => {
|
|
setState(prev => ({ ...prev, current_step: Math.max(prev.current_step - 1, 1) }))
|
|
}, [])
|
|
|
|
const reset = useCallback(() => {
|
|
setState(defaultFoundingWizardState())
|
|
try { localStorage.removeItem(STORAGE_KEY) } catch {}
|
|
}, [])
|
|
|
|
// Gesellschafter helpers
|
|
const addGesellschafter = useCallback((gs: Omit<Gesellschafter, 'id' | 'anteil_nr'>) => {
|
|
setState(prev => {
|
|
const nextNr = (prev.gesellschafter.reduce((m, g) => Math.max(m, g.anteil_nr), 0)) + 1
|
|
const id = `gs_${Date.now()}_${nextNr}`
|
|
return { ...prev, gesellschafter: [...prev.gesellschafter, { ...gs, id, anteil_nr: nextNr }] }
|
|
})
|
|
}, [])
|
|
|
|
const updateGesellschafter = useCallback((id: string, patch: Partial<Gesellschafter>) => {
|
|
setState(prev => ({
|
|
...prev,
|
|
gesellschafter: prev.gesellschafter.map(g => g.id === id ? { ...g, ...patch } : g),
|
|
}))
|
|
}, [])
|
|
|
|
const removeGesellschafter = useCallback((id: string) => {
|
|
setState(prev => ({
|
|
...prev,
|
|
gesellschafter: prev.gesellschafter.filter(g => g.id !== id),
|
|
gf_contracts: prev.gf_contracts.filter(c => c.gesellschafter_id !== id),
|
|
}))
|
|
}, [])
|
|
|
|
// GF Contract helpers
|
|
const upsertGFContract = useCallback((contract: GFContract) => {
|
|
setState(prev => {
|
|
const idx = prev.gf_contracts.findIndex(c => c.gesellschafter_id === contract.gesellschafter_id)
|
|
const next = [...prev.gf_contracts]
|
|
if (idx >= 0) next[idx] = contract
|
|
else next.push(contract)
|
|
return { ...prev, gf_contracts: next }
|
|
})
|
|
}, [])
|
|
|
|
// Validation (canProceed for current step)
|
|
const canProceed = useMemo(() => {
|
|
switch (state.current_step) {
|
|
case 1:
|
|
return state.basics.company_name.trim().length > 1 &&
|
|
state.basics.company_seat.trim().length > 1 &&
|
|
state.basics.company_purpose_description.trim().length > 10
|
|
case 2: {
|
|
if (state.gesellschafter.length < 1) return false
|
|
const sum = state.gesellschafter.reduce((s, g) => s + (g.nennbetrag_eur || 0), 0)
|
|
return sum === state.capital.stammkapital_eur
|
|
}
|
|
case 3:
|
|
return state.gesellschafter.some(g => g.is_geschaeftsfuehrer)
|
|
case 4:
|
|
return state.capital.stammkapital_eur >= 25000
|
|
case 5:
|
|
return state.notar.notary_name.trim().length > 1 && state.notar.notary_place.trim().length > 1
|
|
case 6:
|
|
return true
|
|
case 7:
|
|
return state.gesellschafter.filter(g => g.is_geschaeftsfuehrer)
|
|
.every(g => state.gf_contracts.some(c => c.gesellschafter_id === g.id))
|
|
case 8:
|
|
return state.selected_documents.length > 0
|
|
default:
|
|
return false
|
|
}
|
|
}, [state])
|
|
|
|
const generateDocuments = useCallback(async (): Promise<GeneratedDocument[]> => {
|
|
setGenerating(true)
|
|
setError(null)
|
|
try {
|
|
const response = await fetch('/api/v1/founding-wizard/generate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(state),
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(`Generierung fehlgeschlagen: ${response.status}`)
|
|
}
|
|
const data = await response.json()
|
|
const docs: GeneratedDocument[] = data.documents || []
|
|
setState(prev => ({ ...prev, generated_documents: docs }))
|
|
return docs
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : 'Unbekannter Fehler'
|
|
setError(msg)
|
|
throw e
|
|
} finally {
|
|
setGenerating(false)
|
|
}
|
|
}, [state])
|
|
|
|
// Derived: hat zugehöriger GF einen Vertrag?
|
|
const gf_list = useMemo(
|
|
() => state.gesellschafter.filter(g => g.is_geschaeftsfuehrer),
|
|
[state.gesellschafter]
|
|
)
|
|
|
|
return {
|
|
state, hydrated, generating, error,
|
|
update, setStep, nextStep, prevStep, reset,
|
|
addGesellschafter, updateGesellschafter, removeGesellschafter,
|
|
upsertGFContract,
|
|
canProceed, generateDocuments,
|
|
gf_list,
|
|
steps: FOUNDING_WIZARD_STEPS,
|
|
}
|
|
}
|