feat(generator): "Generate-All" bulk mode for recommended documents
CI / detect-changes (push) Successful in 7s
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 / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Failing after 13s
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 2m19s
CI / test-go (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / detect-changes (push) Successful in 7s
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 / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Failing after 13s
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 2m19s
CI / test-go (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
Phase 2 of the workspace-cutover initiative: the Document Generator
gets a Bulk-Generate mode that produces every recommended document
in one click instead of forcing the user through 25+ per-template
clicks.
New: BulkGenerateModal.tsx (430 LOC)
- On open: POSTs current CompanyProfile + ComplianceScope answers
to /api/sdk/v1/compliance/recommend (Phase 1 endpoint)
- Matches each recommendation's document_type against allTemplates
- Shows tabular list: classification chip, title, document_type,
source citation; checkboxes pre-selected for required+recommended
(only where a template exists)
- On submit: sequentially renders each selected template using the
same pipeline as GeneratorSection (runRuleset → applyBlockRemoval
→ applyConditionalBlocks → placeholder replace), then POSTs
documents + version v1.0 draft
- Per-row progress: ⏳ generiere → ✓ erstellt / ✗ Fehler / —
übersprungen; final summary counts
page.tsx:
- Imports BulkGenerateModal
- Adds prominent "Empfohlene generieren →" CTA above the
RecommendedDocuments block
- Wires SDK state (companyProfile, complianceScope) into the modal
Profile mapper:
- CompanyProfile (camelCase): employeeCount, businessModel,
isDataProcessor → org_employee_count, org_business_model,
comp_has_processors
- ComplianceScope answers (questionId/value): pass through 1:1
since the rule system uses the same field names as the wizard
- compliance_depth_level pulled from decision.determinedLevel
End-to-end flow:
1. User completes CompanyProfile + ComplianceScope
2. Clicks "Empfohlene generieren →"
3. Reviews 25-30 prefilled checkboxes
4. Clicks "Generieren" — modal iterates, all docs land as drafts
in compliance_legal_documents + version v1.0
5. Phase 3 (next): document-library tab makes them findable
6. Phase 4 (next-next): workspace consumes these directly
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Bulk-Generate-Modal: ruft den Compliance-Recommend-Endpoint mit dem aktuellen
|
||||
* Profil/Scope-Stand, matched die empfohlenen Dokumenttypen gegen die geladenen
|
||||
* Templates, und rendert + speichert alle markierten Dokumente in einem Rutsch
|
||||
* (als compliance_legal_documents + version v1.0 draft).
|
||||
*
|
||||
* Verwendet die existierende Render-Pipeline aus GeneratorSection.tsx:
|
||||
* runRuleset -> applyBlockRemoval -> applyConditionalBlocks -> placeholder-Replace
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
applyBlockRemoval,
|
||||
applyConditionalBlocks,
|
||||
buildBoolContext,
|
||||
getDocType,
|
||||
runRuleset,
|
||||
} from '../ruleEngine'
|
||||
import { contextToPlaceholders, type TemplateContext } from '../contextBridge'
|
||||
import type { LegalTemplateResult } from '@/lib/sdk/types'
|
||||
import type { CompanyProfile } from '@/lib/sdk/types/company-profile'
|
||||
import type { ComplianceScopeState } from '@/lib/sdk/compliance-scope-types/state'
|
||||
|
||||
const RECOMMEND_ENDPOINT = '/api/sdk/v1/compliance/recommend'
|
||||
const DOC_ENDPOINT = '/api/sdk/v1/compliance/legal-documents/documents'
|
||||
const VERSION_ENDPOINT = '/api/sdk/v1/compliance/legal-documents/versions'
|
||||
|
||||
interface RecommendedItem {
|
||||
document_type: string
|
||||
title: string
|
||||
rule_id: string
|
||||
rule_key: string
|
||||
classification: 'required' | 'recommended' | 'optional'
|
||||
base_classification: 'required' | 'recommended' | 'optional'
|
||||
source_citation: string
|
||||
reason: string
|
||||
override_applied: boolean
|
||||
}
|
||||
|
||||
interface RecommendationResult {
|
||||
required: RecommendedItem[]
|
||||
recommended: RecommendedItem[]
|
||||
optional: RecommendedItem[]
|
||||
}
|
||||
|
||||
interface Row {
|
||||
item: RecommendedItem
|
||||
template: LegalTemplateResult | undefined
|
||||
selected: boolean
|
||||
state: 'idle' | 'generating' | 'done' | 'skipped' | 'error'
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
allTemplates: LegalTemplateResult[]
|
||||
context: TemplateContext
|
||||
extraPlaceholders: Record<string, string>
|
||||
enabledModules: string[]
|
||||
companyProfile: CompanyProfile | null
|
||||
complianceScope: ComplianceScopeState | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function BulkGenerateModal({
|
||||
allTemplates, context, extraPlaceholders, enabledModules,
|
||||
companyProfile, complianceScope, onClose,
|
||||
}: Props) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
const [rows, setRows] = useState<Row[]>([])
|
||||
const [running, setRunning] = useState(false)
|
||||
const [summary, setSummary] = useState<{ done: number; skipped: number; failed: number } | null>(null)
|
||||
|
||||
const recommendProfile = useMemo(
|
||||
() => buildRecommendProfile(companyProfile, complianceScope),
|
||||
[companyProfile, complianceScope],
|
||||
)
|
||||
|
||||
// Templates nach document_type indizieren — ein Document_type hat oft nur EIN Template
|
||||
const templatesByType = useMemo(() => {
|
||||
const map = new Map<string, LegalTemplateResult>()
|
||||
for (const t of allTemplates) {
|
||||
if (t.templateType && !map.has(t.templateType)) {
|
||||
map.set(t.templateType, t)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [allTemplates])
|
||||
|
||||
// Recommend abrufen sobald das Modal geöffnet ist
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
setLoadError(null)
|
||||
try {
|
||||
const res = await fetch(RECOMMEND_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
profile: recommendProfile,
|
||||
compliance_depth_level: recommendProfile.compliance_depth_level ?? 'L2',
|
||||
}),
|
||||
})
|
||||
if (!res.ok) throw new Error(`Recommend-API: ${res.status}`)
|
||||
const data = (await res.json()) as RecommendationResult
|
||||
if (cancelled) return
|
||||
const all: RecommendedItem[] = [...data.required, ...data.recommended, ...data.optional]
|
||||
const newRows: Row[] = all.map((item) => ({
|
||||
item,
|
||||
template: templatesByType.get(item.document_type),
|
||||
// Default: required + recommended sind aktiv, optional inaktiv,
|
||||
// und ohne Template generell deaktiviert
|
||||
selected: item.classification !== 'optional' && templatesByType.has(item.document_type),
|
||||
state: 'idle',
|
||||
}))
|
||||
setRows(newRows)
|
||||
} catch (e) {
|
||||
if (!cancelled) setLoadError((e as Error).message)
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => { cancelled = true }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const selectedCount = rows.filter((r) => r.selected && r.template).length
|
||||
const unmatchedCount = rows.filter((r) => !r.template).length
|
||||
|
||||
function toggle(i: number) {
|
||||
setRows((rs) => rs.map((r, idx) => idx === i ? { ...r, selected: !r.selected } : r))
|
||||
}
|
||||
|
||||
function setAll(selected: boolean) {
|
||||
setRows((rs) => rs.map((r) => r.template ? { ...r, selected } : r))
|
||||
}
|
||||
|
||||
async function runBulk() {
|
||||
setRunning(true)
|
||||
setSummary(null)
|
||||
let done = 0
|
||||
let failed = 0
|
||||
let skipped = 0
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i]
|
||||
if (!row.selected) continue
|
||||
if (!row.template) { skipped++; continue }
|
||||
|
||||
setRows((rs) => rs.map((r, idx) => idx === i ? { ...r, state: 'generating' } : r))
|
||||
try {
|
||||
const rendered = renderTemplate(row.template, context, extraPlaceholders, enabledModules)
|
||||
await saveDocAndVersion(row.template, rendered)
|
||||
setRows((rs) => rs.map((r, idx) => idx === i ? { ...r, state: 'done' } : r))
|
||||
done++
|
||||
} catch (e) {
|
||||
setRows((rs) => rs.map((r, idx) =>
|
||||
idx === i ? { ...r, state: 'error', errorMessage: (e as Error).message } : r,
|
||||
))
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
setSummary({ done, skipped, failed })
|
||||
setRunning(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={onClose}>
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-2xl w-[820px] max-h-[90vh] flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<header className="px-5 py-3 border-b border-gray-200 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-800">Alle empfohlenen Dokumente generieren</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
Compliance-Recommend wertet das aktuelle CompanyProfile + ComplianceScope aus
|
||||
und schlägt Vorlagen vor. Markierte werden client-seitig gerendert und als
|
||||
Drafts v1.0 in der Document-Library angelegt.
|
||||
</p>
|
||||
</div>
|
||||
<button className="text-gray-400 hover:text-gray-700 text-2xl" onClick={onClose}>×</button>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loading && (
|
||||
<div className="p-8 text-center text-sm text-gray-500">Lade Empfehlungen…</div>
|
||||
)}
|
||||
{loadError && (
|
||||
<div className="m-5 p-3 text-sm text-rose-800 bg-rose-50 border border-rose-200 rounded">
|
||||
{loadError}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !loadError && rows.length === 0 && (
|
||||
<div className="p-8 text-center text-sm text-gray-500">
|
||||
Keine Empfehlungen für dieses Profil.
|
||||
Stell sicher dass CompanyProfile + ComplianceScope ausgefüllt sind.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && rows.length > 0 && (
|
||||
<>
|
||||
<div className="px-5 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between text-xs">
|
||||
<div className="text-gray-600">
|
||||
<b>{selectedCount}</b> von {rows.length} ausgewählt
|
||||
{unmatchedCount > 0 && (
|
||||
<span className="ml-2 text-amber-700">
|
||||
({unmatchedCount} ohne Template — kann nicht generiert werden)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className="px-2 py-1 border border-gray-300 rounded hover:bg-white"
|
||||
onClick={() => setAll(true)}
|
||||
disabled={running}
|
||||
>
|
||||
Alle wählen
|
||||
</button>
|
||||
<button
|
||||
className="px-2 py-1 border border-gray-300 rounded hover:bg-white"
|
||||
onClick={() => setAll(false)}
|
||||
disabled={running}
|
||||
>
|
||||
Keine wählen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-100">
|
||||
{rows.map((row, i) => (
|
||||
<BulkRow key={row.item.rule_id} row={row} onToggle={() => toggle(i)} running={running} />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="px-5 py-3 border-t border-gray-200 bg-gray-50 flex items-center gap-3">
|
||||
{summary ? (
|
||||
<div className="text-sm text-gray-700">
|
||||
<b className="text-emerald-700">{summary.done} erstellt</b>
|
||||
{summary.skipped > 0 && <span className="ml-2 text-amber-700">· {summary.skipped} übersprungen</span>}
|
||||
{summary.failed > 0 && <span className="ml-2 text-rose-700">· {summary.failed} fehlgeschlagen</span>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500">
|
||||
Erzeugt {selectedCount} neue Drafts in der Document-Library.
|
||||
</div>
|
||||
)}
|
||||
<button className="ml-auto px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800" onClick={onClose}>
|
||||
Schließen
|
||||
</button>
|
||||
{!summary && (
|
||||
<button
|
||||
className="px-4 py-1.5 text-sm bg-emerald-600 text-white rounded hover:bg-emerald-700 disabled:opacity-50"
|
||||
disabled={running || loading || selectedCount === 0}
|
||||
onClick={runBulk}
|
||||
>
|
||||
{running ? 'Generiere…' : `${selectedCount} Dokumente generieren`}
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BulkRow({ row, onToggle, running }: { row: Row; onToggle: () => void; running: boolean }) {
|
||||
const hasTemplate = !!row.template
|
||||
const cls = row.item.classification
|
||||
|
||||
const stateBadge = (() => {
|
||||
switch (row.state) {
|
||||
case 'generating': return <span className="text-amber-700">⏳ generiere…</span>
|
||||
case 'done': return <span className="text-emerald-700">✓ erstellt</span>
|
||||
case 'error': return <span className="text-rose-700" title={row.errorMessage}>✗ Fehler</span>
|
||||
case 'skipped': return <span className="text-gray-500">— übersprungen</span>
|
||||
default: return null
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<li className="px-5 py-2 flex items-start gap-3 hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1"
|
||||
checked={row.selected}
|
||||
onChange={onToggle}
|
||||
disabled={!hasTemplate || running}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<ClassChip classification={cls} />
|
||||
<span className="text-sm font-medium text-gray-800">{row.item.title}</span>
|
||||
{!hasTemplate && (
|
||||
<span className="px-1.5 py-0.5 text-xs rounded border bg-amber-50 text-amber-800 border-amber-300">
|
||||
kein Template
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-auto text-xs">{stateBadge}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
<code>{row.item.document_type}</code>
|
||||
{row.item.source_citation && <> · {row.item.source_citation}</>}
|
||||
</div>
|
||||
{row.errorMessage && (
|
||||
<div className="text-xs text-rose-700 mt-0.5">{row.errorMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function ClassChip({ classification }: { classification: 'required' | 'recommended' | 'optional' }) {
|
||||
const map = {
|
||||
required: { label: 'Pflicht', cls: 'bg-rose-100 text-rose-800 border-rose-300' },
|
||||
recommended: { label: 'Empfohlen', cls: 'bg-amber-100 text-amber-800 border-amber-300' },
|
||||
optional: { label: 'Optional', cls: 'bg-slate-100 text-slate-700 border-slate-300' },
|
||||
}[classification]
|
||||
return (
|
||||
<span className={`px-1.5 py-0.5 text-xs rounded border ${map.cls}`}>{map.label}</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ----- Render-Pipeline (Kopie aus GeneratorSection mit gleicher Logik) -----
|
||||
|
||||
function renderTemplate(
|
||||
template: LegalTemplateResult,
|
||||
context: TemplateContext,
|
||||
extraPlaceholders: Record<string, string>,
|
||||
enabledModules: string[],
|
||||
): string {
|
||||
const ruleResult = runRuleset({
|
||||
doc_type: getDocType(template.templateType ?? '', template.language ?? 'de'),
|
||||
render: { lang: template.language ?? 'de', variant: 'standard' },
|
||||
context,
|
||||
modules: { enabled: enabledModules },
|
||||
})
|
||||
const allValues = {
|
||||
...contextToPlaceholders(ruleResult?.contextAfterDefaults ?? context),
|
||||
...extraPlaceholders,
|
||||
}
|
||||
const boolCtx = ruleResult
|
||||
? buildBoolContext(ruleResult.contextAfterDefaults, ruleResult.computedFlags)
|
||||
: {}
|
||||
let content = applyBlockRemoval(template.text, ruleResult?.removedBlocks ?? [])
|
||||
content = applyConditionalBlocks(content, boolCtx)
|
||||
for (const [key, value] of Object.entries(allValues)) {
|
||||
if (value) {
|
||||
content = content.replace(
|
||||
new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
|
||||
value,
|
||||
)
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
async function saveDocAndVersion(
|
||||
template: LegalTemplateResult,
|
||||
renderedContent: string,
|
||||
): Promise<void> {
|
||||
const docRes = await fetch(DOC_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: template.templateType || 'custom',
|
||||
name: template.documentTitle || 'Dokument',
|
||||
description: `Bulk-generiert aus Template ${template.templateType}`,
|
||||
}),
|
||||
})
|
||||
if (!docRes.ok) {
|
||||
throw new Error(`Document anlegen fehlgeschlagen: ${docRes.status} ${await docRes.text().catch(() => '')}`)
|
||||
}
|
||||
const doc = await docRes.json()
|
||||
const verRes = await fetch(VERSION_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
document_id: doc.id,
|
||||
title: template.documentTitle || 'Dokument',
|
||||
content: renderedContent,
|
||||
language: template.language || 'de',
|
||||
version: '1.0',
|
||||
}),
|
||||
})
|
||||
if (!verRes.ok) {
|
||||
throw new Error(`Version anlegen fehlgeschlagen: ${verRes.status} ${await verRes.text().catch(() => '')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Profile-Builder: SDK-State → /recommend Body -----
|
||||
|
||||
function buildRecommendProfile(
|
||||
companyProfile: CompanyProfile | null,
|
||||
complianceScope: ComplianceScopeState | null,
|
||||
): Record<string, unknown> {
|
||||
const profile: Record<string, unknown> = {}
|
||||
|
||||
// Aus CompanyProfile (camelCase TS-Modell)
|
||||
if (companyProfile) {
|
||||
if (companyProfile.employeeCount) {
|
||||
profile.org_employee_count = String(companyProfile.employeeCount).replace(/-/g, '_')
|
||||
}
|
||||
if (companyProfile.businessModel) {
|
||||
profile.org_business_model = String(companyProfile.businessModel).toLowerCase().replace(/\s+/g, '_')
|
||||
}
|
||||
if (companyProfile.isDataProcessor) {
|
||||
profile.comp_has_processors = 'yes'
|
||||
}
|
||||
}
|
||||
|
||||
// ComplianceScope-Antworten: questionId entspricht direkt unserer Profil-
|
||||
// Feld-Konvention (proc_ai_usage, tech_third_country, prod_webshop, etc.)
|
||||
if (complianceScope?.answers) {
|
||||
for (const a of complianceScope.answers) {
|
||||
if (!a.questionId) continue
|
||||
if (a.value === null || a.value === undefined || a.value === '') continue
|
||||
profile[a.questionId] = a.value
|
||||
}
|
||||
}
|
||||
|
||||
if (complianceScope?.decision?.determinedLevel) {
|
||||
profile.compliance_depth_level = complianceScope.decision.determinedLevel
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import TemplateLibrary from './_components/TemplateLibrary'
|
||||
import GeneratorSection from './_components/GeneratorSection'
|
||||
import RecommendedDocuments from './_components/RecommendedDocuments'
|
||||
import { LifecycleFilter, filterTemplatesByStage } from './_components/LifecycleFilter'
|
||||
import BulkGenerateModal from './_components/BulkGenerateModal'
|
||||
import type { LifecycleStage } from '@/lib/sdk/founding/template-categories'
|
||||
|
||||
function DocumentGeneratorPageInner() {
|
||||
@@ -39,6 +40,7 @@ function DocumentGeneratorPageInner() {
|
||||
const generatorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [totalCount, setTotalCount] = useState<number>(0)
|
||||
const [showBulkGenerate, setShowBulkGenerate] = useState(false)
|
||||
|
||||
// Load all templates on mount
|
||||
useEffect(() => {
|
||||
@@ -332,6 +334,23 @@ function DocumentGeneratorPageInner() {
|
||||
countsByStage={countsByStage}
|
||||
/>
|
||||
|
||||
{/* Bulk-Generate-Knopf — alle empfohlenen Dokumente in einem Rutsch */}
|
||||
<div className="flex items-center justify-between bg-emerald-50 border border-emerald-200 rounded p-3">
|
||||
<div className="text-sm text-gray-700">
|
||||
<b>Alle empfohlenen Dokumente in einem Rutsch generieren.</b>
|
||||
<div className="text-xs text-gray-600 mt-0.5">
|
||||
Profil + Scope-Antworten werden gegen die Empfehlungs-Regeln ausgewertet —
|
||||
markierte Templates werden als Drafts v1.0 in die Document-Library angelegt.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="px-4 py-2 text-sm bg-emerald-600 text-white rounded hover:bg-emerald-700 whitespace-nowrap"
|
||||
onClick={() => setShowBulkGenerate(true)}
|
||||
>
|
||||
Empfohlene generieren →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Recommended documents based on scope profile */}
|
||||
<RecommendedDocuments
|
||||
allTemplates={allTemplates}
|
||||
@@ -391,6 +410,18 @@ function DocumentGeneratorPageInner() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBulkGenerate && (
|
||||
<BulkGenerateModal
|
||||
allTemplates={allTemplates}
|
||||
context={context}
|
||||
extraPlaceholders={extraPlaceholders}
|
||||
enabledModules={enabledModules}
|
||||
companyProfile={state.companyProfile ?? null}
|
||||
complianceScope={state.complianceScope ?? null}
|
||||
onClose={() => setShowBulkGenerate(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user