diff --git a/Summarize.md b/Summarize.md index f86546b..f80ef0e 100644 --- a/Summarize.md +++ b/Summarize.md @@ -6,6 +6,38 @@ --- +### Aenderungen 2026-03-13 (16): Global Admin — Login-Screen-Branding (Hintergrund + Logo) + +#### Backend (Core Service) +- `settings/settings.controller.ts` — `GET /settings/branding` + `POST /settings/branding` erweitert: + - 4 neue Felder: `loginBgType` ('gradient'|'solid'|'image'), `loginBgColor1`, `loginBgColor2`, `loginBgImage` + - `loginBgImage` Groeßen-Validierung: max. 2MB + - `loginBgType` Enum-Validierung: nur erlaubte Werte werden gespeichert + - Rueckwaertskompatibel: bestehende Redis-Daten ohne neue Felder liefern `null`-Defaults + +#### Frontend +- `admin/AdminCustomizePage.tsx` — Neuer Card-Block "Login-Hintergrund": + - Typ-Toggle: Farbverlauf / Einfarbig / Hintergrundbild + - Farbverlauf: Zwei Farbpicker (Von/Bis) mit Hex-Eingabe + - Einfarbig: Ein Farbpicker mit Hex-Eingabe + - Hintergrundbild: Datei-Upload (max. 2MB, nur Bilder), Entfernen-Button + - Live-Vorschau: Mini-Login-Mockup (200x120px) mit aktuellem Hintergrund und Logo +- `auth/LoginPage.tsx` — Dynamisches Branding: + - `useQuery(['settings', 'branding'])` — oeffentlicher Endpoint, kein Auth noetig + - `useMemo` → `containerStyle`: leitet CSS `background` aus `loginBgType` ab + - Logo-Anzeige: zeigt `` mit `branding.logo` statt hardcoded "INSIGHT"-Text +- `auth/LoginPage.module.css` — Neue Klasse `.logoImage` (max-width 180px, object-fit contain) + +#### TypeScript +- `npx tsc --noEmit` in packages/frontend: 0 Fehler +- `npx tsc --noEmit` in packages/core-service: 0 Fehler + +#### Deployment-Hinweis (Schritt 16) +- Rebuild + Restart: core-service (Backend) und frontend +- Kein Datenbankschema-Aenderung, keine Migration noetig (nur Redis) + +--- + ### Aenderungen 2026-03-13 (15): Experten-Profil – BulletEditor Fett/Kursiv/Unterstrichen + Aufgaben-Anzeige #### Frontend diff --git a/packages/core-service/src/core/settings/settings.controller.ts b/packages/core-service/src/core/settings/settings.controller.ts index e4b2a17..39adafe 100644 --- a/packages/core-service/src/core/settings/settings.controller.ts +++ b/packages/core-service/src/core/settings/settings.controller.ts @@ -138,7 +138,7 @@ export class SettingsController { /** * GET /api/v1/settings/branding - * Branding-Einstellungen lesen (Logo, Sidebar-Farbe etc.). + * Branding-Einstellungen lesen (Logo, Sidebar-Farbe, Login-Hintergrund etc.). */ @Get('branding') @ApiOperation({ summary: 'Branding-Einstellungen lesen' }) @@ -147,21 +147,36 @@ export class SettingsController { sidebarColor: string | null; logoWidth: number | null; sidebarWidth: number | null; + loginBgType: 'gradient' | 'solid' | 'image' | null; + loginBgColor1: string | null; + loginBgColor2: string | null; + loginBgImage: string | null; }> { const raw = await this.redis.get(BRANDING_LOGO_KEY); - if (!raw) return { logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null }; + const empty = { + logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null, + loginBgType: null, loginBgColor1: null, loginBgColor2: null, loginBgImage: null, + }; + if (!raw) return empty; try { const data = JSON.parse(raw); + const loginBgType = ['gradient', 'solid', 'image'].includes(data.loginBgType) + ? (data.loginBgType as 'gradient' | 'solid' | 'image') + : null; return { - logo: data.logo || null, - sidebarColor: data.sidebarColor || null, - logoWidth: typeof data.logoWidth === 'number' ? data.logoWidth : null, - sidebarWidth: typeof data.sidebarWidth === 'number' ? data.sidebarWidth : null, + logo: data.logo || null, + sidebarColor: data.sidebarColor || null, + logoWidth: typeof data.logoWidth === 'number' ? data.logoWidth : null, + sidebarWidth: typeof data.sidebarWidth === 'number' ? data.sidebarWidth : null, + loginBgType, + loginBgColor1: data.loginBgColor1 || null, + loginBgColor2: data.loginBgColor2 || null, + loginBgImage: data.loginBgImage || null, }; } catch { // Legacy: nur Logo als String - return { logo: raw, sidebarColor: null, logoWidth: null, sidebarWidth: null }; + return { ...empty, logo: raw }; } } @@ -179,12 +194,25 @@ export class SettingsController { sidebarColor?: string | null; logoWidth?: number | null; sidebarWidth?: number | null; + loginBgType?: string | null; + loginBgColor1?: string | null; + loginBgColor2?: string | null; + loginBgImage?: string | null; }, ): Promise<{ success: boolean }> { if (body.logo && body.logo.length > 500_000) { throw new BadRequestException('Logo darf maximal 500KB gross sein'); } + if (body.loginBgImage && body.loginBgImage.length > 2_000_000) { + throw new BadRequestException('Hintergrundbild darf maximal 2MB gross sein'); + } + + const validBgTypes = ['gradient', 'solid', 'image']; + const loginBgType = body.loginBgType && validBgTypes.includes(body.loginBgType) + ? body.loginBgType + : null; + // Werte-Grenzen const logoWidth = typeof body.logoWidth === 'number' ? Math.min(Math.max(Math.round(body.logoWidth), 40), 240) @@ -194,10 +222,14 @@ export class SettingsController { : null; const data = { - logo: body.logo || null, - sidebarColor: body.sidebarColor || null, + logo: body.logo || null, + sidebarColor: body.sidebarColor || null, logoWidth, sidebarWidth, + loginBgType, + loginBgColor1: body.loginBgColor1 || null, + loginBgColor2: body.loginBgColor2 || null, + loginBgImage: body.loginBgImage || null, }; await this.redis.set(BRANDING_LOGO_KEY, JSON.stringify(data)); diff --git a/packages/frontend/src/admin/AdminCustomizePage.tsx b/packages/frontend/src/admin/AdminCustomizePage.tsx index daf905d..1bc6848 100644 --- a/packages/frontend/src/admin/AdminCustomizePage.tsx +++ b/packages/frontend/src/admin/AdminCustomizePage.tsx @@ -7,6 +7,10 @@ interface BrandingData { sidebarColor: string | null; logoWidth: number | null; sidebarWidth: number | null; + loginBgType: 'gradient' | 'solid' | 'image' | null; + loginBgColor1: string | null; + loginBgColor2: string | null; + loginBgImage: string | null; } const SIDEBAR_PRESETS = [ @@ -63,10 +67,15 @@ const labelStyle: React.CSSProperties = { export function AdminCustomizePage() { const queryClient = useQueryClient(); const fileInputRef = useRef(null); + const loginBgFileInputRef = useRef(null); const [logo, setLogo] = useState(null); const [sidebarColor, setSidebarColor] = useState('#1e293b'); const [logoWidth, setLogoWidth] = useState(160); const [sidebarWidth, setSidebarWidth] = useState(240); + const [loginBgType, setLoginBgType] = useState<'gradient' | 'solid' | 'image'>('gradient'); + const [loginBgColor1, setLoginBgColor1] = useState('#1a56db'); + const [loginBgColor2, setLoginBgColor2] = useState('#1e3a5f'); + const [loginBgImage, setLoginBgImage] = useState(null); const [hasChanges, setHasChanges] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); @@ -84,6 +93,10 @@ export function AdminCustomizePage() { setSidebarColor(data.sidebarColor || '#1e293b'); setLogoWidth(data.logoWidth ?? 160); setSidebarWidth(data.sidebarWidth ?? 240); + setLoginBgType(data.loginBgType || 'gradient'); + setLoginBgColor1(data.loginBgColor1 || '#1a56db'); + setLoginBgColor2(data.loginBgColor2 || '#1e3a5f'); + setLoginBgImage(data.loginBgImage || null); setHasChanges(false); } }, [data]); @@ -94,6 +107,10 @@ export function AdminCustomizePage() { sidebarColor: string; logoWidth: number; sidebarWidth: number; + loginBgType: string; + loginBgColor1: string; + loginBgColor2: string; + loginBgImage: string | null; }) => { const res = await api.post('/settings/branding', branding); return res.data; @@ -137,9 +154,44 @@ export function AdminCustomizePage() { }; const handleSave = () => { - saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth }); + saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth, loginBgType, loginBgColor1, loginBgColor2, loginBgImage }); }; + const handleLoginBgFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + alert('Bitte nur Bilddateien hochladen'); + return; + } + + if (file.size > 2_000_000) { + alert('Datei darf maximal 2MB gross sein'); + return; + } + + const reader = new FileReader(); + reader.onload = () => { + setLoginBgImage(reader.result as string); + setHasChanges(true); + }; + reader.readAsDataURL(file); + }; + + const loginBgPreviewStyle: React.CSSProperties = (() => { + if (loginBgType === 'gradient') { + return { background: `linear-gradient(135deg, ${loginBgColor1} 0%, ${loginBgColor2} 100%)` }; + } + if (loginBgType === 'solid') { + return { background: loginBgColor1 }; + } + if (loginBgType === 'image' && loginBgImage) { + return { background: `url(${loginBgImage}) center/cover no-repeat` }; + } + return { background: `linear-gradient(135deg, ${loginBgColor1} 0%, ${loginBgColor2} 100%)` }; + })(); + return (
@@ -511,6 +563,176 @@ export function AdminCustomizePage() {
+ {/* Login-Hintergrund */} +
+

+ Login-Hintergrund +

+

+ Hintergrund des Login-Screens. Das Plattform-Logo (oben) wird ebenfalls auf dem Login-Screen angezeigt. +

+ + {/* Typ-Toggle */} +
+ +
+ {(['gradient', 'solid', 'image'] as const).map((type) => ( + + ))} +
+
+ + {/* Farbverlauf */} + {loginBgType === 'gradient' && ( +
+
+ +
+ { setLoginBgColor1(e.target.value); setHasChanges(true); }} + style={{ width: 40, height: 36, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }} + /> + { setLoginBgColor1(e.target.value); setHasChanges(true); }} + style={{ padding: '0.375rem 0.5rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', width: 100, fontFamily: 'monospace' }} + /> +
+
+
+ +
+ { setLoginBgColor2(e.target.value); setHasChanges(true); }} + style={{ width: 40, height: 36, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }} + /> + { setLoginBgColor2(e.target.value); setHasChanges(true); }} + style={{ padding: '0.375rem 0.5rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', width: 100, fontFamily: 'monospace' }} + /> +
+
+
+ )} + + {/* Einfarbig */} + {loginBgType === 'solid' && ( +
+ +
+ { setLoginBgColor1(e.target.value); setHasChanges(true); }} + style={{ width: 40, height: 40, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }} + /> + { setLoginBgColor1(e.target.value); setHasChanges(true); }} + style={{ padding: '0.5rem 0.75rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', width: 120, fontFamily: 'monospace' }} + /> +
+
+ )} + + {/* Hintergrundbild */} + {loginBgType === 'image' && ( +
+ +

+ Empfohlen: JPG oder PNG, min. 1920×1080px, max. 2MB. +

+
+ + + {loginBgImage && ( + + )} + {loginBgImage && ( + ✓ Bild geladen + )} +
+
+ )} + + {/* Vorschau */} +
+ +
+ {/* Mini-Login-Card */} +
+ {logo ? ( + + ) : ( +
INSIGHT
+ )} +
+
+
+
+
+
+
+ {/* Speichern */}
({ + queryKey: ['settings', 'branding'], + queryFn: async () => { + const res = await api.get('/settings/branding'); + return res.data; + }, + staleTime: 5 * 60 * 1000, + retry: false, + }); + + // Dynamischer Hintergrund basierend auf Branding + const containerStyle = useMemo((): React.CSSProperties => { + if (!branding) return {}; + const type = branding.loginBgType || 'gradient'; + if (type === 'gradient') { + const c1 = branding.loginBgColor1 || '#1a56db'; + const c2 = branding.loginBgColor2 || '#1e3a5f'; + return { background: `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)` }; + } + if (type === 'solid') { + return { background: branding.loginBgColor1 || '#1a56db' }; + } + if (type === 'image' && branding.loginBgImage) { + return { background: `url(${branding.loginBgImage}) center/cover no-repeat` }; + } + return {}; + }, [branding]); + // SSO-Status abfragen: Ist Microsoft SSO konfiguriert? const { data: ssoStatus } = useQuery<{ microsoft: boolean }>({ queryKey: ['sso', 'status'], @@ -70,10 +107,14 @@ export function LoginPage() { }; return ( -
+
-

INSIGHT

+ {branding?.logo ? ( + Logo + ) : ( +

INSIGHT

+ )}

Business Platform