"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 && (
| Table |
Sync |
Lignes |
{status.tables.map((t) => (
| {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 (
);
}
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 */}
{/* API FF Nancy */}
{/* AI Provider selector */}
{/* OpenRouter */}
{/* 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 */}
);
}