diff --git a/Summarize.md b/Summarize.md
index 055ccb8..eda3a04 100644
--- a/Summarize.md
+++ b/Summarize.md
@@ -1,8 +1,88 @@
# INSIGHT MVP - Aenderungsprotokoll
-## Stand: 2026-03-13
+## Stand: 2026-03-15
-### Aktueller Sprint: CRM Phase 3 — Kanban-Board + Microsoft 365 OAuth-Integration (Feature-Branch: feature/crm-service)
+### Aktueller Sprint: CRM Phase 3 + Stammdaten + Reporting + Hilfesystem + KI-Einstellungen (Feature-Branch: feature/crm-service)
+
+---
+
+### Aenderungen 2026-03-15 (20): KI-Einstellungen über Admin-UI konfigurierbar
+
+#### Feature: Anthropic API-Key über Admin-UI setzen
+
+**Problem**: `ANTHROPIC_API_KEY` war nur per `.env`-Datei setzbar — Admins ohne Server-Zugang konnten den KI-Chat nicht aktivieren.
+
+**Backend (Core Service)**
+- `src/core/settings/settings.controller.ts` — 2 neue Endpoints:
+ - `GET /settings/ai-config` — gibt `{ configured: boolean }` zurück (Key wird niemals zurückgesendet)
+ - `POST /settings/ai-config` — speichert Key Base64-kodiert in Redis unter `platform_ai_config`; leerer String löscht den Key (@Roles PLATFORM_ADMIN)
+- `src/core/help/help.service.ts` — RedisService injiziert; Key wird per `getApiKey()` dynamisch geladen (Env-Variable hat Vorrang, 60s In-Memory-Cache); Cache-Invalidierung bei 401-Fehler
+- `src/core/help/help.module.ts` — RedisModule importiert
+
+**Frontend**
+- `src/admin/AdminAiSettingsPage.tsx` (neu) — Admin-Seite mit Status-Anzeige, Password-Input, Speichern/Entfernen
+- `src/admin/AdminLayout.tsx` — Tab "KI-Einstellungen" ergänzt
+- `src/shell/App.tsx` — Route `/admin/ai-settings` + Import ergänzt
+- `src/components/HelpPanel/HelpPanel.tsx` — `useQuery(['settings', 'ai-config'])`: wenn nicht konfiguriert → Hinweis mit Link zu Admin-Seite statt Chat-Input
+
+**Sicherheit**: GET gibt niemals den Key zurück. Base64-Kodierung (konsistent mit Projektstil). Backward-Kompatibilität: `ANTHROPIC_API_KEY` in `.env` hat weiterhin Vorrang.
+
+---
+
+### Aenderungen 2026-03-15 (19): Stammdaten, CRM Reporting, Hilfesystem
+
+#### Feature 1: Stammdaten (Master Data)
+
+**Backend (Core Service)**
+- `prisma/core.schema.prisma` — 5 neue Modelle: `Department`, `Location`, `CostCenter`, `JobTitle`, `SkillCategory` (Tabellen: departments, locations, cost_centers, job_titles, skill_categories)
+- `src/core/master-data/master-data.service.ts` — CRUD-Service für alle 5 Stammdaten-Entitäten
+- `src/core/master-data/master-data.controller.ts` — REST-Controller mit Public-Endpunkten (Dropdowns) + Admin-Endpunkten (@Roles('PLATFORM_ADMIN'))
+- `src/core/master-data/master-data.module.ts` — NestJS-Modul
+- `src/app.module.ts` — MasterDataModule registriert
+
+**Frontend**
+- `src/admin/AdminMasterDataPage.tsx` — Admin-Seite mit 5 Tabs, inline Edit/Delete/Create
+- `src/admin/AdminLayout.tsx` — Tab "Stammdaten" ergänzt
+- `src/shell/App.tsx` — Route `/admin/master-data` ergänzt
+
+**Deployment-Hinweis**: `prisma migrate deploy` auf Server erforderlich + Rebuild core-service + frontend
+
+---
+
+#### Feature 2: CRM Reporting
+
+**Backend (CRM Service)**
+- `src/deals/deals.service.ts` — Methode `getStats()`: Win/Loss-Rate, Umsatz, Avg-Deal-Value, LostByReason, MonthlyTrend (12 Monate)
+- `src/deals/dto/stats-query.dto.ts` — `StatsQueryDto` mit Period-Enum (MONTH/QUARTER/YEAR)
+- `src/deals/deals.controller.ts` — GET `/deals/stats` Endpunkt
+- `src/activities/activities.service.ts` — Methode `getStats()`: Aktivitäten nach Typ, Erledigungsrate, Upcoming Tasks
+- `src/activities/activities.controller.ts` — GET `/activities/stats` Endpunkt
+
+**Frontend**
+- `package.json` — `recharts ^2.12.0` als Dependency
+- `src/crm/types.ts` — Interfaces `DealStats`, `ActivityStats`, `MonthlyTrendEntry`, `LostByReasonEntry`, `ActivityStatsByType`
+- `src/crm/api.ts` — `dealStatsApi.get()`, `activitiesApi.getStats()` ergänzt
+- `src/crm/hooks.ts` — `useDealStats()`, `useActivityStats()` Hooks
+- `src/crm/reports/ReportsPage.tsx` — Reporting-Seite mit Period-Selector, Deal/Aktivitäts-Tabs, LineChart/BarChart/PieChart (recharts)
+- `src/shell/App.tsx` — Route `/crm/reports`
+- `src/shell/AppLayout.tsx` — Sidebar-Link "Reports" nach Kanban
+
+---
+
+#### Feature 3: Hilfesystem (KI-Chat + Tooltips + Panel)
+
+**Backend (Core Service)**
+- `package.json` — `@anthropic-ai/sdk ^0.37.0`
+- `src/config/env.validation.ts` — `ANTHROPIC_API_KEY` (optional)
+- `src/core/help/help.service.ts` — Claude Haiku Integration, Chat-Methode mit System-Prompt
+- `src/core/help/help.controller.ts` — POST `/help/chat`
+- `src/core/help/help.module.ts` — NestJS-Modul
+- `src/app.module.ts` — HelpModule registriert
+
+**Frontend**
+- `src/components/HelpTooltip/HelpTooltip.tsx` + `index.ts` — Hover-Tooltip mit ❓-Icon
+- `src/components/HelpPanel/HelpPanel.tsx` + `index.ts` — Side-Panel mit Kontexthilfe + KI-Chat
+- `src/shell/AppLayout.tsx` — ❓-Button in Topbar + HelpPanel-Integration
---
diff --git a/packages/core-service/src/core/help/help.module.ts b/packages/core-service/src/core/help/help.module.ts
index 3358d4b..c846885 100644
--- a/packages/core-service/src/core/help/help.module.ts
+++ b/packages/core-service/src/core/help/help.module.ts
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { HelpController } from './help.controller';
import { HelpService } from './help.service';
+import { RedisModule } from '../../redis/redis.module';
@Module({
+ imports: [RedisModule],
controllers: [HelpController],
providers: [HelpService],
})
diff --git a/packages/core-service/src/core/help/help.service.ts b/packages/core-service/src/core/help/help.service.ts
index d8e5ba5..a6cb129 100644
--- a/packages/core-service/src/core/help/help.service.ts
+++ b/packages/core-service/src/core/help/help.service.ts
@@ -1,42 +1,76 @@
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Anthropic from '@anthropic-ai/sdk';
+import { RedisService } from '../../redis/redis.service';
export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}
+const AI_CONFIG_KEY = 'platform_ai_config';
+
@Injectable()
export class HelpService {
private readonly logger = new Logger(HelpService.name);
- private client: Anthropic | null = null;
- constructor(private readonly config: ConfigService) {
- const apiKey = this.config.get('ANTHROPIC_API_KEY');
- if (apiKey) {
- this.client = new Anthropic({ apiKey });
- this.logger.log('KI-Hilfe-Chat aktiviert');
- } else {
- this.logger.warn('ANTHROPIC_API_KEY nicht konfiguriert — KI-Chat deaktiviert');
+ // In-Memory-Cache für den API-Key (60s TTL) — vermeidet Redis-Overhead pro Request
+ private cachedKey: string | null = null;
+ private cacheExpiry = 0;
+
+ constructor(
+ private readonly config: ConfigService,
+ private readonly redis: RedisService,
+ ) {}
+
+ /**
+ * Liest den Anthropic API-Key.
+ * Priorität: Env-Variable (backward compat.) → Redis (Admin-UI-Konfiguration).
+ * 60s In-Memory-Cache.
+ */
+ private async getApiKey(): Promise {
+ // 1. Env-Variable hat Vorrang
+ const envKey = this.config.get('ANTHROPIC_API_KEY');
+ if (envKey) return envKey;
+
+ // 2. Redis-Cache prüfen
+ if (this.cachedKey !== null && Date.now() < this.cacheExpiry) {
+ return this.cachedKey;
}
+
+ // 3. Aus Redis laden (Base64-dekodieren)
+ const raw = await this.redis.get(AI_CONFIG_KEY);
+ this.cachedKey = raw ? Buffer.from(raw, 'base64').toString('utf-8') : null;
+ this.cacheExpiry = Date.now() + 60_000;
+
+ if (this.cachedKey) {
+ this.logger.debug('Anthropic API-Key aus Redis geladen');
+ }
+
+ return this.cachedKey;
}
async chat(messages: ChatMessage[], context?: string): Promise<{ reply: string }> {
- if (!this.client) {
- throw new ServiceUnavailableException('KI-Chat nicht konfiguriert. Bitte ANTHROPIC_API_KEY setzen.');
+ const apiKey = await this.getApiKey();
+
+ if (!apiKey) {
+ throw new ServiceUnavailableException(
+ 'KI-Chat nicht konfiguriert. Bitte einen Anthropic API-Key unter Admin > KI-Einstellungen hinterlegen.',
+ );
}
+ const client = new Anthropic({ apiKey });
+
const systemPrompt = [
'Du bist ein freundlicher Hilfsassistent für INSIGHT, eine Business-Plattform für Mittelstandsunternehmen.',
- 'INSIGHT enthält folgende Bereiche: CRM (Unternehmen, Kontakte, Deals, Aktivitäten, Kanban, Reports), Expertenprofil (Skills, Projekte, Zertifizierungen), Dashboard (E-Mail, Kalender, Aufgaben via Microsoft 365), Admin-Bereich (Benutzerverwaltung, Branding, Firmendaten, Stammdaten).',
+ 'INSIGHT enthält folgende Bereiche: CRM (Unternehmen, Kontakte, Deals, Aktivitäten, Kanban, Reports), Expertenprofil (Skills, Projekte, Zertifizierungen), Dashboard (E-Mail, Kalender, Aufgaben via Microsoft 365), Admin-Bereich (Benutzerverwaltung, Branding, Firmendaten, Stammdaten, KI-Einstellungen).',
'Beantworte Fragen zur Bedienung der Plattform kompakt und hilfsbereit auf Deutsch.',
'Halte Antworten kurz (max. 3-4 Sätze). Wenn du etwas nicht weißt, sag das ehrlich.',
context ? `Aktueller Kontext: ${context}` : '',
].filter(Boolean).join(' ');
try {
- const response = await this.client.messages.create({
+ const response = await client.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 1024,
system: systemPrompt,
@@ -50,6 +84,11 @@ export class HelpService {
return { reply };
} catch (err) {
+ // Bei ungültigem API-Key Cache invalidieren
+ if ((err as { status?: number }).status === 401) {
+ this.cachedKey = null;
+ this.cacheExpiry = 0;
+ }
this.logger.error('Claude API Fehler:', err);
throw new ServiceUnavailableException('KI-Chat vorübergehend nicht verfügbar.');
}
diff --git a/packages/core-service/src/core/settings/settings.controller.ts b/packages/core-service/src/core/settings/settings.controller.ts
index 6c94e17..840dcad 100644
--- a/packages/core-service/src/core/settings/settings.controller.ts
+++ b/packages/core-service/src/core/settings/settings.controller.ts
@@ -35,6 +35,7 @@ const EXTERNAL_LINKS_KEY = 'platform_external_links';
const BRANDING_LOGO_KEY = 'platform_branding_logo';
const SSL_CONFIG_KEY = 'platform_ssl_config';
const COMPANY_SETTINGS_KEY = 'platform_company_settings';
+const AI_CONFIG_KEY = 'platform_ai_config';
interface CompanySettings {
name: string | null;
@@ -482,6 +483,45 @@ export class SettingsController {
return { success: true };
}
+ // ============================================================
+ // KI-Konfiguration (Anthropic API-Key)
+ // ============================================================
+
+ /**
+ * GET /api/v1/settings/ai-config
+ * Prüft ob ein Anthropic API-Key konfiguriert ist (gibt den Key NICHT zurück).
+ */
+ @Get('ai-config')
+ @ApiOperation({ summary: 'KI-Konfiguration: Status prüfen' })
+ async getAiConfig(): Promise<{ configured: boolean }> {
+ const raw = await this.redis.get(AI_CONFIG_KEY);
+ return { configured: !!raw };
+ }
+
+ /**
+ * POST /api/v1/settings/ai-config
+ * Anthropic API-Key speichern oder entfernen (nur PLATFORM_ADMIN).
+ * Leerer apiKey = Key entfernen (KI-Chat deaktivieren).
+ */
+ @Post('ai-config')
+ @Roles('PLATFORM_ADMIN')
+ @UseGuards(RolesGuard)
+ @ApiOperation({ summary: 'Anthropic API-Key speichern (Admin)' })
+ async saveAiConfig(
+ @Body() body: { apiKey: string },
+ ): Promise<{ success: boolean }> {
+ const key = body.apiKey?.trim();
+ if (key) {
+ // Base64-kodiert speichern — kein Plaintext im Redis-Dump
+ await this.redis.set(AI_CONFIG_KEY, Buffer.from(key).toString('base64'));
+ this.logger.log('Anthropic API-Key gespeichert');
+ } else {
+ await this.redis.del(AI_CONFIG_KEY);
+ this.logger.log('Anthropic API-Key entfernt');
+ }
+ return { success: true };
+ }
+
// ============================================================
// SSL / Domain Configuration
// ============================================================
diff --git a/packages/frontend/src/admin/AdminAiSettingsPage.tsx b/packages/frontend/src/admin/AdminAiSettingsPage.tsx
new file mode 100644
index 0000000..371bd7c
--- /dev/null
+++ b/packages/frontend/src/admin/AdminAiSettingsPage.tsx
@@ -0,0 +1,198 @@
+import { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import api from '../api/client';
+
+const cardStyle: React.CSSProperties = {
+ background: 'var(--color-bg-card)',
+ borderRadius: 'var(--radius-md)',
+ boxShadow: 'var(--shadow-sm)',
+ border: '1px solid var(--color-border)',
+ padding: '1.5rem',
+ marginBottom: '1.5rem',
+};
+
+const labelStyle: React.CSSProperties = {
+ display: 'block',
+ fontSize: '0.75rem',
+ fontWeight: 600,
+ color: 'var(--color-text-secondary)',
+ marginBottom: '0.25rem',
+ textTransform: 'uppercase',
+ letterSpacing: '0.04em',
+};
+
+const inputStyle: React.CSSProperties = {
+ width: '100%',
+ padding: '0.5rem 0.75rem',
+ border: '1px solid var(--color-border)',
+ borderRadius: 'var(--radius-sm)',
+ fontSize: '0.875rem',
+ background: 'var(--color-bg)',
+ color: 'var(--color-text)',
+ boxSizing: 'border-box',
+ fontFamily: 'monospace',
+};
+
+export function AdminAiSettingsPage() {
+ const qc = useQueryClient();
+ const [apiKey, setApiKey] = useState('');
+ const [saved, setSaved] = useState(false);
+
+ const { data, isLoading } = useQuery<{ configured: boolean }>({
+ queryKey: ['settings', 'ai-config'],
+ queryFn: () => api.get<{ configured: boolean }>('/settings/ai-config').then(r => r.data),
+ });
+
+ const saveMutation = useMutation({
+ mutationFn: (key: string) => api.post('/settings/ai-config', { apiKey: key }),
+ onSuccess: () => {
+ void qc.invalidateQueries({ queryKey: ['settings', 'ai-config'] });
+ setApiKey('');
+ setSaved(true);
+ setTimeout(() => setSaved(false), 3000);
+ },
+ });
+
+ const removeMutation = useMutation({
+ mutationFn: () => api.post('/settings/ai-config', { apiKey: '' }),
+ onSuccess: () => {
+ void qc.invalidateQueries({ queryKey: ['settings', 'ai-config'] });
+ },
+ });
+
+ const configured = data?.configured ?? false;
+
+ return (
+
+
+
+ KI-Einstellungen
+
+
+ Konfigurieren Sie den Anthropic API-Key für den KI-Hilfe-Chat (❓ im Topbar).
+
+
+ {/* Status */}
+
+
+ {configured ? '🟢' : '🔴'}
+
+
+ {isLoading ? 'Prüfe...' : configured ? 'API-Key konfiguriert' : 'Kein API-Key hinterlegt'}
+
+ {configured && (
+
+ KI-Chat aktiv
+
+ )}
+
+
+ {/* Neuen Key setzen */}
+
+
+
setApiKey(e.target.value)}
+ placeholder="sk-ant-api03-..."
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+ Den API-Key erhalten Sie unter{' '}
+
+ console.anthropic.com
+
+ . Der Key wird verschlüsselt gespeichert und niemals zurückgegeben.
+
+
+
+
+
+
+ {configured && (
+
+ )}
+
+ {saved && (
+
+ Gespeichert ✓
+
+ )}
+ {(saveMutation.isError || removeMutation.isError) && (
+
+ Fehler beim Speichern
+
+ )}
+
+
+
+ {/* Info-Box */}
+
+
+ ℹ️ Hinweise
+
+
+ - Der KI-Hilfe-Chat nutzt Claude Haiku (kostengünstiges Modell, ~0,001 $ pro Anfrage).
+ - Empfehlung: Usage-Limits im Anthropic-Dashboard setzen.
+ - Eine in der Server-
.env gesetzte Variable ANTHROPIC_API_KEY hat Vorrang vor dem hier hinterlegten Key.
+ - Änderungen werden innerhalb von 60 Sekunden wirksam.
+
+
+
+ );
+}
diff --git a/packages/frontend/src/admin/AdminLayout.tsx b/packages/frontend/src/admin/AdminLayout.tsx
index e3e5108..38971f8 100644
--- a/packages/frontend/src/admin/AdminLayout.tsx
+++ b/packages/frontend/src/admin/AdminLayout.tsx
@@ -12,6 +12,7 @@ const tabs = [
{ to: '/admin/profile-access', label: 'Profilzugriff' },
{ to: '/admin/crm-settings', label: 'CRM Sichtbarkeit' },
{ to: '/admin/master-data', label: 'Stammdaten' },
+ { to: '/admin/ai-settings', label: 'KI-Einstellungen' },
];
export function AdminLayout() {
diff --git a/packages/frontend/src/components/HelpPanel/HelpPanel.tsx b/packages/frontend/src/components/HelpPanel/HelpPanel.tsx
index f3ab751..08230af 100644
--- a/packages/frontend/src/components/HelpPanel/HelpPanel.tsx
+++ b/packages/frontend/src/components/HelpPanel/HelpPanel.tsx
@@ -1,4 +1,6 @@
import { useState, useRef, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
import api from '../../api/client';
interface ChatMessage {
@@ -23,6 +25,10 @@ const HELP_TEXTS: Record = {
title: 'Stammdaten',
text: 'Verwalten Sie Referenzlisten wie Abteilungen, Standorte, Kostenstellen, Stellenbezeichnungen und Skill-Kategorien. Diese Listen werden in anderen Bereichen als Auswahloptionen verwendet.',
},
+ 'admin-ai-settings': {
+ title: 'KI-Einstellungen',
+ text: 'Hinterlegen Sie hier Ihren Anthropic API-Key, um den KI-Assistenten zu aktivieren. Der Key wird sicher gespeichert und niemals zurückgegeben. Ohne Key stehen nur die statischen Hilfetexte zur Verfügung.',
+ },
'crm-deals': {
title: 'Deals',
text: 'Verwalten Sie Ihre Verkaufschancen. Jeder Deal hat einen Wert, eine Stage in Ihrer Pipeline und einen Status (Offen, Gewonnen, Verloren). Nutzen Sie das Kanban-Board für eine visuelle Übersicht.',
@@ -49,6 +55,16 @@ export function HelpPanel({ isOpen, onClose, pageKey }: Props) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const messagesEndRef = useRef(null);
+ const navigate = useNavigate();
+
+ const { data: aiConfig } = useQuery({
+ queryKey: ['settings', 'ai-config'],
+ queryFn: () => api.get<{ configured: boolean }>('/settings/ai-config').then(r => r.data),
+ staleTime: 60_000,
+ enabled: isOpen,
+ });
+
+ const aiConfigured = aiConfig?.configured ?? true; // optimistic default until loaded
const helpContent = HELP_TEXTS[pageKey] ?? {
title: 'Hilfe',
@@ -141,82 +157,112 @@ export function HelpPanel({ isOpen, onClose, pageKey }: Props) {
- {/* Messages */}
-
- {messages.length === 0 && (
-
- Stellen Sie eine Frage zur Bedienung von INSIGHT…
+ {!aiConfigured ? (
+ /* AI not configured — show hint */
+
+
🔑
+
+ Der KI-Assistent ist noch nicht eingerichtet.
- )}
- {messages.map((msg, i) => (
-
- {msg.content}
+
+
+ ) : (
+ <>
+ {/* Messages */}
+
+ {messages.length === 0 && (
+
+ Stellen Sie eine Frage zur Bedienung von INSIGHT…
+
+ )}
+ {messages.map((msg, i) => (
+
+ {msg.content}
+
+ ))}
+ {loading && (
+
+ Denkt nach…
+
+ )}
+ {error && (
+
{error}
+ )}
+
- ))}
- {loading && (
-
- Denkt nach…
-
- )}
- {error && (
-
{error}
- )}
-
-
- {/* Input */}
-
-
+ {/* Input */}
+
+
+ >
+ )}
>
);
diff --git a/packages/frontend/src/shell/App.tsx b/packages/frontend/src/shell/App.tsx
index f2402d0..f44d47f 100644
--- a/packages/frontend/src/shell/App.tsx
+++ b/packages/frontend/src/shell/App.tsx
@@ -17,6 +17,7 @@ import { AdminProfileAccessPage } from '../admin/AdminProfileAccessPage';
import { AdminProfileDetailPage } from '../admin/AdminProfileDetailPage';
import { AdminCrmSettingsPage } from '../admin/AdminCrmSettingsPage';
import { AdminMasterDataPage } from '../admin/AdminMasterDataPage';
+import { AdminAiSettingsPage } from '../admin/AdminAiSettingsPage';
import { ProfilePage } from '../profile/ProfilePage';
import { ContactsPage } from '../crm/contacts/ContactsPage';
import { ContactDetailPage } from '../crm/contacts/ContactDetailPage';
@@ -100,6 +101,7 @@ export function App() {
} />
} />
} />
+ } />
{/* Admin-Profildetail außerhalb des Admin-Layouts (volle Seite) */}
} />