+
+
+ Synchronisation nocturne
+
+
+ Les fournisseurs sont traités du plus anciennement synchronisé au plus récent. Si la fenêtre
+ se referme avant la fin, la nuit suivante reprend là où on s'est arrêté ; quand tout le
+ monde est passé, un nouveau tour recommence.
+
+
+ {erreur && (
+
+ )}
+
+ {/* ── Réglages ── */}
+ {settings && (
+
+
+
+ majSettings({ actif: e.target.checked })} className="w-4 h-4" />
+
+ Synchronisation nocturne active
+
+
+
+
+
+ De
+ majSettings({ heureDebut: Number(e.target.value) })}
+ className="w-16 rounded-lg px-2 py-1.5 text-[13px]"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
+
+
h à
+
+ À
+ majSettings({ heureFin: Number(e.target.value) })}
+ className="w-16 rounded-lg px-2 py-1.5 text-[13px]"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
+
+
h
+
+
+
+ Qlik / nuit
+ majSettings({ qlikParNuit: Number(e.target.value) })}
+ className="w-20 rounded-lg px-2 py-1.5 text-[13px]"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
+
+
+ Qlik si + vieux que
+ majSettings({ qlikMinJours: Number(e.target.value) })}
+ className="w-20 rounded-lg px-2 py-1.5 text-[13px]"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
+
+
+
+
+ {state?.enCours ? (
+
action("stop")} className="rounded-xl px-4 py-2 text-[13px] font-semibold flex items-center gap-2"
+ style={{ background: "var(--accent-error-bg)", color: "var(--accent-error)", border: "1px solid var(--accent-error)" }}>
+ Arrêter
+
+ ) : (
+
action("start", !dansLaFenetre)} className="rounded-xl px-4 py-2 text-[13px] font-semibold text-white flex items-center gap-2"
+ style={{ background: "var(--accent)" }}>
+ Lancer maintenant
+
+ )}
+
+
+
+ {dansLaFenetre
+ ? "Nous sommes actuellement dans la fenêtre."
+ : "Hors fenêtre — « Lancer maintenant » forcera un round immédiat."}
+ {" "}Une extraction Qlik peut durer 8 minutes : le plafond par nuit évite qu'elle
+ monopolise la fenêtre. Un fournisseur déjà démarré n'est jamais interrompu à l'heure de fin.
+
+
+ )}
+
+ {/* ── Round en cours ── */}
+ {state && (state.enCours || state.finAt) && (
+
+
+ {state.enCours
+ ?
+ : }
+
+ {state.enCours ? `Round ${state.origine} en cours` : "Dernier round terminé"}
+
+ {state.enCours && state.fournisseurCourant && (
+
+ — {state.etape === "qlik" ? "Qlik" : "SQL"} : {state.fournisseurCourant}
+
+ )}
+
+
+ SQL : {state.sqlFaits} ok{state.sqlEchecs > 0 && `, ${state.sqlEchecs} en échec`}
+ Qlik : {state.qlikFaits} ok{state.qlikEchecs > 0 && `, ${state.qlikEchecs} en échec`}
+ {state.debutAt && Début {fmtDate(state.debutAt)} }
+ {state.tourTermine && Tour bouclé — tout le monde est à jour }
+
+ {state.derniereErreur && (
+
Dernière erreur : {state.derniereErreur}
+ )}
+
+ )}
+
+ {/* ── Résumé ── */}
+ {resume && (
+
+ {[
+ { l: "Fournisseurs", v: resume.total },
+ { l: "Actifs SQL", v: resume.actifsSql },
+ { l: "Actifs Qlik", v: resume.actifsQlik },
+ { l: "Jamais synchronisés", v: resume.jamaisSql },
+ { l: "Désactivés auto", v: resume.desactivesAuto },
+ { l: "En échec", v: resume.enEchecSql + resume.enEchecQlik },
+ ].map((s) => (
+
+ {s.l} {s.v}
+
+ ))}
+
+ )}
+
+ {/* ── Filtres et actions de masse ── */}
+
+
+
+ setRecherche(e.target.value)} placeholder="Code ou nom"
+ className="rounded-lg pl-8 pr-3 py-1.5 text-[12px] w-56"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-primary)" }} />
+
+ {(["tous", "actifs", "inactifs", "echecs"] as const).map((f) => (
+
setFiltre(f)}
+ className="rounded-full px-3 py-1 text-[12px] font-semibold"
+ style={{
+ background: filtre === f ? "var(--accent)" : "var(--bg-elevated)",
+ color: filtre === f ? "#fff" : "var(--text-secondary)",
+ border: "1px solid var(--border)",
+ }}>
+ {f === "echecs" ? "en échec" : f}
+
+ ))}
+
+
{visibles.length} affichés :
+
basculer(codesVisibles, "actifSql", true)} className="rounded-lg px-2.5 py-1 text-[11px]"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-secondary)" }}>
+ Activer SQL
+
+
basculer(codesVisibles, "actifSql", false)} className="rounded-lg px-2.5 py-1 text-[11px]"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-secondary)" }}>
+ Désactiver SQL
+
+
basculer(codesVisibles, "actifQlik", false)} className="rounded-lg px-2.5 py-1 text-[11px]"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-secondary)" }}>
+ Désactiver Qlik
+
+
action("seed")} title="Récupérer les nouveaux fournisseurs du référentiel"
+ className="rounded-lg px-2.5 py-1 text-[11px] flex items-center gap-1"
+ style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-secondary)" }}>
+ Actualiser la liste
+
+
+
+ {/* ── Tableau ── */}
+
+ {chargement ? (
+
+ Chargement…
+
+ ) : visibles.length === 0 ? (
+
+ Aucun fournisseur. Utilisez « Actualiser la liste » pour importer le référentiel.
+
+ ) : (
+
+ )}
+
+