feat(vvt): Aufklappbare Abteilungskacheln mit Datenkategorien + Wiki-Infoboxen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 23s

Step 2 im VVT-Generator: Ja/Nein-Buttons durch expandierbare Kacheln ersetzt.
Pro Abteilung werden typische Datenkategorien als Checkboxen angezeigt (isTypical
vorausgefuellt), Art. 9 Kategorien orange hervorgehoben mit DSGVO-Warnung.
7 neue Wiki-Artikel fuer Datenkategorien pro Geschaeftsbereich (HR, Finanzen,
Vertrieb, Marketing, Support, IT, Produktion).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-10 13:11:00 +01:00
parent 3512963006
commit e3fb81fc0d
3 changed files with 768 additions and 48 deletions

View File

@@ -34,6 +34,7 @@ import type { VVTActivity, VVTOrganizationHeader, BusinessFunction } from '@/lib
import {
PROFILING_STEPS,
PROFILING_QUESTIONS,
DEPARTMENT_DATA_CATEGORIES,
getQuestionsForStep,
getStepProgress,
getTotalProgress,
@@ -1100,61 +1101,187 @@ function TabGenerator({
<p className="text-sm text-gray-500 mt-1">{currentStepInfo?.description}</p>
<div className="mt-6 space-y-6">
{questions.map(q => (
<div key={q.id}>
<label className="block text-sm font-medium text-gray-700 mb-2">{q.question}</label>
{q.helpText && <p className="text-xs text-gray-400 mb-2">{q.helpText}</p>}
{questions.map(q => {
const deptConfig = DEPARTMENT_DATA_CATEGORIES[q.id]
const isDeptQuestion = q.type === 'boolean' && q.step === 2 && deptConfig
const isActive = answers[q.id] === true
const categoriesKey = `${q.id}_categories`
const selectedCategories = (answers[categoriesKey] as string[] | undefined) || []
{q.type === 'boolean' && (
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" name={q.id} checked={answers[q.id] === true}
onChange={() => setAnswers({ ...answers, [q.id]: true })}
className="w-4 h-4 text-purple-600" />
<span className="text-sm">Ja</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" name={q.id} checked={answers[q.id] === false}
onChange={() => setAnswers({ ...answers, [q.id]: false })}
className="w-4 h-4 text-purple-600" />
<span className="text-sm">Nein</span>
</label>
// Initialize typical categories when dept is activated
const handleDeptToggle = (value: boolean) => {
const updated = { ...answers, [q.id]: value }
if (value && deptConfig && !answers[categoriesKey]) {
// Prefill typical categories
updated[categoriesKey] = deptConfig.categories
.filter(c => c.isTypical)
.map(c => c.id)
}
setAnswers(updated)
}
const handleCategoryToggle = (catId: string) => {
const current = (answers[categoriesKey] as string[] | undefined) || []
const updated = current.includes(catId)
? current.filter(id => id !== catId)
: [...current, catId]
setAnswers({ ...answers, [categoriesKey]: updated })
}
// Expandable department tile (Step 2)
if (isDeptQuestion) {
const hasArt9Selected = deptConfig.categories
.filter(c => c.isArt9)
.some(c => selectedCategories.includes(c.id))
return (
<div key={q.id} className={`border rounded-xl overflow-hidden transition-all ${
isActive ? 'border-purple-400 bg-white shadow-sm' : 'border-gray-200 bg-white'
}`}>
{/* Header row with Ja/Nein */}
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<span className="text-xl">{deptConfig.icon}</span>
<div>
<span className="text-sm font-medium text-gray-900">{deptConfig.label}</span>
{q.helpText && <p className="text-xs text-gray-400">{q.helpText}</p>}
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => handleDeptToggle(true)}
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
isActive ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
Ja
</button>
<button
type="button"
onClick={() => handleDeptToggle(false)}
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
answers[q.id] === false ? 'bg-gray-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
Nein
</button>
</div>
</div>
{/* Expandable categories panel */}
{isActive && (
<div className="border-t border-gray-100 px-4 pt-3 pb-4">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
Typische Datenkategorien
</p>
<div className="space-y-1.5">
{deptConfig.categories.map(cat => {
const isChecked = selectedCategories.includes(cat.id)
return (
<label
key={cat.id}
className={`flex items-start gap-3 p-2.5 rounded-lg cursor-pointer transition-colors ${
cat.isArt9
? isChecked ? 'bg-orange-50 hover:bg-orange-100' : 'bg-gray-50 hover:bg-orange-50'
: isChecked ? 'bg-purple-50 hover:bg-purple-100' : 'bg-gray-50 hover:bg-gray-100'
}`}
>
<input
type="checkbox"
checked={isChecked}
onChange={() => handleCategoryToggle(cat.id)}
className={`w-4 h-4 mt-0.5 rounded ${cat.isArt9 ? 'text-orange-500' : 'text-purple-600'}`}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">{cat.label}</span>
{cat.isArt9 && (
<span className="px-1.5 py-0.5 text-[10px] font-semibold bg-orange-100 text-orange-700 rounded">
Art. 9
</span>
)}
</div>
<p className="text-xs text-gray-500 mt-0.5">{cat.info}</p>
</div>
</label>
)
})}
</div>
{/* Art. 9 warning */}
{hasArt9Selected && (
<div className="mt-3 p-3 bg-orange-50 border border-orange-200 rounded-lg">
<p className="text-xs text-orange-800">
<span className="font-semibold">Art. 9 DSGVO:</span> Sie verarbeiten besondere Kategorien
personenbezogener Daten. Eine zusaetzliche Rechtsgrundlage nach Art. 9 Abs. 2 DSGVO ist
erforderlich (z.B. § 26 Abs. 3 BDSG fuer Beschaeftigtendaten).
</p>
</div>
)}
</div>
)}
</div>
)}
)
}
{q.type === 'single_choice' && q.options && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{q.options.map(opt => (
<label
key={opt.value}
className={`flex items-center gap-2 p-3 border rounded-lg cursor-pointer transition-colors ${
answers[q.id] === opt.value ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'
}`}
>
<input type="radio" name={q.id} value={opt.value}
checked={answers[q.id] === opt.value}
onChange={() => setAnswers({ ...answers, [q.id]: opt.value })}
// Standard rendering for non-department questions
return (
<div key={q.id}>
<label className="block text-sm font-medium text-gray-700 mb-2">{q.question}</label>
{q.helpText && <p className="text-xs text-gray-400 mb-2">{q.helpText}</p>}
{q.type === 'boolean' && (
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" name={q.id} checked={answers[q.id] === true}
onChange={() => setAnswers({ ...answers, [q.id]: true })}
className="w-4 h-4 text-purple-600" />
<span className="text-sm">{opt.label}</span>
<span className="text-sm">Ja</span>
</label>
))}
</div>
)}
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" name={q.id} checked={answers[q.id] === false}
onChange={() => setAnswers({ ...answers, [q.id]: false })}
className="w-4 h-4 text-purple-600" />
<span className="text-sm">Nein</span>
</label>
</div>
)}
{q.type === 'number' && (
<input type="number" value={typeof answers[q.id] === 'number' ? answers[q.id] as number : ''}
onChange={(e) => setAnswers({ ...answers, [q.id]: parseInt(e.target.value) || 0 })}
className="w-40 px-3 py-2 border border-gray-300 rounded-lg"
placeholder="Anzahl" />
)}
{q.type === 'single_choice' && q.options && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{q.options.map(opt => (
<label
key={opt.value}
className={`flex items-center gap-2 p-3 border rounded-lg cursor-pointer transition-colors ${
answers[q.id] === opt.value ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'
}`}
>
<input type="radio" name={q.id} value={opt.value}
checked={answers[q.id] === opt.value}
onChange={() => setAnswers({ ...answers, [q.id]: opt.value })}
className="w-4 h-4 text-purple-600" />
<span className="text-sm">{opt.label}</span>
</label>
))}
</div>
)}
{q.type === 'text' && (
<input type="text" value={(answers[q.id] as string) || ''}
onChange={(e) => setAnswers({ ...answers, [q.id]: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg" />
)}
</div>
))}
{q.type === 'number' && (
<input type="number" value={typeof answers[q.id] === 'number' ? answers[q.id] as number : ''}
onChange={(e) => setAnswers({ ...answers, [q.id]: parseInt(e.target.value) || 0 })}
className="w-40 px-3 py-2 border border-gray-300 rounded-lg"
placeholder="Anzahl" />
)}
{q.type === 'text' && (
<input type="text" value={(answers[q.id] as string) || ''}
onChange={(e) => setAnswers({ ...answers, [q.id]: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg" />
)}
</div>
)
})}
</div>
</div>