feat(sdk): Company Profile Wizard verbessert
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 32s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 19s

- B2B2C als Geschaeftsmodell hinzugefuegt
- URL-Felder bei Offering-Auswahl (Website, Shop, App, SaaS) — optional
- Schritt-spezifische Erklaerungen in "Warum diese Fragen?"
- Firmenname ohne Rechtsform, Templates bauen automatisch zusammen
- Gruendungsjahr springt auf 2000 statt 1800
- SDK-Abdeckung Panel und Profil-loeschen Button entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-08 22:41:15 +01:00
parent fd45545fbe
commit 2abf0b4cac
3 changed files with 83 additions and 212 deletions

View File

@@ -22,7 +22,6 @@ import {
AI_INTEGRATION_TYPE_LABELS,
HUMAN_OVERSIGHT_LABELS,
CRITICAL_SECTOR_LABELS,
SDKCoverageAssessment,
} from '@/lib/sdk/types'
// =============================================================================
@@ -103,60 +102,6 @@ const MACHINE_BUILDER_INDUSTRIES = [
const isMachineBuilderIndustry = (industry: string) =>
MACHINE_BUILDER_INDUSTRIES.includes(industry)
// =============================================================================
// HELPER: ASSESS SDK COVERAGE
// =============================================================================
function assessSDKCoverage(profile: Partial<CompanyProfile>): SDKCoverageAssessment {
const coveredRegulations: string[] = ['DSGVO', 'BDSG', 'TTDSG', 'AI Act']
const partiallyCoveredRegulations: string[] = []
const notCoveredRegulations: string[] = []
const reasons: string[] = []
const recommendations: string[] = []
// Check target markets
const targetMarkets = profile.targetMarkets || []
if (targetMarkets.includes('worldwide')) {
notCoveredRegulations.push('CCPA (Kalifornien)', 'LGPD (Brasilien)', 'POPIA (Südafrika)')
reasons.push('Weltweiter Betrieb erfordert Kenntnisse lokaler Datenschutzgesetze')
recommendations.push('Für außereuropäische Märkte empfehlen wir die Konsultation lokaler Rechtsanwälte')
}
if (targetMarkets.includes('eu_uk')) {
partiallyCoveredRegulations.push('UK GDPR', 'UK AI Framework')
reasons.push('UK-Recht weicht nach Brexit teilweise von EU-Recht ab')
recommendations.push('Prüfen Sie UK-spezifische Anpassungen Ihrer Datenschutzerklärung')
}
// Check company size
if (profile.companySize === 'enterprise' || profile.companySize === 'large') {
coveredRegulations.push('NIS2')
reasons.push('Als größeres Unternehmen können NIS2-Pflichten relevant sein')
}
// Check offerings
const offerings = profile.offerings || []
if (offerings.includes('webshop')) {
coveredRegulations.push('Fernabsatzrecht')
recommendations.push('Widerrufsbelehrung und AGB-Generator sind im SDK enthalten')
}
// Determine if fully covered
const requiresLegalCounsel = notCoveredRegulations.length > 0 || targetMarkets.includes('worldwide')
const isFullyCovered = !requiresLegalCounsel && notCoveredRegulations.length === 0
return {
isFullyCovered,
coveredRegulations,
partiallyCoveredRegulations,
notCoveredRegulations,
requiresLegalCounsel,
reasons,
recommendations,
}
}
// =============================================================================
// STEP COMPONENTS
// =============================================================================
@@ -178,7 +123,7 @@ function StepBasicInfo({
type="text"
value={data.companyName || ''}
onChange={e => onChange({ companyName: e.target.value })}
placeholder="Ihre Firma GmbH"
placeholder="Ihre Firma (ohne Rechtsform)"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
@@ -222,9 +167,15 @@ function StepBasicInfo({
<input
type="number"
value={data.foundedYear || ''}
onChange={e => onChange({ foundedYear: parseInt(e.target.value) || null })}
onChange={e => {
const val = parseInt(e.target.value)
onChange({ foundedYear: isNaN(val) ? null : val })
}}
onFocus={e => {
if (!data.foundedYear) onChange({ foundedYear: 2000 })
}}
placeholder="2020"
min="1800"
min="1900"
max={new Date().getFullYear()}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
@@ -233,6 +184,27 @@ function StepBasicInfo({
)
}
// URL fields shown when specific offerings are selected
const OFFERING_URL_CONFIG: Partial<Record<OfferingType, { label: string; placeholder: string; hint: string }>> = {
website: { label: 'Website-Domain', placeholder: 'https://www.beispiel.de', hint: 'Ihre Unternehmenswebsite' },
webshop: { label: 'Online-Shop URL', placeholder: 'https://shop.beispiel.de', hint: 'URL zu Ihrem Online-Shop' },
app_mobile: { label: 'App-Store Links', placeholder: 'https://apps.apple.com/... oder https://play.google.com/...', hint: 'Apple App Store und/oder Google Play Store Link' },
software_saas: { label: 'SaaS-Portal URL', placeholder: 'https://app.beispiel.de', hint: 'Login-/Registrierungsseite Ihres Kundenportals' },
app_web: { label: 'Web-App URL', placeholder: 'https://app.beispiel.de', hint: 'URL zu Ihrer Web-Anwendung' },
}
// Step-specific explanations for "Warum diese Fragen?"
const STEP_EXPLANATIONS: Record<number, string> = {
1: 'Rechtsform und Gründungsjahr bestimmen, welche Meldepflichten und Schwellenwerte für Ihr Unternehmen gelten (z.B. NIS2, AI Act).',
2: 'Ihr Geschäftsmodell und Ihre Angebote bestimmen, welche DSGVO-Pflichten greifen: B2C erfordert z.B. strengere Einwilligungsregeln, Webshops brauchen Cookie-Banner und Datenschutzerklärungen, SaaS-Angebote eine Auftragsverarbeitung.',
3: 'Die Unternehmensgröße bestimmt, ob Sie einen DSB benennen müssen (ab 20 MA), ob NIS2-Pflichten greifen und welche Audit-Anforderungen gelten.',
4: 'Standorte und Zielmärkte bestimmen, welche nationalen Datenschutzgesetze zusätzlich zur DSGVO greifen (z.B. BDSG, DSG-AT, UK GDPR, CCPA).',
5: 'Ob Sie Verantwortlicher oder Auftragsverarbeiter sind, bestimmt Ihre DSGVO-Pflichten grundlegend. KI-Nutzung löst zusätzliche AI-Act-Pflichten aus.',
6: 'Ihre IT-Systeme und KI-Anwendungen werden für das Verarbeitungsverzeichnis (VVT), die technisch-organisatorischen Maßnahmen (TOM) und die KI-Risikobewertung benötigt.',
7: 'Regulierungsrahmen und Prüfzyklen definieren, welche Compliance-Module für Sie aktiviert werden und in welchem Rhythmus Audits stattfinden.',
8: 'Als Maschinenbauer gelten zusätzliche Anforderungen: CE-Kennzeichnung, Maschinenverordnung, Produktsicherheit und ggf. Hochrisiko-KI im Sinne des AI Act.',
}
function StepBusinessModel({
data,
onChange,
@@ -243,19 +215,29 @@ function StepBusinessModel({
const toggleOffering = (offering: OfferingType) => {
const current = data.offerings || []
if (current.includes(offering)) {
onChange({ offerings: current.filter(o => o !== offering) })
// Remove offering and its URL
const urls = { ...(data.offeringUrls || {}) }
delete urls[offering]
onChange({ offerings: current.filter(o => o !== offering), offeringUrls: urls })
} else {
onChange({ offerings: [...current, offering] })
}
}
const updateOfferingUrl = (offering: string, url: string) => {
onChange({ offeringUrls: { ...(data.offeringUrls || {}), [offering]: url } })
}
// Offerings that are selected and have URL config
const selectedWithUrls = (data.offerings || []).filter(o => o in OFFERING_URL_CONFIG)
return (
<div className="space-y-8">
<div>
<label className="block text-sm font-medium text-gray-700 mb-4">
Geschäftsmodell <span className="text-red-500">*</span>
</label>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{Object.entries(BUSINESS_MODEL_LABELS).map(([value, label]) => (
<button
key={value}
@@ -268,9 +250,9 @@ function StepBusinessModel({
}`}
>
<div className="text-2xl mb-2">
{value === 'B2B' ? '🏢' : value === 'B2C' ? '👥' : '🏢👥'}
{value === 'B2B' ? '🏢' : value === 'B2C' ? '👥' : value === 'B2B2C' ? '🏢🔗👥' : '🏢👥'}
</div>
<div className="font-medium">{label}</div>
<div className="font-medium text-sm">{label}</div>
</button>
))}
</div>
@@ -298,6 +280,31 @@ function StepBusinessModel({
))}
</div>
</div>
{/* URL fields for selected offerings */}
{selectedWithUrls.length > 0 && (
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-700">
Zugehörige URLs
</label>
{selectedWithUrls.map(offering => {
const config = OFFERING_URL_CONFIG[offering]!
return (
<div key={offering}>
<label className="block text-sm text-gray-600 mb-1">{config.label}</label>
<input
type="url"
value={(data.offeringUrls || {})[offering] || ''}
onChange={e => updateOfferingUrl(offering, e.target.value)}
placeholder={config.placeholder}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
<p className="text-xs text-gray-400 mt-1">{config.hint}</p>
</div>
)
})}
</div>
)}
</div>
)
}
@@ -1234,134 +1241,6 @@ function StepMachineBuilder({
// COVERAGE ASSESSMENT COMPONENT
// =============================================================================
function CoverageAssessmentPanel({ profile }: { profile: Partial<CompanyProfile> }) {
const assessment = assessSDKCoverage(profile)
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">SDK-Abdeckung</h3>
{/* Status */}
<div
className={`p-4 rounded-lg mb-4 ${
assessment.isFullyCovered
? 'bg-green-50 border border-green-200'
: 'bg-amber-50 border border-amber-200'
}`}
>
<div className="flex items-center gap-2">
{assessment.isFullyCovered ? (
<>
<svg
className="w-5 h-5 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="font-medium text-green-800">Vollständig durch SDK abgedeckt</span>
</>
) : (
<>
<svg
className="w-5 h-5 text-amber-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<span className="font-medium text-amber-800">Teilweise Einschränkungen</span>
</>
)}
</div>
</div>
{/* Covered Regulations */}
{assessment.coveredRegulations.length > 0 && (
<div className="mb-4">
<div className="text-sm font-medium text-gray-700 mb-2">Abgedeckte Regulierungen</div>
<div className="flex flex-wrap gap-2">
{assessment.coveredRegulations.map(reg => (
<span key={reg} className="px-2 py-1 bg-green-100 text-green-700 text-sm rounded-full">
{reg}
</span>
))}
</div>
</div>
)}
{/* Not Covered */}
{assessment.notCoveredRegulations.length > 0 && (
<div className="mb-4">
<div className="text-sm font-medium text-gray-700 mb-2">Nicht abgedeckt</div>
<div className="flex flex-wrap gap-2">
{assessment.notCoveredRegulations.map(reg => (
<span key={reg} className="px-2 py-1 bg-red-100 text-red-700 text-sm rounded-full">
{reg}
</span>
))}
</div>
</div>
)}
{/* Recommendations */}
{assessment.recommendations.length > 0 && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<div className="text-sm font-medium text-blue-800 mb-2">Empfehlungen</div>
<ul className="text-sm text-blue-700 space-y-1">
{assessment.recommendations.map((rec, i) => (
<li key={i} className="flex items-start gap-2">
<span></span>
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
{/* Legal Counsel Warning */}
{assessment.requiresLegalCounsel && (
<div className="mt-4 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<div className="flex items-start gap-3">
<svg
className="w-5 h-5 text-amber-600 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<div>
<div className="font-medium text-amber-800">Rechtsberatung empfohlen</div>
<div className="text-sm text-amber-700 mt-1">
Basierend auf Ihrem Profil empfehlen wir die Konsultation eines spezialisierten
Rechtsanwalts für Bereiche, die über den Scope dieses SDKs hinausgehen.
</div>
</div>
</div>
</div>
)}
</div>
)
}
// =============================================================================
// GENERATE DOCUMENTS BUTTON
// =============================================================================
@@ -1443,6 +1322,7 @@ export default function CompanyProfilePage() {
foundedYear: null,
businessModel: undefined,
offerings: [],
offeringUrls: {},
companySize: undefined,
employeeCount: '',
annualRevenue: '',
@@ -1486,6 +1366,7 @@ export default function CompanyProfilePage() {
foundedYear: data.founded_year || undefined,
businessModel: data.business_model || undefined,
offerings: data.offerings || [],
offeringUrls: data.offering_urls || {},
companySize: data.company_size || undefined,
employeeCount: data.employee_count || '',
annualRevenue: data.annual_revenue || '',
@@ -1552,6 +1433,7 @@ export default function CompanyProfilePage() {
founded_year: formData.foundedYear || null,
business_model: formData.businessModel || 'B2B',
offerings: formData.offerings || [],
offering_urls: formData.offeringUrls || {},
company_size: formData.companySize || 'small',
employee_count: formData.employeeCount || '',
annual_revenue: formData.annualRevenue || '',
@@ -1695,6 +1577,7 @@ export default function CompanyProfilePage() {
foundedYear: null,
businessModel: undefined,
offerings: [],
offeringUrls: {},
companySize: undefined,
employeeCount: '',
annualRevenue: '',
@@ -1894,15 +1777,13 @@ export default function CompanyProfilePage() {
</div>
)}
{/* Sidebar: Coverage Assessment */}
{/* Sidebar */}
<div className="lg:col-span-1">
<CoverageAssessmentPanel profile={formData} />
{/* Info Box */}
<div className="mt-6 bg-blue-50 rounded-xl border border-blue-200 p-6">
{/* Step-specific explanation */}
<div className="bg-blue-50 rounded-xl border border-blue-200 p-6">
<div className="flex items-start gap-3">
<svg
className="w-5 h-5 text-blue-600 mt-0.5"
className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -1917,8 +1798,7 @@ export default function CompanyProfilePage() {
<div>
<div className="font-medium text-blue-800">Warum diese Fragen?</div>
<div className="text-sm text-blue-700 mt-1">
Diese Informationen helfen uns, die für Ihr Unternehmen relevanten Regulierungen
zu identifizieren und ehrlich zu kommunizieren, wo unsere Grenzen liegen.
{STEP_EXPLANATIONS[currentStep] || 'Diese Informationen helfen uns, die für Ihr Unternehmen relevanten Regulierungen zu identifizieren.'}
</div>
</div>
</div>
@@ -1933,18 +1813,6 @@ export default function CompanyProfilePage() {
<GenerateDocumentsButton />
</div>
)}
{/* Delete Profile Button */}
{formData.companyName && (
<div className="mt-6">
<button
onClick={() => setShowDeleteConfirm(true)}
className="w-full px-4 py-2 text-sm text-red-600 border border-red-200 rounded-lg hover:bg-red-50 transition-colors"
>
Profil löschen (Art. 17 DSGVO)
</button>
</div>
)}
</div>
</div>
</div>

View File

@@ -27,7 +27,7 @@ export type CustomerType = 'new' | 'existing'
// COMPANY PROFILE (Business Context - collected before use cases)
// =============================================================================
export type BusinessModel = 'B2B' | 'B2C' | 'B2B_B2C'
export type BusinessModel = 'B2B' | 'B2C' | 'B2B_B2C' | 'B2B2C'
export type OfferingType =
| 'app_mobile' // Mobile App
@@ -154,6 +154,7 @@ export interface CompanyProfile {
// Business Model
businessModel: BusinessModel
offerings: OfferingType[]
offeringUrls: Partial<Record<string, string>> // e.g. { website: 'https://...', webshop: 'https://...' }
// Size & Scope
companySize: CompanySize
@@ -204,6 +205,7 @@ export const BUSINESS_MODEL_LABELS: Record<BusinessModel, string> = {
B2B: 'B2B (Geschäftskunden)',
B2C: 'B2C (Privatkunden)',
B2B_B2C: 'B2B und B2C',
B2B2C: 'B2B2C (über Partner an Endkunden)',
}
export const OFFERING_TYPE_LABELS: Record<OfferingType, { label: string; description: string }> = {

View File

@@ -48,7 +48,8 @@ def _get_template_context(db, tid: str) -> dict:
resp = row_to_response(row)
# Build flat context
return {
"company_name": resp.company_name,
"company_name": f"{resp.company_name} {resp.legal_form}".strip() if resp.legal_form else resp.company_name,
"company_name_short": resp.company_name,
"legal_form": resp.legal_form,
"industry": resp.industry,
"business_model": resp.business_model,