diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index edad1337..bafe97d9 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -1066,6 +1066,16 @@ export interface AppSettings { delivery_mode: DeliveryModeConfig; shop_name: string; contact_telegram: string; + admin_color_primary: string; + admin_color_secondary: string; + admin_color_success: string; + admin_color_danger: string; + admin_color_warning: string; + client_color_primary: string; + client_color_secondary: string; + client_color_success: string; + client_color_danger: string; + client_color_warning: string; } export const getSettings = async (): Promise<{ diff --git a/frontend-admin/src/context/ThemeContext.tsx b/frontend-admin/src/context/ThemeContext.tsx index 18db59fe..fbc07d98 100644 --- a/frontend-admin/src/context/ThemeContext.tsx +++ b/frontend-admin/src/context/ThemeContext.tsx @@ -1,6 +1,8 @@ -import React, { createContext, useContext, useState, useEffect, type ReactNode } from "react"; +import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react"; +import { AppState } from "react-native"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { darkColors, lightColors, type Colors } from "../theme/colors"; +import { getSettings } from "../api/api_admin"; type ThemeMode = "dark" | "light"; @@ -12,16 +14,48 @@ interface ThemeContextType { } const STORAGE_KEY = "@theme_mode"; +const COLORS_CACHE_KEY = "@admin_colors_cache"; const ThemeContext = createContext(undefined); export function ThemeProvider({ children }: { children: ReactNode }) { const [mode, setMode] = useState("dark"); + const [colorOverrides, setColorOverrides] = useState>({}); + + const fetchAdminColors = useCallback(async () => { + try { + const result = await getSettings(); + if (result.success && result.settings) { + const s = result.settings; + const overrides: Partial = {}; + if (s.admin_color_primary) overrides.accent = s.admin_color_primary; + if (s.admin_color_secondary) overrides.secondary = s.admin_color_secondary; + if (s.admin_color_success) overrides.success = s.admin_color_success; + if (s.admin_color_danger) overrides.danger = s.admin_color_danger; + if (s.admin_color_warning) overrides.warning = s.admin_color_warning; + setColorOverrides(overrides); + AsyncStorage.setItem(COLORS_CACHE_KEY, JSON.stringify(overrides)).catch(() => {}); + } + } catch {} + }, []); useEffect(() => { AsyncStorage.getItem(STORAGE_KEY).then((val) => { if (val === "light" || val === "dark") setMode(val); }); - }, []); + AsyncStorage.getItem(COLORS_CACHE_KEY).then((val) => { + if (val) { + try { setColorOverrides(JSON.parse(val)); } catch {} + } + }); + fetchAdminColors(); + }, [fetchAdminColors]); + + useEffect(() => { + const sub = AppState.addEventListener("change", (state) => { + if (state === "active") fetchAdminColors(); + }); + return () => sub.remove(); + }, [fetchAdminColors]); const toggleTheme = () => { const next = mode === "dark" ? "light" : "dark"; @@ -29,8 +63,11 @@ export function ThemeProvider({ children }: { children: ReactNode }) { AsyncStorage.setItem(STORAGE_KEY, next); }; + const baseColors = mode === "dark" ? darkColors : lightColors; + const colors: Colors = { ...baseColors, ...colorOverrides }; + const value: ThemeContextType = { - colors: mode === "dark" ? darkColors : lightColors, + colors, mode, toggleTheme, isDark: mode === "dark", diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index b60e098c..94ec9512 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -1194,6 +1194,16 @@ export default function SettingsScreen() { shop_name: "Milieu-Nantais", contact_telegram: "", points_reward: null, + admin_color_primary: "#7c3aed", + admin_color_secondary: "#22d3ee", + admin_color_success: "#4ade80", + admin_color_danger: "#ef4444", + admin_color_warning: "#f59e0b", + client_color_primary: "#7c3aed", + client_color_secondary: "#22d3ee", + client_color_success: "#4ade80", + client_color_danger: "#ef4444", + client_color_warning: "#f59e0b", }); const [showApiKey, setShowApiKey] = useState(false); const [showIpnSecret, setShowIpnSecret] = useState(false); @@ -2182,6 +2192,66 @@ export default function SettingsScreen() { )} + {/* ============================================ */} + {/* 🎨 COULEURS DE L'INTERFACE */} + {/* ============================================ */} + + + Personnalisez les couleurs de l'espace admin et de l'app client / site web. + + + {(["admin", "client"] as const).map((scope) => ( + + + {scope === "admin" ? "Espace admin" : "App client & site web"} + + {([ + { label: "Principale", key: `${scope}_color_primary` as const }, + { label: "Secondaire", key: `${scope}_color_secondary` as const }, + { label: "Succès", key: `${scope}_color_success` as const }, + { label: "Danger", key: `${scope}_color_danger` as const }, + { label: "Avertissement", key: `${scope}_color_warning` as const }, + ] as { label: string; key: keyof typeof settings }[]).map(({ label, key }) => ( + + {label} + + + setSettings((p) => ({ ...p, [key]: v }))} + placeholder="#7c3aed" + placeholderTextColor={colors.textMuted} + autoCapitalize="none" + autoCorrect={false} + /> + + + {["#7c3aed", "#2563eb", "#0891b2", "#059669", "#d97706", "#dc2626", "#db2777", "#f59e0b", "#4ade80", "#ef4444", "#22d3ee", "#0f172a"].map((color) => ( + setSettings((p) => ({ ...p, [key]: color }))} + style={{ + width: 26, height: 26, borderRadius: 13, + backgroundColor: color, + borderWidth: (settings[key] as string) === color ? 2.5 : 1, + borderColor: (settings[key] as string) === color ? colors.textPrimary : colors.border, + }} + /> + ))} + + + ))} + + ))} + + = 3 ? "value-danger" : penaltyCount > 0 ? "value-warning" : ""}`} > - {penaltyCount} + {penaltyCount} €

Score amendes

diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 29f9c894..3cd79596 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -778,6 +778,11 @@ export interface PublicSettings { telegram_notifications_enabled: boolean; two_fa_enabled: boolean; contact_telegram: string; + client_color_primary: string; + client_color_secondary: string; + client_color_success: string; + client_color_danger: string; + client_color_warning: string; } export const getPublicSettings = async (): Promise => { @@ -794,6 +799,11 @@ export const getPublicSettings = async (): Promise => { telegram_notifications_enabled: false, two_fa_enabled: false, contact_telegram: "", + client_color_primary: "#7c3aed", + client_color_secondary: "#22d3ee", + client_color_success: "#4ade80", + client_color_danger: "#ef4444", + client_color_warning: "#f59e0b", }; try { const { data } = await apiClient.get(`${V1}/app-settings`); @@ -816,6 +826,11 @@ export const getPublicSettings = async (): Promise => { data.telegram_notifications_enabled ?? false, two_fa_enabled: data.two_fa_enabled ?? false, contact_telegram: data.contact_telegram || "", + client_color_primary: data.client_color_primary || "#7c3aed", + client_color_secondary: data.client_color_secondary || "#22d3ee", + client_color_success: data.client_color_success || "#4ade80", + client_color_danger: data.client_color_danger || "#ef4444", + client_color_warning: data.client_color_warning || "#f59e0b", }; } catch { return defaults; diff --git a/mobile/src/context/ThemeContext.tsx b/mobile/src/context/ThemeContext.tsx index 18db59fe..d2c0ecfd 100644 --- a/mobile/src/context/ThemeContext.tsx +++ b/mobile/src/context/ThemeContext.tsx @@ -1,6 +1,8 @@ -import React, { createContext, useContext, useState, useEffect, type ReactNode } from "react"; +import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react"; +import { AppState } from "react-native"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { darkColors, lightColors, type Colors } from "../theme/colors"; +import { getPublicSettings } from "../api/api"; type ThemeMode = "dark" | "light"; @@ -12,16 +14,45 @@ interface ThemeContextType { } const STORAGE_KEY = "@theme_mode"; +const COLORS_CACHE_KEY = "@client_colors_cache"; const ThemeContext = createContext(undefined); export function ThemeProvider({ children }: { children: ReactNode }) { const [mode, setMode] = useState("dark"); + const [colorOverrides, setColorOverrides] = useState>({}); + + const fetchClientColors = useCallback(async () => { + try { + const s = await getPublicSettings(); + const overrides: Partial = {}; + if (s.client_color_primary) overrides.accent = s.client_color_primary; + if (s.client_color_secondary) overrides.secondary = s.client_color_secondary; + if (s.client_color_success) overrides.success = s.client_color_success; + if (s.client_color_danger) overrides.danger = s.client_color_danger; + if (s.client_color_warning) overrides.warning = s.client_color_warning; + setColorOverrides(overrides); + AsyncStorage.setItem(COLORS_CACHE_KEY, JSON.stringify(overrides)).catch(() => {}); + } catch {} + }, []); useEffect(() => { AsyncStorage.getItem(STORAGE_KEY).then((val) => { if (val === "light" || val === "dark") setMode(val); }); - }, []); + AsyncStorage.getItem(COLORS_CACHE_KEY).then((val) => { + if (val) { + try { setColorOverrides(JSON.parse(val)); } catch {} + } + }); + fetchClientColors(); + }, [fetchClientColors]); + + useEffect(() => { + const sub = AppState.addEventListener("change", (state) => { + if (state === "active") fetchClientColors(); + }); + return () => sub.remove(); + }, [fetchClientColors]); const toggleTheme = () => { const next = mode === "dark" ? "light" : "dark"; @@ -29,8 +60,11 @@ export function ThemeProvider({ children }: { children: ReactNode }) { AsyncStorage.setItem(STORAGE_KEY, next); }; + const baseColors = mode === "dark" ? darkColors : lightColors; + const colors: Colors = { ...baseColors, ...colorOverrides }; + const value: ThemeContextType = { - colors: mode === "dark" ? darkColors : lightColors, + colors, mode, toggleTheme, isDark: mode === "dark", diff --git a/mobile/src/theme/colors.ts b/mobile/src/theme/colors.ts index 914a6238..3dd5a6ca 100644 --- a/mobile/src/theme/colors.ts +++ b/mobile/src/theme/colors.ts @@ -16,6 +16,7 @@ export const darkColors = { accent: "#7c3aed", accentDark: "#6d28d9", accentLight: "#8b5cf6", + secondary: "#22d3ee", // Status success: "#4ade80", @@ -62,6 +63,7 @@ export const lightColors: Colors = { accent: "#7c3aed", accentDark: "#6d28d9", accentLight: "#8b5cf6", + secondary: "#0891b2", // Status success: "#16a34a",