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
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user