"use client"; import { useState, useCallback } from "react"; import { Save, Eye, EyeOff, CheckCircle, AlertCircle, Loader2, RefreshCw, Sun, Moon, RotateCcw } from "lucide-react"; import { useTheme } from "next-themes"; import { useGridStore } from "@/features/grid/store/use-grid-store"; import { useDbSettingsStore } from "@/features/settings/store/use-db-settings-store"; import { testDatabaseConnection, saveDatabaseSettings, getSavedDatabaseConfig, saveQlikSettings, testQlikConnection } from "@/features/settings/actions"; import { useEffect } from "react"; import { UserManagement } from "@/features/admin/components/user-management"; interface OpenRouterModel { id: string; name: string; free: boolean; } function Section({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) { return (

{title}

{subtitle &&

{subtitle}

}
{children}
); } function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
{children} {hint &&

{hint}

}
); } interface FfSyncTable { nom: string; derniereSync: string; nbLignes?: number; } interface FfSyncStatus { lastSync: string; tables: FfSyncTable[]; } function FfApiStatusSection() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchStatus = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch("/api/ff-status"); if (!res.ok) throw new Error(`HTTP ${res.status}`); setStatus(await res.json()); } catch (e) { setError(e instanceof Error ? e.message : "Erreur inconnue"); } finally { setLoading(false); } }, []); useEffect(() => { fetchStatus(); }, [fetchStatus]); return (
{error && {error}}
{status && (

Dernière sync globale : {(() => { const d = new Date(status.lastSync); return isNaN(d.getTime()) ? (status.lastSync ?? "—") : d.toLocaleString("fr-FR"); })()}

{status.tables?.length > 0 && (
{status.tables.map((t) => ( ))}
Table Sync Lignes
{t.nom} {(() => { const d = new Date(t.derniereSync); return isNaN(d.getTime()) ? (t.derniereSync ?? "—") : d.toLocaleString("fr-FR"); })()} {t.nbLignes?.toLocaleString("fr-FR") ?? "—"}
)}
)}
); } function QlikSettingsSection() { const [host, setHost] = useState(""); const [user, setUser] = useState(""); const [password, setPassword] = useState(""); const [showPwd, setShowPwd] = useState(false); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [status, setStatus] = useState<{ type: "ok" | "err"; msg: string } | null>(null); useEffect(() => { getSavedDatabaseConfig().then((c) => { if (!c) return; setHost(c.qlikHost ?? ""); setUser(c.qlikUser ?? ""); setPassword(c.qlikPassword ?? ""); }); }, []); const handleSave = async () => { setSaving(true); setStatus(null); const res = await saveQlikSettings(host.trim(), user.trim(), password); setSaving(false); setStatus(res.success ? { type: "ok", msg: "Réglages Qlik enregistrés" } : { type: "err", msg: res.error || "Erreur" }); }; const handleTest = async () => { setTesting(true); setStatus(null); const res = await testQlikConnection(host.trim(), user.trim(), password); setTesting(false); setStatus(res.success ? { type: "ok", msg: "Connexion Qlik OK" } : { type: "err", msg: res.error || "Échec connexion" }); }; return (
setHost(e.target.value)} className="apple-input font-mono" /> setUser(e.target.value)} className="apple-input font-mono" />
setPassword(e.target.value)} className="apple-input font-mono pr-10" />
{status && (
{status.type === "ok" ? : } {status.msg}
)}
); } export default function SettingsPage() { const { resolvedTheme, setTheme } = useTheme(); const isDark = resolvedTheme === "dark"; const displayDensity = useGridStore((s) => s.displayDensity); const setDisplayDensity = useGridStore((s) => s.setDisplayDensity); const [showKey, setShowKey] = useState(false); const [apiKey, setApiKey] = useState(""); const [dbStatus, setDbStatus] = useState<"idle" | "testing" | "ok" | "error">("idle"); const [dbError, setDbError] = useState(null); const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle"); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState("google/gemini-2.0-flash-001"); const [modelsStatus, setModelsStatus] = useState<"idle" | "loading" | "ok" | "error">("idle"); // AI Provider const [aiProvider, setAiProvider] = useState<"openrouter" | "google">("openrouter"); const [googleAiKey, setGoogleAiKey] = useState(""); const [showGoogleKey, setShowGoogleKey] = useState(false); const [googleAiModel, setGoogleAiModel] = useState(""); const [googleModels, setGoogleModels] = useState<{ id: string; name: string }[]>([]); const [googleModelsStatus, setGoogleModelsStatus] = useState<"idle" | "loading" | "ok" | "error">("idle"); const [isMounted, setIsMounted] = useState(false); const { host, setHost, port, setPort, database, setDatabase, user, setUser, password, setPassword, ssl, setSsl, getDatabaseUrl } = useDbSettingsStore(); const fetchGoogleModels = useCallback(async (key: string) => { if (!key.trim()) return; setGoogleModelsStatus("loading"); try { const res = await fetch("/api/google-ai/models", { headers: { "x-google-ai-key": key } }); if (!res.ok) throw new Error(); const data = await res.json(); setGoogleModels(data.models ?? []); setGoogleModelsStatus("ok"); } catch { setGoogleModelsStatus("error"); } }, []); const fetchModels = useCallback(async (key: string) => { if (!key.trim()) return; setModelsStatus("loading"); try { const res = await fetch("/api/openrouter/models", { headers: { "x-openrouter-key": key } }); if (!res.ok) throw new Error(); const data = await res.json(); setModels(data.models ?? []); setModelsStatus("ok"); } catch { setModelsStatus("error"); } }, []); const reloadFromServer = useCallback(async () => { const config = await getSavedDatabaseConfig(); if (config) { if (config.url) { // Parser l'URL pour remettre dans le store try { const url = new URL(config.url.replace("postgres://", "http://")); // URL parser helper setHost(url.hostname); setPort(url.port || "5432"); setDatabase(url.pathname.slice(1).split("?")[0]); setUser(url.username); setPassword(decodeURIComponent(url.password)); setSsl(config.url.includes("sslmode=require")); } catch (e) { console.error("Failed to parse saved URL", e); } } if (config.openRouterKey) { setApiKey(config.openRouterKey); fetchModels(config.openRouterKey); } if (config.openRouterModel) { setSelectedModel(config.openRouterModel); } if (config.aiProvider) setAiProvider(config.aiProvider); if (config.googleAiKey) { setGoogleAiKey(config.googleAiKey); fetchGoogleModels(config.googleAiKey); } if (config.googleAiModel) setGoogleAiModel(config.googleAiModel); } }, [fetchModels, fetchGoogleModels, setApiKey, setDatabase, setHost, setPassword, setPort, setSelectedModel, setSsl, setUser]); useEffect(() => { setIsMounted(true); reloadFromServer(); }, [reloadFromServer]); const testDb = async () => { setDbStatus("testing"); setDbError(null); const url = getDatabaseUrl(); const res = await testDatabaseConnection(url); if (res.success) { setDbStatus("ok"); } else { setDbStatus("error"); setDbError(res.error || "Erreur de connexion inconnue"); } }; const handleSave = async () => { setSaveStatus("saving"); const url = getDatabaseUrl(); const res = await saveDatabaseSettings(url, apiKey, selectedModel, aiProvider, googleAiKey, googleAiModel); if (res.success) { setSaveStatus("saved"); setTimeout(() => setSaveStatus("idle"), 2500); } else { setSaveStatus("idle"); alert("Erreur lors de la sauvegarde : " + res.error); } }; if (!isMounted) { return null; } return (

Paramètres

Configuration de la connexion et des services.

{/* PostgreSQL */}
setHost(e.target.value)} className="apple-input font-mono" />
setPort(e.target.value)} className="apple-input font-mono" /> setDatabase(e.target.value)} className="apple-input font-mono" />
setUser(e.target.value)} className="apple-input font-mono" /> setPassword(e.target.value)} className="apple-input font-mono" />
setSsl(e.target.checked)} className="w-3.5 h-3.5 rounded accent-teal-500 cursor-pointer" />
{dbStatus === "error" && (
Connexion refusée {dbError && {dbError}}
)} {dbStatus === "ok" && ( Connexion réussie ! )}
{/* API FF Nancy */} {/* AI Provider selector */}
{aiProvider === "google" && ( <>
setGoogleAiKey(e.target.value)} className="apple-input font-mono pr-10" />
0 ? ` (${googleModels.length} disponibles)` : ""}`} hint={googleModelsStatus === "error" ? "⚠ Erreur — vérifiez votre clé puis rechargez" : googleModelsStatus === "idle" ? "Entrez votre clé et cliquez « Charger les modèles »" : undefined} > {googleModels.length > 0 ? ( ) : ( setGoogleAiModel(e.target.value)} placeholder="ex: gemini-2.0-flash" className="apple-input" /> )} )} {aiProvider === "openrouter" && (

Utilise la clé OpenRouter configurée ci-dessous et le modèle sélectionné.

)}
{/* OpenRouter */}
setApiKey(e.target.value)} className="apple-input font-mono pr-10" />
0 ? ` (${models.length} disponibles)` : ""}`} hint={modelsStatus === "error" ? "⚠ Erreur — vérifiez votre clé puis rechargez" : modelsStatus === "idle" ? "Entrez votre clé et cliquez « Charger les modèles »" : undefined} > {models.length > 0 ? ( ) : ( setSelectedModel(e.target.value)} placeholder="ex: google/gemini-2.0-flash-001" className="apple-input" /> )}
{/* Apparence */}

Thème

Interface claire ou sombre

{/* Segment control */}

Densité de la grille

Hauteur de ligne par défaut

{/* Score Produit */}

Formule : Volume (50pts) + CA (30pts) + Marge (20pts) + Bonus relatif (±10pts)

Le score est basé sur des paliers absolus sensibles au prix unitaire, avec un ajustement relatif selon la position dans le lot fournisseur.

Volume (0-50 pts) — unités / magasin / mois

CA (0-30 pts) — € / magasin / an

Marge (0-20 pts) — taux de marge %

Bonus relatif (±10 pts) — percentile composite dans le lot

Pénalité de régularité appliquée si inactivité prolongée. Score final borné 0-100.

{/* Gestion des Utilisateurs */}
{/* Save Button — Floating/Sticky style at bottom */}
); }