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
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:
@@ -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: '0–3 Jahre, <25 Mitarbeiter — Arbeitsverträge, AVV, Datenschutz',
|
||||||
|
kmu: '3+ Jahre, 25–250 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 TemplateLibrary from './_components/TemplateLibrary'
|
||||||
import GeneratorSection from './_components/GeneratorSection'
|
import GeneratorSection from './_components/GeneratorSection'
|
||||||
import RecommendedDocuments from './_components/RecommendedDocuments'
|
import RecommendedDocuments from './_components/RecommendedDocuments'
|
||||||
|
import { LifecycleFilter, filterTemplatesByStage } from './_components/LifecycleFilter'
|
||||||
|
import type { LifecycleStage } from '@/lib/sdk/founding/template-categories'
|
||||||
|
|
||||||
function DocumentGeneratorPageInner() {
|
function DocumentGeneratorPageInner() {
|
||||||
const { state } = useSDK()
|
const { state } = useSDK()
|
||||||
@@ -24,6 +26,7 @@ function DocumentGeneratorPageInner() {
|
|||||||
const [allTemplates, setAllTemplates] = useState<LegalTemplateResult[]>([])
|
const [allTemplates, setAllTemplates] = useState<LegalTemplateResult[]>([])
|
||||||
const [isLoadingLibrary, setIsLoadingLibrary] = useState(true)
|
const [isLoadingLibrary, setIsLoadingLibrary] = useState(true)
|
||||||
const [activeCategory, setActiveCategory] = useState<string>('all')
|
const [activeCategory, setActiveCategory] = useState<string>('all')
|
||||||
|
const [activeStage, setActiveStage] = useState<LifecycleStage | 'all'>('all')
|
||||||
const [activeLanguage, setActiveLanguage] = useState<'all' | 'de' | 'en'>('all')
|
const [activeLanguage, setActiveLanguage] = useState<'all' | 'de' | 'en'>('all')
|
||||||
const [librarySearch, setLibrarySearch] = useState('')
|
const [librarySearch, setLibrarySearch] = useState('')
|
||||||
const [expandedPreviewId, setExpandedPreviewId] = useState<string | null>(null)
|
const [expandedPreviewId, setExpandedPreviewId] = useState<string | null>(null)
|
||||||
@@ -209,10 +212,15 @@ function DocumentGeneratorPageInner() {
|
|||||||
}
|
}
|
||||||
}, [selectedDataPointsData])
|
}, [selectedDataPointsData])
|
||||||
|
|
||||||
// Filtered templates (computed)
|
// Filtered templates (computed) — Lifecycle + Category + Language + Search
|
||||||
const filteredTemplates = useMemo(() => {
|
const filteredTemplates = useMemo(() => {
|
||||||
const category = CATEGORIES.find((c: { key: string }) => c.key === activeCategory)
|
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 && category.types !== null) {
|
||||||
if (!category.types.includes(t.templateType || '')) return false
|
if (!category.types.includes(t.templateType || '')) return false
|
||||||
}
|
}
|
||||||
@@ -225,7 +233,22 @@ function DocumentGeneratorPageInner() {
|
|||||||
}
|
}
|
||||||
return true
|
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) => {
|
const handleUseTemplate = useCallback((t: LegalTemplateResult) => {
|
||||||
setActiveTemplate(t)
|
setActiveTemplate(t)
|
||||||
@@ -292,6 +315,13 @@ function DocumentGeneratorPageInner() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Lifecycle-Phase Filter */}
|
||||||
|
<LifecycleFilter
|
||||||
|
activeStage={activeStage}
|
||||||
|
onChange={setActiveStage}
|
||||||
|
countsByStage={countsByStage}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Recommended documents based on scope profile */}
|
{/* Recommended documents based on scope profile */}
|
||||||
<RecommendedDocuments
|
<RecommendedDocuments
|
||||||
allTemplates={allTemplates}
|
allTemplates={allTemplates}
|
||||||
|
|||||||
@@ -51,3 +51,4 @@ redis==5.2.1
|
|||||||
idna>=3.7
|
idna>=3.7
|
||||||
cryptography>=42.0.0
|
cryptography>=42.0.0
|
||||||
pillow>=12.1.1
|
pillow>=12.1.1
|
||||||
|
python-docx==1.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user