diff --git a/packages/core-service/src/core/settings/settings.controller.ts b/packages/core-service/src/core/settings/settings.controller.ts index 9fbcdd3..b0890a4 100644 --- a/packages/core-service/src/core/settings/settings.controller.ts +++ b/packages/core-service/src/core/settings/settings.controller.ts @@ -164,11 +164,13 @@ export class SettingsController { loginBgColor1: string | null; loginBgColor2: string | null; loginBgImage: string | null; + primaryColor: string | null; }> { const raw = await this.redis.get(BRANDING_LOGO_KEY); const empty = { logo: null, sidebarColor: null, logoWidth: null, sidebarWidth: null, loginBgType: null, loginBgColor1: null, loginBgColor2: null, loginBgImage: null, + primaryColor: null, }; if (!raw) return empty; @@ -177,6 +179,8 @@ export class SettingsController { const loginBgType = ['gradient', 'solid', 'image'].includes(data.loginBgType) ? (data.loginBgType as 'gradient' | 'solid' | 'image') : null; + // Hex-Farbe validieren (#rrggbb) + const hexColorRe = /^#[0-9a-fA-F]{6}$/; return { logo: data.logo || null, sidebarColor: data.sidebarColor || null, @@ -186,6 +190,9 @@ export class SettingsController { loginBgColor1: data.loginBgColor1 || null, loginBgColor2: data.loginBgColor2 || null, loginBgImage: data.loginBgImage || null, + primaryColor: typeof data.primaryColor === 'string' && hexColorRe.test(data.primaryColor) + ? data.primaryColor + : null, }; } catch { // Legacy: nur Logo als String @@ -211,6 +218,7 @@ export class SettingsController { loginBgColor1?: string | null; loginBgColor2?: string | null; loginBgImage?: string | null; + primaryColor?: string | null; }, ): Promise<{ success: boolean }> { if (body.logo && body.logo.length > 500_000) { @@ -234,6 +242,12 @@ export class SettingsController { ? Math.min(Math.max(Math.round(body.sidebarWidth), 200), 360) : null; + // Primärfarbe validieren — muss gültiges #rrggbb sein + const hexColorRe = /^#[0-9a-fA-F]{6}$/; + const primaryColor = typeof body.primaryColor === 'string' && hexColorRe.test(body.primaryColor) + ? body.primaryColor + : null; + const data = { logo: body.logo || null, sidebarColor: body.sidebarColor || null, @@ -243,6 +257,7 @@ export class SettingsController { loginBgColor1: body.loginBgColor1 || null, loginBgColor2: body.loginBgColor2 || null, loginBgImage: body.loginBgImage || null, + primaryColor, }; 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 1bc6848..1058d8d 100644 --- a/packages/frontend/src/admin/AdminCustomizePage.tsx +++ b/packages/frontend/src/admin/AdminCustomizePage.tsx @@ -11,8 +11,18 @@ interface BrandingData { loginBgColor1: string | null; loginBgColor2: string | null; loginBgImage: string | null; + primaryColor: string | null; } +const PRIMARY_COLOR_PRESETS = [ + { label: 'Standard', color: '#1040bb' }, + { label: 'Indigo', color: '#4f46e5' }, + { label: 'Grün', color: '#059669' }, + { label: 'Orange', color: '#d97706' }, + { label: 'Lila', color: '#7c3aed' }, + { label: 'Rot', color: '#dc2626' }, +]; + const SIDEBAR_PRESETS = [ { label: 'Standard', color: '#1e293b' }, { label: 'Dunkelblau', color: '#0f172a' }, @@ -76,6 +86,7 @@ export function AdminCustomizePage() { const [loginBgColor1, setLoginBgColor1] = useState('#1a56db'); const [loginBgColor2, setLoginBgColor2] = useState('#1e3a5f'); const [loginBgImage, setLoginBgImage] = useState(null); + const [primaryColor, setPrimaryColor] = useState('#1040bb'); const [hasChanges, setHasChanges] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); @@ -97,6 +108,7 @@ export function AdminCustomizePage() { setLoginBgColor1(data.loginBgColor1 || '#1a56db'); setLoginBgColor2(data.loginBgColor2 || '#1e3a5f'); setLoginBgImage(data.loginBgImage || null); + setPrimaryColor(data.primaryColor || '#1040bb'); setHasChanges(false); } }, [data]); @@ -111,6 +123,7 @@ export function AdminCustomizePage() { loginBgColor1: string; loginBgColor2: string; loginBgImage: string | null; + primaryColor: string; }) => { const res = await api.post('/settings/branding', branding); return res.data; @@ -154,7 +167,7 @@ export function AdminCustomizePage() { }; const handleSave = () => { - saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth, loginBgType, loginBgColor1, loginBgColor2, loginBgImage }); + saveMutation.mutate({ logo, sidebarColor, logoWidth, sidebarWidth, loginBgType, loginBgColor1, loginBgColor2, loginBgImage, primaryColor }); }; const handleLoginBgFileSelect = (e: React.ChangeEvent) => { @@ -733,6 +746,103 @@ export function AdminCustomizePage() { + {/* Button-/Primärfarbe */} +
+

+ Button-/Primärfarbe +

+

+ Farbe für Buttons, aktive Links und Akzente in der gesamten Oberfläche. +

+ + {/* Voreinstellungen */} +
+ +
+ {PRIMARY_COLOR_PRESETS.map((preset) => ( + + ))} +
+
+ + {/* Eigene Farbe + Vorschau */} +
+ +
+ { setPrimaryColor(e.target.value); setHasChanges(true); }} + style={{ width: 40, height: 40, padding: 0, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }} + /> + { setPrimaryColor(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' }} + /> + {/* Vorschau-Button */} +
+ + +
+
+
+
+ {/* Speichern */}
({ queryKey: ['settings', 'branding'], queryFn: async () => { @@ -164,12 +165,35 @@ export function AppLayout() { sidebarColor: string | null; logoWidth: number | null; sidebarWidth: number | null; + primaryColor: string | null; }>('/settings/branding'); return res.data; }, staleTime: 5 * 60 * 1000, }); + // Primärfarbe als CSS-Variable setzen + useEffect(() => { + const root = document.documentElement; + const hex = branding?.primaryColor; + if (hex && /^#[0-9a-fA-F]{6}$/.test(hex)) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + const rh = Math.round(r * 0.82); + const gh = Math.round(g * 0.82); + const bh = Math.round(b * 0.82); + const hoverHex = `#${rh.toString(16).padStart(2, '0')}${gh.toString(16).padStart(2, '0')}${bh.toString(16).padStart(2, '0')}`; + root.style.setProperty('--color-primary', hex); + root.style.setProperty('--color-primary-hover', hoverHex); + root.style.setProperty('--color-primary-light', `rgba(${r}, ${g}, ${b}, 0.12)`); + } else { + root.style.removeProperty('--color-primary'); + root.style.removeProperty('--color-primary-hover'); + root.style.removeProperty('--color-primary-light'); + } + }, [branding?.primaryColor]); + const handleLogout = async () => { await logout(); navigate('/login');