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
147 lines
6.0 KiB
TypeScript
147 lines
6.0 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import type { FoundingWizardState, GeneratedDocument } from '@/lib/sdk/founding/types'
|
|
import { NOTARY_BUNDLE_DOCUMENTS } from '@/lib/sdk/founding/template-categories'
|
|
|
|
interface Props {
|
|
state: FoundingWizardState
|
|
update: <K extends keyof FoundingWizardState>(k: K, v: FoundingWizardState[K]) => void
|
|
generating: boolean
|
|
error: string | null
|
|
onGenerate: () => Promise<GeneratedDocument[]>
|
|
}
|
|
|
|
const DOC_LABELS: Record<string, string> = {
|
|
articles_of_association: 'Satzung',
|
|
gesellschafterliste: 'Gesellschafterliste (§ 40 GmbHG)',
|
|
gf_bestellungsbeschluss: 'Gesellschafterbeschluss zur GF-Bestellung',
|
|
hrb_anmeldung: 'Handelsregister-Anmeldung',
|
|
sha: 'Shareholders\' Agreement (SHA)',
|
|
geschaeftsordnung_gf: 'Geschäftsordnung Geschäftsführung (GO-GF)',
|
|
managing_director_employment_contract: 'GF-Dienstvertrag (pro GF)',
|
|
ip_assignment_agreement: 'IP-Assignment (pro Gründer)',
|
|
term_sheet: 'Term Sheet',
|
|
convertible_loan_agreement: 'Wandeldarlehensvertrag',
|
|
subscription_agreement: 'Beteiligungsvertrag',
|
|
esop_plan: 'ESOP/VSOP-Plan',
|
|
cap_table: 'Cap Table',
|
|
}
|
|
|
|
export function StepGenerate({ state, update, generating, error, onGenerate }: Props) {
|
|
const toggleDoc = (docType: string) => {
|
|
const next = state.selected_documents.includes(docType)
|
|
? state.selected_documents.filter(d => d !== docType)
|
|
: [...state.selected_documents, docType]
|
|
update('selected_documents', next)
|
|
}
|
|
|
|
const selectNotaryBundle = () => {
|
|
update('selected_documents', [...NOTARY_BUNDLE_DOCUMENTS])
|
|
}
|
|
|
|
const summary = useMemo(() => ({
|
|
name: state.basics.company_name,
|
|
seat: state.basics.company_seat,
|
|
stammkapital: state.capital.stammkapital_eur,
|
|
num_gesellschafter: state.gesellschafter.length,
|
|
num_gf: state.gesellschafter.filter(g => g.is_geschaeftsfuehrer).length,
|
|
}), [state])
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
|
<h3 className="font-semibold text-purple-900 mb-2">Zusammenfassung</h3>
|
|
<dl className="grid grid-cols-2 gap-2 text-sm" data-testid="generate-summary">
|
|
<dt className="text-gray-600">Firma:</dt><dd>{summary.name} ({state.basics.legal_form})</dd>
|
|
<dt className="text-gray-600">Sitz:</dt><dd>{summary.seat}</dd>
|
|
<dt className="text-gray-600">Stammkapital:</dt><dd>{summary.stammkapital.toLocaleString('de-DE')} €</dd>
|
|
<dt className="text-gray-600">Gesellschafter:</dt><dd>{summary.num_gesellschafter}</dd>
|
|
<dt className="text-gray-600">Geschäftsführer:</dt><dd>{summary.num_gf}</dd>
|
|
<dt className="text-gray-600">Notar:</dt><dd>{state.notar.notary_name} ({state.notar.notary_place})</dd>
|
|
</dl>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between items-center mb-3">
|
|
<h3 className="font-semibold">Zu generierende Dokumente</h3>
|
|
<button
|
|
type="button"
|
|
data-testid="select-notary-bundle"
|
|
onClick={selectNotaryBundle}
|
|
className="text-sm text-purple-600 hover:underline"
|
|
>
|
|
➜ Notartermin-Bundle auswählen
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{Object.entries(DOC_LABELS).map(([docType, label]) => (
|
|
<label key={docType} className="flex items-start gap-3 p-2 hover:bg-gray-50 rounded">
|
|
<input
|
|
type="checkbox"
|
|
data-testid={`doc-${docType}`}
|
|
checked={state.selected_documents.includes(docType)}
|
|
onChange={() => toggleDoc(docType)}
|
|
className="mt-1"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium">{label}</div>
|
|
<div className="text-xs text-gray-500">{docType}</div>
|
|
</div>
|
|
{NOTARY_BUNDLE_DOCUMENTS.includes(docType) && (
|
|
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded">Notartermin</span>
|
|
)}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center pt-4 border-t">
|
|
<p className="text-sm text-gray-500">
|
|
{state.selected_documents.length} Dokument(e) ausgewählt
|
|
</p>
|
|
<button
|
|
data-testid="generate-docs"
|
|
onClick={onGenerate}
|
|
disabled={generating || state.selected_documents.length === 0}
|
|
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 font-medium"
|
|
>
|
|
{generating ? 'Generiere...' : 'Dokumente als Word generieren'}
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-900" data-testid="generate-error">
|
|
Fehler: {error}
|
|
</div>
|
|
)}
|
|
|
|
{state.generated_documents && state.generated_documents.length > 0 && (
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4" data-testid="generated-docs">
|
|
<h3 className="font-semibold text-green-900 mb-3">
|
|
✓ {state.generated_documents.length} Dokument(e) generiert
|
|
</h3>
|
|
<ul className="space-y-2">
|
|
{state.generated_documents.map((doc, idx) => (
|
|
<li key={idx} className="flex justify-between items-center bg-white rounded px-3 py-2 border border-green-200">
|
|
<div>
|
|
<div className="text-sm font-medium">{doc.title}</div>
|
|
<div className="text-xs text-gray-500">{(doc.size_bytes / 1024).toFixed(1)} KB</div>
|
|
</div>
|
|
<a
|
|
href={doc.download_url}
|
|
download
|
|
data-testid={`download-${doc.document_type}`}
|
|
className="px-3 py-1.5 bg-green-600 text-white rounded text-sm hover:bg-green-700"
|
|
>
|
|
Word herunterladen
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|