fix(founding-wizard): add python-docx dep + Lifecycle filter UI
CI / detect-changes (push) Successful in 10s
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 17s
CI / loc-budget (push) Successful in 18s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m53s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 44s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

- requirements.txt: python-docx==1.2.0 (Container hatte das modul nicht)
- document-generator: Lifecycle-Filter (Pre-Founding/Founding/Startup/KMU/Konzern)
  zeigt nur relevante Templates fuer aktuelle Phase
This commit is contained in:
Benjamin Admin
2026-05-20 16:41:36 +02:00
parent 4478b7f479
commit 6f3301d246
3 changed files with 158 additions and 3 deletions
@@ -0,0 +1,124 @@
'use client'
/**
* Lifecycle-Phasen-Filter für den Document-Generator.
*
* Zeigt 5 Phasen-Tabs (Pre-Founding, Founding, Startup, KMU, Konzern) und
* filtert die angezeigten Templates entsprechend ihres `lifecycle_stage`-Arrays.
*
* Phasen-Definitionen synchron zu lib/sdk/founding/template-categories.ts
*/
import {
LIFECYCLE_STAGE_LABELS,
type LifecycleStage,
TEMPLATE_CATEGORIES,
} from '@/lib/sdk/founding/template-categories'
interface Props {
activeStage: LifecycleStage | 'all'
onChange: (stage: LifecycleStage | 'all') => void
/** Template-Counts pro Stage (optional, sonst aus Code-Registry berechnet) */
countsByStage?: Record<string, number>
}
const STAGE_ORDER: (LifecycleStage | 'all')[] = [
'all',
'pre_founding',
'founding',
'startup',
'kmu',
'konzern',
]
const STAGE_ICONS: Record<LifecycleStage | 'all', string> = {
all: '📚',
pre_founding: '🌱',
founding: '⚖️',
startup: '🚀',
kmu: '🏢',
konzern: '🏛️',
}
const STAGE_HINTS: Record<LifecycleStage, string> = {
pre_founding: 'Vor dem Notartermin — Term Sheet, IP-Sicherung, Wandeldarlehen',
founding: 'Für den Notartermin — Satzung, Gesellschafterliste, HRB-Anmeldung',
startup: '03 Jahre, <25 Mitarbeiter — Arbeitsverträge, AVV, Datenschutz',
kmu: '3+ Jahre, 25250 MA — ISMS, Whistleblower, vollständige TOM',
konzern: '250+ MA — Konzern-Compliance, ISO 27001',
}
export function LifecycleFilter({ activeStage, onChange, countsByStage }: Props) {
const counts = countsByStage || computeCountsFromRegistry()
return (
<div className="mb-6" data-testid="lifecycle-filter">
<div className="flex items-center gap-2 mb-2">
<h3 className="text-sm font-semibold text-gray-700">Phase Deines Unternehmens</h3>
<span className="text-xs text-gray-500"> filtert Dokumente nach Lifecycle</span>
</div>
<div className="flex flex-wrap gap-2">
{STAGE_ORDER.map(stage => {
const isAll = stage === 'all'
const count = isAll
? Object.values(counts).reduce((s, c) => s + c, 0)
: (counts[stage] || 0)
const label = isAll ? 'Alle' : LIFECYCLE_STAGE_LABELS[stage as LifecycleStage].split(' (')[0]
const isActive = activeStage === stage
return (
<button
key={stage}
type="button"
data-testid={`stage-tab-${stage}`}
onClick={() => onChange(stage)}
className={`px-3 py-2 rounded-lg border text-sm font-medium transition ${
isActive
? 'bg-purple-600 text-white border-purple-600 shadow-sm'
: 'bg-white text-gray-700 border-gray-200 hover:border-purple-300 hover:bg-purple-50'
}`}
>
<span className="mr-1.5">{STAGE_ICONS[stage]}</span>
{label}
<span className={`ml-2 px-1.5 py-0.5 text-xs rounded-full ${
isActive ? 'bg-white/20' : 'bg-gray-100 text-gray-600'
}`}>
{count}
</span>
</button>
)
})}
</div>
{activeStage !== 'all' && (
<p className="mt-2 text-sm text-gray-500" data-testid="stage-hint">
{STAGE_HINTS[activeStage as LifecycleStage]}
</p>
)}
</div>
)
}
function computeCountsFromRegistry(): Record<string, number> {
const counts: Record<string, number> = {
pre_founding: 0, founding: 0, startup: 0, kmu: 0, konzern: 0,
}
for (const cat of Object.values(TEMPLATE_CATEGORIES)) {
for (const stage of cat.lifecycle_stage) {
counts[stage] = (counts[stage] || 0) + 1
}
}
return counts
}
export function filterTemplatesByStage<T extends { document_type?: string; type?: string }>(
templates: T[],
stage: LifecycleStage | 'all'
): T[] {
if (stage === 'all') return templates
return templates.filter(t => {
const docType = t.document_type || t.type
if (!docType) return false
const cat = TEMPLATE_CATEGORIES[docType]
if (!cat) return stage === 'startup' // Fallback: unkategorisierte zeigen wir in Startup
return cat.lifecycle_stage.includes(stage)
})
}
@@ -15,6 +15,8 @@ import { getGeneratorDefaults, getProfileLabel } from './scopeDefaults'
import TemplateLibrary from './_components/TemplateLibrary'
import GeneratorSection from './_components/GeneratorSection'
import RecommendedDocuments from './_components/RecommendedDocuments'
import { LifecycleFilter, filterTemplatesByStage } from './_components/LifecycleFilter'
import type { LifecycleStage } from '@/lib/sdk/founding/template-categories'
function DocumentGeneratorPageInner() {
const { state } = useSDK()
@@ -24,6 +26,7 @@ function DocumentGeneratorPageInner() {
const [allTemplates, setAllTemplates] = useState<LegalTemplateResult[]>([])
const [isLoadingLibrary, setIsLoadingLibrary] = useState(true)
const [activeCategory, setActiveCategory] = useState<string>('all')
const [activeStage, setActiveStage] = useState<LifecycleStage | 'all'>('all')
const [activeLanguage, setActiveLanguage] = useState<'all' | 'de' | 'en'>('all')
const [librarySearch, setLibrarySearch] = useState('')
const [expandedPreviewId, setExpandedPreviewId] = useState<string | null>(null)
@@ -209,10 +212,15 @@ function DocumentGeneratorPageInner() {
}
}, [selectedDataPointsData])
// Filtered templates (computed)
// Filtered templates (computed) — Lifecycle + Category + Language + Search
const filteredTemplates = useMemo(() => {
const category = CATEGORIES.find((c: { key: string }) => c.key === activeCategory)
return allTemplates.filter((t) => {
// 1. Lifecycle-Phase Filter via Code-Registry (mapped auf templateType)
const stageFiltered = filterTemplatesByStage(
allTemplates.map(t => ({ ...t, document_type: t.templateType || '' })),
activeStage
)
return stageFiltered.filter((t) => {
if (category && category.types !== null) {
if (!category.types.includes(t.templateType || '')) return false
}
@@ -225,7 +233,22 @@ function DocumentGeneratorPageInner() {
}
return true
})
}, [allTemplates, activeCategory, activeLanguage, librarySearch])
}, [allTemplates, activeCategory, activeStage, activeLanguage, librarySearch])
// Counts by stage for filter UI
const countsByStage = useMemo(() => {
const counts: Record<string, number> = { pre_founding: 0, founding: 0, startup: 0, kmu: 0, konzern: 0 }
const stages: LifecycleStage[] = ['pre_founding', 'founding', 'startup', 'kmu', 'konzern']
for (const t of allTemplates) {
const docType = t.templateType || ''
for (const s of stages) {
if (filterTemplatesByStage([{ document_type: docType }], s).length) {
counts[s]++
}
}
}
return counts
}, [allTemplates])
const handleUseTemplate = useCallback((t: LegalTemplateResult) => {
setActiveTemplate(t)
@@ -292,6 +315,13 @@ function DocumentGeneratorPageInner() {
</div>
</div>
{/* Lifecycle-Phase Filter */}
<LifecycleFilter
activeStage={activeStage}
onChange={setActiveStage}
countsByStage={countsByStage}
/>
{/* Recommended documents based on scope profile */}
<RecommendedDocuments
allTemplates={allTemplates}
+1
View File
@@ -51,3 +51,4 @@ redis==5.2.1
idna>=3.7
cryptography>=42.0.0
pillow>=12.1.1
python-docx==1.2.0