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>

View File

@@ -285,6 +285,103 @@ export const PROFILING_QUESTIONS: ProfilingQuestion[] = [
},
]
// =============================================================================
// DEPARTMENT DATA CATEGORIES (Aufklappbare Kacheln Step 2)
// =============================================================================
export interface DepartmentCategory {
id: string
label: string
info: string
isArt9?: boolean
isTypical?: boolean
}
export interface DepartmentDataConfig {
label: string
icon: string
categories: DepartmentCategory[]
}
export const DEPARTMENT_DATA_CATEGORIES: Record<string, DepartmentDataConfig> = {
dept_hr: {
label: 'Personal (HR)',
icon: '👥',
categories: [
{ id: 'NAME', label: 'Stammdaten', info: 'Vor-/Nachname, Titel, Geschlecht, Geburtsdatum', isTypical: true },
{ id: 'ADDRESS', label: 'Adressdaten', info: 'Wohn-/Melde-/Lieferadresse, Telefon, E-Mail', isTypical: true },
{ id: 'SOCIAL_SECURITY', label: 'Sozialversicherungsnr.', info: 'SV-Nummer fuer Meldungen an DRV, Krankenkasse', isTypical: true },
{ id: 'TAX_ID', label: 'Steuer-ID', info: 'Steueridentifikationsnummer, Steuerklasse, Freibetraege', isTypical: true },
{ id: 'BANK_ACCOUNT', label: 'Bankverbindung', info: 'IBAN, BIC fuer Gehaltsueberweisungen', isTypical: true },
{ id: 'SALARY_DATA', label: 'Gehaltsdaten', info: 'Bruttogehalt, Zulagen, Praemien, VWL, Abzuege', isTypical: true },
{ id: 'EMPLOYMENT_DATA', label: 'Beschaeftigungsdaten', info: 'Vertrag, Eintrittsdatum, Abteilung, Position, Arbeitszeitmodell', isTypical: true },
{ id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Krankheitstage (AU-Bescheinigungen), BEM-Daten, Schwerbehinderung', isArt9: true },
{ id: 'RELIGIOUS_BELIEFS', label: 'Religionszugehoerigkeit', info: 'Konfession fuer Kirchensteuer-Abfuehrung', isArt9: true },
{ id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Zertifikate, Weiterbildungen, Schulungsnachweise' },
{ id: 'PHOTO_VIDEO', label: 'Mitarbeiterfotos', info: 'Passbilder fuer Ausweise, Intranet-Profilbilder' },
]
},
dept_recruiting: {
label: 'Recruiting / Bewerbermanagement',
icon: '📋',
categories: [
{ id: 'NAME', label: 'Bewerberstammdaten', info: 'Name, Anschrift, Kontaktdaten der Bewerber', isTypical: true },
{ id: 'APPLICATION_DATA', label: 'Bewerbungsunterlagen', info: 'Lebenslauf, Anschreiben, Zeugnisse, Zertifikate', isTypical: true },
{ id: 'EDUCATION_DATA', label: 'Qualifikationen', info: 'Abschluesse, Berufserfahrung, Sprachkenntnisse', isTypical: true },
{ id: 'ASSESSMENT_DATA', label: 'Bewertungsdaten', info: 'Interviewnotizen, Assessment-Ergebnisse, Eignungstests' },
{ id: 'HEALTH_DATA', label: 'Gesundheitsdaten', info: 'Schwerbehinderung (freiwillige Angabe), Eignungsuntersuchung', isArt9: true },
{ id: 'PHOTO_VIDEO', label: 'Bewerbungsfotos', info: 'Bewerbungsfoto (freiwillig), Video-Interview-Aufnahmen' },
]
},
dept_finance: {
label: 'Finanzen & Buchhaltung',
icon: '💰',
categories: [
{ id: 'NAME', label: 'Kunden-/Lieferantenstammdaten', info: 'Firmenname, Ansprechpartner, Kontaktdaten', isTypical: true },
{ id: 'ADDRESS', label: 'Rechnungsadressen', info: 'Rechnungs-/Lieferadressen, USt-IdNr.', isTypical: true },
{ id: 'BANK_ACCOUNT', label: 'Bankverbindungen', info: 'IBAN, BIC, SEPA-Mandate, Zahlungsbedingungen', isTypical: true },
{ id: 'TAX_ID', label: 'Steuer-IDs', info: 'Steuernummer, USt-IdNr., Steueridentifikationsnr.', isTypical: true },
{ id: 'INVOICE_DATA', label: 'Rechnungsdaten', info: 'Rechnungen, Gutschriften, Mahnungen, Zahlungshistorie', isTypical: true },
{ id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Vertragskonditionen, Laufzeiten, Kuendigungsfristen' },
]
},
dept_sales: {
label: 'Vertrieb & CRM',
icon: '🤝',
categories: [
{ id: 'NAME', label: 'Kontaktdaten', info: 'Name, E-Mail, Telefon, Position der Ansprechpartner', isTypical: true },
{ id: 'ADDRESS', label: 'Firmenadresse', info: 'Firmenanschrift, Standorte', isTypical: true },
{ id: 'CRM_DATA', label: 'CRM-Daten', info: 'Lead-Status, Opportunities, Sales-Pipeline, Umsatzhistorie', isTypical: true },
{ id: 'COMMUNICATION_DATA', label: 'Kommunikation', info: 'E-Mail-Verlauf, Gespraechsnotizen, Meeting-Protokolle', isTypical: true },
{ id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Angebote, Bestellungen, Rahmenvertraege' },
{ id: 'PREFERENCE_DATA', label: 'Praeferenzen', info: 'Produktinteressen, Kaufhistorie, Kundensegmentierung' },
]
},
dept_marketing: {
label: 'Marketing',
icon: '📢',
categories: [
{ id: 'EMAIL', label: 'E-Mail-Adressen', info: 'Newsletter-Abonnenten, Kampagnen-Empfaenger', isTypical: true },
{ id: 'TRACKING_DATA', label: 'Tracking-/Analytics-Daten', info: 'IP-Adressen, Cookies, Seitenaufrufe, Klickpfade', isTypical: true },
{ id: 'CONSENT_DATA', label: 'Einwilligungsdaten', info: 'Cookie-Consent, Newsletter-Opt-in, Widerrufe', isTypical: true },
{ id: 'SOCIAL_MEDIA_DATA', label: 'Social-Media-Daten', info: 'Follower-Interaktionen, Kommentare, Reichweitendaten' },
{ id: 'PREFERENCE_DATA', label: 'Interessenprofil', info: 'Produktinteressen, Segmentierung, A/B-Test-Zuordnungen' },
{ id: 'PHOTO_VIDEO', label: 'Bild-/Videomaterial', info: 'Kundenfotos (Testimonials), Event-Aufnahmen, UGC' },
]
},
dept_support: {
label: 'Kundenservice / Support',
icon: '🎧',
categories: [
{ id: 'NAME', label: 'Kundenstammdaten', info: 'Name, E-Mail, Telefon, Kundennummer', isTypical: true },
{ id: 'TICKET_DATA', label: 'Ticket-/Anfragedaten', info: 'Ticketnummer, Betreff, Beschreibung, Status, Prioritaet', isTypical: true },
{ id: 'COMMUNICATION_DATA', label: 'Kommunikationsverlauf', info: 'E-Mails, Chat-Protokolle, Anrufnotizen', isTypical: true },
{ id: 'CONTRACT_DATA', label: 'Vertragsdaten', info: 'Produktversion, Lizenz, SLA-Status', isTypical: true },
{ id: 'TECHNICAL_DATA', label: 'Technische Daten', info: 'Systeminfos, Logdateien, Screenshots bei Fehlermeldungen' },
]
},
}
// =============================================================================
// GENERATOR LOGIC
// =============================================================================