diff --git a/packages/core-service/src/core/settings/settings.controller.ts b/packages/core-service/src/core/settings/settings.controller.ts index ed2ada8..e4b2a17 100644 --- a/packages/core-service/src/core/settings/settings.controller.ts +++ b/packages/core-service/src/core/settings/settings.controller.ts @@ -145,19 +145,23 @@ export class SettingsController { async getBranding(): Promise<{ logo: string | null; sidebarColor: string | null; + logoWidth: number | null; + sidebarWidth: number | null; }> { const raw = await this.redis.get(BRANDING_LOGO_KEY); - if (!raw) return { logo: null, sidebarColor: null }; + if (!raw) return { logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null }; try { const data = JSON.parse(raw); return { - logo: data.logo || 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, }; } catch { // Legacy: nur Logo als String - return { logo: raw, sidebarColor: null }; + return { logo: raw, sidebarColor: null, logoWidth: null, sidebarWidth: null }; } } @@ -170,15 +174,30 @@ export class SettingsController { @UseGuards(RolesGuard) @ApiOperation({ summary: 'Branding-Einstellungen speichern (Admin)' }) async saveBranding( - @Body() body: { logo?: string | null; sidebarColor?: string | null }, + @Body() body: { + logo?: string | null; + sidebarColor?: string | null; + logoWidth?: number | null; + sidebarWidth?: number | null; + }, ): Promise<{ success: boolean }> { if (body.logo && body.logo.length > 500_000) { throw new BadRequestException('Logo darf maximal 500KB gross sein'); } + // Werte-Grenzen + const logoWidth = typeof body.logoWidth === 'number' + ? Math.min(Math.max(Math.round(body.logoWidth), 40), 240) + : null; + const sidebarWidth = typeof body.sidebarWidth === 'number' + ? Math.min(Math.max(Math.round(body.sidebarWidth), 200), 360) + : null; + const data = { - logo: body.logo || null, + logo: body.logo || null, sidebarColor: body.sidebarColor || null, + logoWidth, + sidebarWidth, }; 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 32afa07..daf905d 100644 --- a/packages/frontend/src/admin/AdminCustomizePage.tsx +++ b/packages/frontend/src/admin/AdminCustomizePage.tsx @@ -5,6 +5,8 @@ import api from '../api/client'; interface BrandingData { logo: string | null; sidebarColor: string | null; + logoWidth: number | null; + sidebarWidth: number | null; } const SIDEBAR_PRESETS = [ @@ -63,6 +65,8 @@ export function AdminCustomizePage() { const fileInputRef = useRef(null); const [logo, setLogo] = useState(null); const [sidebarColor, setSidebarColor] = useState('#1e293b'); + const [logoWidth, setLogoWidth] = useState(160); + const [sidebarWidth, setSidebarWidth] = useState(240); const [hasChanges, setHasChanges] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); @@ -78,6 +82,8 @@ export function AdminCustomizePage() { if (data) { setLogo(data.logo); setSidebarColor(data.sidebarColor || '#1e293b'); + setLogoWidth(data.logoWidth ?? 160); + setSidebarWidth(data.sidebarWidth ?? 240); setHasChanges(false); } }, [data]); @@ -86,6 +92,8 @@ export function AdminCustomizePage() { mutationFn: async (branding: { logo: string | null; sidebarColor: string; + logoWidth: number; + sidebarWidth: number; }) => { const res = await api.post('/settings/branding', branding); return res.data; @@ -129,7 +137,7 @@ export function AdminCustomizePage() { }; const handleSave = () => { - saveMutation.mutate({ logo, sidebarColor }); + saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth }); }; return ( @@ -169,6 +177,7 @@ export function AdminCustomizePage() { SVG mit transparentem Hintergrund, max. 500KB.

+ {/* Vorschau + Upload */}
{logo ? ( @@ -195,7 +205,7 @@ export function AdminCustomizePage() { src={logo} alt="Logo" style={{ - maxWidth: '100%', + maxWidth: `${Math.round(logoWidth * 0.72)}px`, maxHeight: '100%', objectFit: 'contain', }} @@ -242,6 +252,29 @@ export function AdminCustomizePage() { )}
+ + {/* Logo-Breite Slider */} +
+ + { + setLogoWidth(Number(e.target.value)); + setHasChanges(true); + }} + style={{ width: '100%', maxWidth: 320, display: 'block', marginBottom: '0.25rem' }} + /> +
+ 40px + 240px +
+
{/* Sidebar-Farbe */} @@ -414,6 +447,70 @@ export function AdminCustomizePage() { + {/* Sidebar-Breite */} +
+

+ Sidebar-Breite +

+

+ Breite der linken Menü-Leiste im ausgeklappten Zustand. +

+ + {/* Slider + Vorschau */} +
+
+ + { + setSidebarWidth(Number(e.target.value)); + setHasChanges(true); + }} + style={{ width: '100%', maxWidth: 320, display: 'block', marginBottom: '0.25rem' }} + /> +
+ 200px (schmal) + 360px (breit) +
+
+ + {/* Sidebar-Vorschau skaliert */} +
+ {/* Mini-Logo */} +
+ {logo ? ( + + ) : 'INSIGHT'} +
+ {/* Mini Nav-Einträge */} + {[80, 65, 75, 55].map((w, i) => ( +
+ ))} +
+
+
+ {/* Speichern */}
({ queryKey: ['settings', 'branding'], queryFn: async () => { const res = await api.get<{ logo: string | null; sidebarColor: string | null; + logoWidth: number | null; + sidebarWidth: number | null; }>('/settings/branding'); return res.data; }, @@ -172,11 +176,10 @@ export function AppLayout() { {/* Sidebar */}