feat(settings): configure Qlik credentials in the UI

Add a "Qlik Sense — Données réseau" section in Paramètres (host/user/password
+ Save & Test buttons), stored in data/.db-config.json. qlik-client reads this
file first (env fallback), so no env vars needed for the sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-20 23:28:55 +02:00
1 parent 2ffa7dd1c0
commit abed369bfb
3 files changed
+142 -5

No files matched your search

+79 -1
View File
@@ -5,7 +5,7 @@ import { Save, Eye, EyeOff, CheckCircle, AlertCircle, Loader2, RefreshCw, Sun, M
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 } from "@/features/settings/actions";
import { testDatabaseConnection, saveDatabaseSettings, getSavedDatabaseConfig, saveQlikSettings, testQlikConnection } from "@/features/settings/actions";
import { useEffect } from "react";
import { UserManagement } from "@/features/admin/components/user-management";
@@ -106,6 +106,82 @@ function FfApiStatusSection() {
);
}
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 (
<Section title="Qlik Sense — Données réseau" subtitle="Identifiants pour la synchro des données réseau (~270 magasins). Auth NTLM ; laisser l'hôte vide pour le défaut.">
<Field label="Hôte" hint="Défaut : reporting-magasins.lafoirfouille.fr (laisser vide pour ce défaut)">
<input type="text" placeholder="reporting-magasins.lafoirfouille.fr" value={host}
onChange={(e) => setHost(e.target.value)} className="apple-input font-mono" />
</Field>
<Field label="Utilisateur" hint="Ex : FFSCH (sans domaine)">
<input type="text" placeholder="FFSCH" value={user}
onChange={(e) => setUser(e.target.value)} className="apple-input font-mono" />
</Field>
<Field label="Mot de passe">
<div className="relative">
<input type={showPwd ? "text" : "password"} placeholder="••••••••" value={password}
onChange={(e) => setPassword(e.target.value)} className="apple-input font-mono pr-10" />
<button type="button" onClick={() => setShowPwd((v) => !v)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--text-muted)]">
{showPwd ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</Field>
{status && (
<div className={`flex items-center gap-1.5 text-[12px] ${status.type === "ok" ? "text-emerald-600" : "text-rose-600"}`}>
{status.type === "ok" ? <CheckCircle className="w-4 h-4" /> : <AlertCircle className="w-4 h-4" />}
{status.msg}
</div>
)}
<div className="flex gap-2">
<button onClick={handleSave} disabled={saving}
className="btn-action btn-action-primary flex items-center gap-1.5 disabled:opacity-60">
{saving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
Enregistrer
</button>
<button onClick={handleTest} disabled={testing || !user || !password}
className="btn-action btn-action-secondary flex items-center gap-1.5 disabled:opacity-60">
{testing ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <RefreshCw className="w-3.5 h-3.5" />}
Tester
</button>
</div>
</Section>
);
}
export default function SettingsPage() {
const { resolvedTheme, setTheme } = useTheme();
const isDark = resolvedTheme === "dark";
@@ -336,6 +412,8 @@ export default function SettingsPage() {
{/* API FF Nancy */}
<FfApiStatusSection />
<QlikSettingsSection />
{/* AI Provider selector */}
<Section title="Admin AI Chat — Fournisseur" subtitle="Choisissez le fournisseur IA utilisé dans l'onglet Admin AI Chat">
<div className="flex gap-2">
+45 -1
View File
@@ -14,6 +14,20 @@ export interface DbConfig {
aiProvider?: "openrouter" | "google";
googleAiKey?: string;
googleAiModel?: string;
/** Qlik Sense — données réseau */
qlikHost?: string;
qlikUser?: string;
qlikPassword?: string;
}
/** Lit la config existante (ou {} si absente). */
async function readConfig(): Promise<Partial<DbConfig>> {
try {
const data = await fs.readFile(CONFIG_FILE, "utf-8");
return JSON.parse(data);
} catch {
return {};
}
}
export async function testDatabaseConnection(url: string) {
@@ -46,7 +60,9 @@ export async function saveDatabaseSettings(
googleAiModel?: string,
) {
try {
const config: DbConfig = { url, openRouterKey, openRouterModel, aiProvider, googleAiKey, googleAiModel };
// Merge avec l'existant pour préserver les autres réglages (ex: Qlik)
const existing = await readConfig();
const config: DbConfig = { ...existing, url, openRouterKey, openRouterModel, aiProvider, googleAiKey, googleAiModel };
// S'assurer que le dossier data existe
await fs.mkdir(DATA_DIR, { recursive: true });
@@ -67,6 +83,34 @@ export async function saveDatabaseSettings(
}
}
export async function saveQlikSettings(qlikHost: string, qlikUser: string, qlikPassword: string) {
try {
const existing = await readConfig();
const config = { ...existing, qlikHost, qlikUser, qlikPassword } as DbConfig;
await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
console.log("[Settings] Qlik configuration saved.");
return { success: true };
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : String(error);
console.error("[Settings] Failed to save Qlik config:", msg);
return { success: false, error: msg };
}
}
export async function testQlikConnection(qlikHost: string, qlikUser: string, qlikPassword: string) {
try {
const { getQlikConfig, qlikNtlmSession } = await import("@/lib/qlik-client");
const cfg = { ...getQlikConfig(), user: qlikUser, password: qlikPassword };
if (qlikHost) cfg.host = qlikHost;
await qlikNtlmSession(cfg);
return { success: true };
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : String(error);
return { success: false, error: msg };
}
}
export async function getSavedDatabaseConfig(): Promise<DbConfig | null> {
try {
if (!(await fs.stat(CONFIG_FILE).catch(() => null))) {
+18 -3
View File
@@ -19,6 +19,19 @@
import httpntlm from "httpntlm";
import { WebSocket } from "ws";
import { randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
/** Lit les réglages Qlik depuis data/.db-config.json (configurés dans Paramètres). */
function readQlikFileConfig(): { qlikHost?: string; qlikUser?: string; qlikPassword?: string } {
try {
const raw = readFileSync(join(process.cwd(), "data", ".db-config.json"), "utf-8");
const c = JSON.parse(raw);
return { qlikHost: c.qlikHost, qlikUser: c.qlikUser, qlikPassword: c.qlikPassword };
} catch {
return {};
}
}
// ---------------------------------------------------------------------------
// Config (env) — defauts issus de la discovery
@@ -40,10 +53,12 @@ export interface QlikConfig {
}
export function getQlikConfig(): QlikConfig {
// Priorité : réglages UI (data/.db-config.json) > env > défaut
const f = readQlikFileConfig();
return {
host: process.env.QLIK_HOST ?? "reporting-magasins.lafoirfouille.fr",
user: process.env.QLIK_USER ?? "FFSCH",
password: process.env.QLIK_PWD ?? "",
host: f.qlikHost || process.env.QLIK_HOST || "reporting-magasins.lafoirfouille.fr",
user: f.qlikUser || process.env.QLIK_USER || "FFSCH",
password: f.qlikPassword || process.env.QLIK_PWD || "",
domain: process.env.QLIK_DOMAIN ?? "",
workstation: process.env.QLIK_WORKSTATION ?? "",
appNetwork: process.env.QLIK_APP_NETWORK ?? "9872ee6e-d64a-4b43-984a-076bf1f7f647",