"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, saveFfApiSettings, testFfApiConnection } from "@/features/settings/actions";
import { useEffect } from "react";
import { UserManagement } from "@/features/admin/components/user-management";
import { ApiKeyManagement } from "@/features/admin/components/api-key-management";
import { ApiConnectionInfo } from "@/features/admin/components/api-connection-info";
import { GridWarmup } from "@/features/admin/components/grid-warmup";
import { ServerLogs } from "@/features/settings/components/server-logs";
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 (
{label}
{children}
{hint &&
{hint}
}
);
}
interface FfSyncTable { nom: string; derniereSync: string; nbLignes?: number; statut?: string; erreur?: string | null; }
interface FfSyncStatus { lastSync: string; tables: FfSyncTable[]; }
function FfApiStatusSection() {
const [status, setStatus] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [url, setUrl] = useState("");
const [savedUrl, setSavedUrl] = useState(null);
const [saving, setSaving] = useState(false);
const [testedUrl, setTestedUrl] = useState(null);
// Charge l'URL enregistrée pour la préremplir (vide = valeur par défaut).
useEffect(() => {
getSavedDatabaseConfig()
.then((cfg) => {
setSavedUrl(cfg?.ffApiBaseUrl ?? null);
if (cfg?.ffApiBaseUrl) setUrl(cfg.ffApiBaseUrl);
})
.catch(() => { /* réglage optionnel : on laisse le champ vide */ });
}, []);
const runTest = useCallback((candidate?: string) => {
setLoading(true);
setError(null);
setStatus(null);
testFfApiConnection(candidate)
.then((res) => {
setTestedUrl(res.url);
if (res.success) setStatus(res.status as FfSyncStatus);
else setError(res.error ?? "Échec inconnu");
})
.catch((e) => setError(e instanceof Error ? e.message : "Erreur inconnue"))
.finally(() => setLoading(false));
}, []);
useEffect(() => { runTest(); }, [runTest]);
const save = async () => {
setSaving(true);
setError(null);
try {
const res = await saveFfApiSettings(url);
if (!res.success) { setError(res.error ?? "Enregistrement impossible"); return; }
setSavedUrl(url.trim() || null);
runTest(url);
} finally {
setSaving(false);
}
};
return (
setUrl(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") void save(); }}
className="apple-input w-full"
/>
{saving ? : }
Enregistrer
runTest(url)} disabled={loading} className="apple-btn-secondary h-9 px-4">
{loading ? : }
Tester
{status &&
Connexion OK}
{error &&
{error}}
{/* L'URL réellement appelée : évite de croire qu'on teste celle du champ
alors que le réglage enregistré ou la variable d'environnement prime. */}
{testedUrl && (
Adresse testée : {testedUrl}
{!savedUrl && <> — valeur par défaut, aucun réglage enregistré>}
)}
{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
État
{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") ?? "—"}
{/* Une table en erreur est précisément ce qu'on vient chercher ici. */}
{t.statut === "ok"
? ok
: {t.statut ?? "—"} }
))}
)}
)}
);
}
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 [dbStatus, setDbStatus] = useState<"idle" | "testing" | "ok" | "error">("idle");
const [dbError, setDbError] = useState(null);
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle");
const [isMounted, setIsMounted] = useState(false);
const {
host, setHost,
port, setPort,
database, setDatabase,
user, setUser,
password, setPassword,
ssl, setSsl,
getDatabaseUrl
} = useDbSettingsStore();
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);
}
}
}
}, [setDatabase, setHost, setPassword, setPort, setSsl, setUser]);
useEffect(() => {
// `isMounted` sert à éviter un écart d'hydratation : il doit donc être posé
// au montage, ce que la règle déconseille en général mais qui est ici le but.
// eslint-disable-next-line react-hooks/set-state-in-effect
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);
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 */}
{/* Apparence */}
Thème
Interface claire ou sombre
{/* Segment control */}
setTheme("light")} className={`segment-btn ${!isDark ? "active" : ""}`}>
Clair
setTheme("dark")} className={`segment-btn ${isDark ? "active" : ""}`}>
Sombre
Densité de la grille
Hauteur de ligne par défaut
setDisplayDensity(e.target.value as "compact" | "normal" | "comfortable")}
>
Compact (32px)
Normal (40px)
Spacieux (48px)
{/* Journaux serveur — diagnostic des extractions Qlik */}
{/* Gestion des Utilisateurs */}
{/* API publique /api/v1 — connexion puis gestion des clés */}
{/* Save Button — Floating/Sticky style at bottom */}
{saveStatus === "saving" ? (
) : saveStatus === "saved" ? (
) : (
)}
{saveStatus === "saving"
? "Enregistrement en cours..."
: saveStatus === "saved"
? "Configuration enregistrée !"
: "Sauvegarder la configuration"}
);
}