7335f64f4f
CI / loc-budget (push) Failing after 20s
CI / detect-changes (push) Successful in 12s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 19s
CI / nodejs-build (push) Successful in 3m17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
Wizard unterstuetzt jetzt 2-4 Gesellschafter mit individuellem IP-Bereich: - Pro Gruender ein IP-Assignment-Vertrag (z.B. Benjamin: Compliance+RAG; Sharang: Security+Infrastruktur). Pro GF ein eigener Dienstvertrag. - Step 1: Prefill-Button aus Unternehmensprofil + Felder Registergericht und HRB-Nr. - Step 2: Rollen-Dropdown (CEO/CTO/CFO/COO/CPO/GF/Sonstige) statt freie Texteingabe, IP-Bereiche-Textarea pro Person. Backend: - generate_documents() iteriert pro Person fuer PER_PERSON_DOCS. - _build_person_context() injiziert ASSIGNOR_*, GF_*, IP_LIST_DETAILS aus person.ip_areas. - base_context() propagiert basics.register_court und basics.hrb_number. Tests: - 30/30 Pytest gruen (6 neue: Per-Person-Context, Slug-Helper, Registergericht-Propagation). - 4 neue Playwright-E2E-Specs (hermetisch via route.fulfill, mit Console-/Page-Error-Traps): kompletter 8-Step-Flow, Prefill-Fehlerpfad, Step-Navigation/Reset, Rollen-Dropdown + IP-Areas. - Spec setzt 'bp-sdk-cookie-consent' im addInitScript damit der CookieBannerOverlay nicht die Wizard-Buttons ueberlagert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
216 lines
9.2 KiB
TypeScript
216 lines
9.2 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { FoundingWizardState, Gesellschafter } from '@/lib/sdk/founding/types'
|
|
|
|
interface Props {
|
|
state: FoundingWizardState
|
|
addGesellschafter: (g: Omit<Gesellschafter, 'id' | 'anteil_nr'>) => void
|
|
updateGesellschafter: (id: string, p: Partial<Gesellschafter>) => void
|
|
removeGesellschafter: (id: string) => void
|
|
}
|
|
|
|
export function StepGesellschafter({ state, addGesellschafter, updateGesellschafter, removeGesellschafter }: Props) {
|
|
const [form, setForm] = useState({
|
|
name: '', geburtsdatum: '', adresse: '', email: '',
|
|
nennbetrag_eur: 12500, is_geschaeftsfuehrer: true, internal_role: '',
|
|
has_academic_background: false, ip_areas: '',
|
|
})
|
|
|
|
const totalNennbetrag = state.gesellschafter.reduce((s, g) => s + g.nennbetrag_eur, 0)
|
|
const target = state.capital.stammkapital_eur
|
|
|
|
const handleAdd = () => {
|
|
if (!form.name.trim()) return
|
|
const ip_areas = form.ip_areas
|
|
.split('\n').map(s => s.trim()).filter(Boolean)
|
|
addGesellschafter({
|
|
rolle: 'founder',
|
|
name: form.name,
|
|
geburtsdatum: form.geburtsdatum || undefined,
|
|
adresse: form.adresse,
|
|
email: form.email || undefined,
|
|
nennbetrag_eur: form.nennbetrag_eur,
|
|
is_geschaeftsfuehrer: form.is_geschaeftsfuehrer,
|
|
internal_role: form.internal_role || undefined,
|
|
has_academic_background: form.has_academic_background,
|
|
ip_areas: ip_areas.length > 0 ? ip_areas : undefined,
|
|
})
|
|
setForm({ name: '', geburtsdatum: '', adresse: '', email: '', nennbetrag_eur: 12500,
|
|
is_geschaeftsfuehrer: true, internal_role: '', has_academic_background: false, ip_areas: '' })
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h3 className="font-semibold mb-3">Neuen Gesellschafter hinzufügen</h3>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<input
|
|
data-testid="gs-name"
|
|
placeholder="Name"
|
|
value={form.name}
|
|
onChange={e => setForm({ ...form, name: e.target.value })}
|
|
className="px-3 py-2 border rounded"
|
|
/>
|
|
<input
|
|
data-testid="gs-birthdate"
|
|
type="date"
|
|
placeholder="Geburtsdatum"
|
|
value={form.geburtsdatum}
|
|
onChange={e => setForm({ ...form, geburtsdatum: e.target.value })}
|
|
className="px-3 py-2 border rounded"
|
|
/>
|
|
<input
|
|
data-testid="gs-address"
|
|
placeholder="Adresse (Straße, PLZ Ort)"
|
|
value={form.adresse}
|
|
onChange={e => setForm({ ...form, adresse: e.target.value })}
|
|
className="px-3 py-2 border rounded col-span-2"
|
|
/>
|
|
<input
|
|
data-testid="gs-email"
|
|
type="email"
|
|
placeholder="E-Mail (optional)"
|
|
value={form.email}
|
|
onChange={e => setForm({ ...form, email: e.target.value })}
|
|
className="px-3 py-2 border rounded"
|
|
/>
|
|
<input
|
|
data-testid="gs-nennbetrag"
|
|
type="number"
|
|
min={1}
|
|
step={1}
|
|
placeholder="Nennbetrag in EUR"
|
|
value={form.nennbetrag_eur}
|
|
onChange={e => setForm({ ...form, nennbetrag_eur: parseInt(e.target.value) || 0 })}
|
|
className="px-3 py-2 border rounded"
|
|
/>
|
|
<select
|
|
data-testid="gs-role"
|
|
value={form.internal_role}
|
|
onChange={e => setForm({ ...form, internal_role: e.target.value })}
|
|
className="px-3 py-2 border rounded bg-white"
|
|
>
|
|
<option value="">Rolle wählen…</option>
|
|
<option value="CEO">CEO (Chief Executive Officer)</option>
|
|
<option value="CTO">CTO (Chief Technical Officer)</option>
|
|
<option value="CFO">CFO (Chief Financial Officer)</option>
|
|
<option value="COO">COO (Chief Operating Officer)</option>
|
|
<option value="CPO">CPO (Chief Product Officer)</option>
|
|
<option value="Geschäftsführer">Geschäftsführer (ohne Spezialisierung)</option>
|
|
<option value="Gesellschafter">Gesellschafter (kein GF)</option>
|
|
<option value="Sonstige">Sonstige</option>
|
|
</select>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
data-testid="gs-is-gf"
|
|
checked={form.is_geschaeftsfuehrer}
|
|
onChange={e => setForm({ ...form, is_geschaeftsfuehrer: e.target.checked })}
|
|
/>
|
|
<label className="text-sm">Geschäftsführer/in</label>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
data-testid="gs-academic"
|
|
checked={form.has_academic_background}
|
|
onChange={e => setForm({ ...form, has_academic_background: e.target.checked })}
|
|
/>
|
|
<label className="text-sm">Akademischer Hintergrund</label>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
IP-Bereiche, die diese Person in die Gesellschaft einbringt
|
|
<span className="text-gray-400"> (optional, eine Zeile pro Bereich)</span>
|
|
</label>
|
|
<textarea
|
|
data-testid="gs-ip-areas"
|
|
value={form.ip_areas}
|
|
onChange={e => setForm({ ...form, ip_areas: e.target.value })}
|
|
rows={3}
|
|
placeholder={'z.B.\nCompliance-Engine (Quellcode + Architektur)\nRAG-Pipeline\nKonfigurationsdaten'}
|
|
className="w-full px-3 py-2 border rounded font-mono text-xs"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Bei mehreren Gründern wird pro Person ein eigener IP-Assignment-Vertrag generiert.
|
|
</p>
|
|
</div>
|
|
<button
|
|
data-testid="add-gesellschafter"
|
|
onClick={handleAdd}
|
|
disabled={!form.name.trim() || form.nennbetrag_eur < 1}
|
|
className="mt-3 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
Gesellschafter hinzufügen
|
|
</button>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="font-semibold mb-3">Gesellschafter ({state.gesellschafter.length})</h3>
|
|
{state.gesellschafter.length === 0 ? (
|
|
<p className="text-gray-500 text-sm">Noch keine Gesellschafter angelegt.</p>
|
|
) : (
|
|
<table className="w-full text-sm" data-testid="gs-table">
|
|
<thead className="bg-gray-100">
|
|
<tr>
|
|
<th className="px-3 py-2 text-left">Nr.</th>
|
|
<th className="px-3 py-2 text-left">Name</th>
|
|
<th className="px-3 py-2 text-left">Geburtsdatum</th>
|
|
<th className="px-3 py-2 text-right">Nennbetrag</th>
|
|
<th className="px-3 py-2 text-right">Anteil %</th>
|
|
<th className="px-3 py-2">GF?</th>
|
|
<th className="px-3 py-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{state.gesellschafter.map(g => (
|
|
<tr key={g.id} className="border-t" data-testid={`gs-row-${g.anteil_nr}`}>
|
|
<td className="px-3 py-2">{g.anteil_nr}</td>
|
|
<td className="px-3 py-2 font-medium">
|
|
{g.name}{g.internal_role ? ` (${g.internal_role})` : ''}
|
|
{g.ip_areas && g.ip_areas.length > 0 && (
|
|
<div className="text-xs text-gray-500 mt-0.5">
|
|
IP: {g.ip_areas.join(', ')}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-3 py-2">{g.geburtsdatum || '—'}</td>
|
|
<td className="px-3 py-2 text-right">{g.nennbetrag_eur.toLocaleString('de-DE')} €</td>
|
|
<td className="px-3 py-2 text-right">{((g.nennbetrag_eur / Math.max(target, 1)) * 100).toFixed(2)}%</td>
|
|
<td className="px-3 py-2 text-center">{g.is_geschaeftsfuehrer ? '✓' : '—'}</td>
|
|
<td className="px-3 py-2">
|
|
<button
|
|
onClick={() => removeGesellschafter(g.id)}
|
|
className="text-red-600 hover:underline text-xs"
|
|
>
|
|
Entfernen
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
<tr className="border-t-2 font-semibold bg-gray-50">
|
|
<td colSpan={3} className="px-3 py-2">Summe</td>
|
|
<td className="px-3 py-2 text-right" data-testid="gs-total">
|
|
{totalNennbetrag.toLocaleString('de-DE')} €
|
|
</td>
|
|
<td className="px-3 py-2 text-right">
|
|
{totalNennbetrag === target ? '100%' : `≠ ${target.toLocaleString('de-DE')} €`}
|
|
</td>
|
|
<td colSpan={2}></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
{totalNennbetrag !== target && state.gesellschafter.length > 0 && (
|
|
<p className="mt-2 text-sm text-orange-600">
|
|
⚠ Die Summe der Nennbeträge ({totalNennbetrag.toLocaleString('de-DE')} €)
|
|
entspricht nicht dem Stammkapital ({target.toLocaleString('de-DE')} €).
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|