mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
fix(qlik): track async sync job status
This commit is contained in:
1 parent
e7bcba169c
commit
850852912f
2 files changed
+406
-57
No files matched your search
+186
-38
@@ -5,28 +5,107 @@ import { upsertNetworkMetrics } from "@/lib/qlik-network-cache";
|
||||
import { pgGetArticlesByFournisseur } from "@/lib/pg-ff-client";
|
||||
import { buildGridNetworkQlikDateFilter } from "@/lib/qlik-date-range";
|
||||
|
||||
// Synchro potentiellement longue (extraction hypercube paginee).
|
||||
// Tâche d'extraction Qlik potentiellement très longue (hypercube paginé).
|
||||
// On accepte quand même 5 min côté plateforme Next.js, mais on rend la main au
|
||||
// client tout de suite via le state job ci-dessous.
|
||||
export const maxDuration = 300;
|
||||
// Forcer le runtime Node.js : Playwright/Chromium ne tourne pas en edge et on a
|
||||
// besoin d'un process persistant pour conserver l'état de job en mémoire.
|
||||
export const runtime = "nodejs";
|
||||
// Toujours exécuter dynamiquement (sinon Next peut essayer de cacher des POST).
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/** État d'un job de sync Qlik pour un fournisseur donné. */
|
||||
type QlikSyncStatus = "idle" | "running" | "success" | "error";
|
||||
|
||||
interface QlikSyncJob {
|
||||
jobId: string;
|
||||
fournisseur: string;
|
||||
status: QlikSyncStatus;
|
||||
requested: number;
|
||||
fetched: number;
|
||||
upserted: number;
|
||||
periode?: string;
|
||||
dateDebut?: string;
|
||||
dateFin?: string;
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/qlik/sync?fournisseur=XXX
|
||||
* Tire de Qlik les metriques reseau des articles du fournisseur (par code centrale)
|
||||
* et met a jour le cache local. Admin uniquement.
|
||||
* Store en mémoire au niveau module. Une Map par fournisseur car on ne lance
|
||||
* qu'une extraction à la fois par fournisseur. C'est volontairement simple :
|
||||
* si le process redémarre, les jobs en cours sont perdus → GET renverra idle
|
||||
* et l'utilisateur pourra relancer. Suffisant pour corriger le bug UX.
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const jobs = new Map<string, QlikSyncJob>();
|
||||
|
||||
/** Construit un jobId court et lisible. */
|
||||
function newJobId(): string {
|
||||
return `qlik_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
/** Sérialise un job pour la réponse HTTP. */
|
||||
function publicJob(job: QlikSyncJob) {
|
||||
return {
|
||||
jobId: job.jobId,
|
||||
fournisseur: job.fournisseur,
|
||||
status: job.status,
|
||||
requested: job.requested,
|
||||
fetched: job.fetched,
|
||||
upserted: job.upserted,
|
||||
periode: job.periode,
|
||||
dateDebut: job.dateDebut,
|
||||
dateFin: job.dateFin,
|
||||
startedAt: job.startedAt,
|
||||
finishedAt: job.finishedAt,
|
||||
error: job.error,
|
||||
message: job.message,
|
||||
};
|
||||
}
|
||||
|
||||
/** État "idle" sérialisé (pas de job connu pour ce fournisseur). */
|
||||
function idleJob(fournisseur: string) {
|
||||
return {
|
||||
jobId: null,
|
||||
fournisseur,
|
||||
status: "idle" as const,
|
||||
requested: 0,
|
||||
fetched: 0,
|
||||
upserted: 0,
|
||||
periode: undefined,
|
||||
dateDebut: undefined,
|
||||
dateFin: undefined,
|
||||
startedAt: undefined,
|
||||
finishedAt: undefined,
|
||||
error: undefined,
|
||||
message: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'auth admin. Renvoie une réponse 403/401 si refusée, sinon null.
|
||||
*/
|
||||
async function requireAdmin(): Promise<NextResponse | null> {
|
||||
const session = await auth();
|
||||
if (!session || (session.user as { role?: string } | undefined)?.role !== "admin") {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const fournisseur = req.nextUrl.searchParams.get("fournisseur");
|
||||
if (!fournisseur) {
|
||||
return NextResponse.json({ error: "Param 'fournisseur' requis" }, { status: 400 });
|
||||
if ((session.user as { role?: string } | undefined)?.role !== "admin") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute réellement l'extraction + l'upsert, en mettant à jour le job
|
||||
* au fur et à mesure (compteur fetched, puis upserted, puis finishedAt).
|
||||
*/
|
||||
async function runJob(job: QlikSyncJob): Promise<void> {
|
||||
try {
|
||||
// Codes centraux des articles de ce fournisseur
|
||||
const articles = await pgGetArticlesByFournisseur(fournisseur);
|
||||
const articles = await pgGetArticlesByFournisseur(job.fournisseur);
|
||||
const codes = [
|
||||
...new Set(
|
||||
articles
|
||||
@@ -34,38 +113,107 @@ export async function POST(req: NextRequest) {
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
console.log(`[api/qlik/sync] fournisseur=${fournisseur} — ${articles.length} articles, ${codes.length} codes centraux uniques. Échantillon: ${JSON.stringify(codes.slice(0, 5))}`);
|
||||
job.requested = codes.length;
|
||||
console.log(
|
||||
`[api/qlik/sync] job=${job.jobId} fournisseur=${job.fournisseur} — ${articles.length} articles, ${codes.length} codes centraux uniques. Échantillon: ${JSON.stringify(codes.slice(0, 5))}`,
|
||||
);
|
||||
if (codes.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
fournisseur,
|
||||
fetched: 0,
|
||||
upserted: 0,
|
||||
message: "Aucun article avec code centrale pour ce fournisseur",
|
||||
fetchedAt: new Date().toISOString(),
|
||||
});
|
||||
job.status = "success";
|
||||
job.message = "Aucun article avec code centrale pour ce fournisseur";
|
||||
job.finishedAt = new Date().toISOString();
|
||||
return;
|
||||
}
|
||||
|
||||
const dateFilter = buildGridNetworkQlikDateFilter();
|
||||
console.log(`[api/qlik/sync] → lancement extraction Qlik pour ${codes.length} codes — période ${dateFilter.label} (${dateFilter.dateDebut} → ${dateFilter.dateFin})…`);
|
||||
job.periode = dateFilter.label;
|
||||
job.dateDebut = dateFilter.dateDebut;
|
||||
job.dateFin = dateFilter.dateFin;
|
||||
console.log(
|
||||
`[api/qlik/sync] job=${job.jobId} → extraction Qlik pour ${codes.length} codes — période ${dateFilter.label} (${dateFilter.dateDebut} → ${dateFilter.dateFin})…`,
|
||||
);
|
||||
const metrics = await fetchNetworkMetricsPlaywright(codes, undefined, dateFilter);
|
||||
console.log(`[api/qlik/sync] ← Qlik a renvoyé ${metrics.size} produits réseau`);
|
||||
job.fetched = metrics.size;
|
||||
console.log(`[api/qlik/sync] job=${job.jobId} ← Qlik a renvoyé ${metrics.size} produits réseau`);
|
||||
const count = await upsertNetworkMetrics([...metrics.values()]);
|
||||
console.log(`[api/qlik/sync] ${count} lignes upsert dans le cache`);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
fournisseur,
|
||||
requested: codes.length,
|
||||
fetched: metrics.size,
|
||||
upserted: count,
|
||||
periode: dateFilter.label,
|
||||
dateDebut: dateFilter.dateDebut,
|
||||
dateFin: dateFilter.dateFin,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
});
|
||||
job.upserted = count;
|
||||
console.log(`[api/qlik/sync] job=${job.jobId} ${count} lignes upsert dans le cache`);
|
||||
job.status = "success";
|
||||
job.finishedAt = new Date().toISOString();
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("[api/qlik/sync]", msg);
|
||||
return NextResponse.json({ success: false, error: msg }, { status: 500 });
|
||||
console.error(`[api/qlik/sync] job=${job.jobId}`, msg);
|
||||
job.status = "error";
|
||||
job.error = msg;
|
||||
job.finishedAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/qlik/sync?fournisseur=XXX
|
||||
* Retourne l'état courant du job pour ce fournisseur. Si aucun job n'est
|
||||
* connu, renvoie un état "idle" (avec un message explicite). Permet au
|
||||
* frontend de reprendre l'état après un reload et de poller en arrière-plan.
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireAdmin();
|
||||
if (denied) return denied;
|
||||
|
||||
const fournisseur = req.nextUrl.searchParams.get("fournisseur");
|
||||
if (!fournisseur) {
|
||||
return NextResponse.json({ error: "Param 'fournisseur' requis" }, { status: 400 });
|
||||
}
|
||||
const job = jobs.get(fournisseur);
|
||||
if (!job) {
|
||||
return NextResponse.json({ success: true, ...idleJob(fournisseur) });
|
||||
}
|
||||
return NextResponse.json({ success: true, ...publicJob(job) });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/qlik/sync?fournisseur=XXX
|
||||
* Démarre (ou réutilise si déjà running) un job d'extraction en arrière-plan
|
||||
* et retourne *immédiatement* { success, status, jobId, ... }. Le client doit
|
||||
* ensuite poller GET pour suivre l'avancement et être notifié de la fin.
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const denied = await requireAdmin();
|
||||
if (denied) return denied;
|
||||
|
||||
const fournisseur = req.nextUrl.searchParams.get("fournisseur");
|
||||
if (!fournisseur) {
|
||||
return NextResponse.json({ error: "Param 'fournisseur' requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = jobs.get(fournisseur);
|
||||
if (existing && existing.status === "running") {
|
||||
// Un job tourne déjà pour ce fournisseur : on le réutilise.
|
||||
return NextResponse.json({ success: true, ...publicJob(existing) });
|
||||
}
|
||||
|
||||
const job: QlikSyncJob = {
|
||||
jobId: newJobId(),
|
||||
fournisseur,
|
||||
status: "running",
|
||||
requested: 0,
|
||||
fetched: 0,
|
||||
upserted: 0,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
jobs.set(fournisseur, job);
|
||||
console.log(`[api/qlik/sync] job=${job.jobId} démarré pour fournisseur=${fournisseur}`);
|
||||
|
||||
// Lancement fire-and-forget. On ne `await` PAS l'extraction ici : on
|
||||
// renvoie tout de suite au client et il polle GET. Les erreurs sont
|
||||
// capturées dans runJob et inscrites dans le job.
|
||||
void runJob(job).catch((e) => {
|
||||
// Filet de sécurité (runJob ne devrait pas throw, mais on ne veut
|
||||
// jamais qu'une exception non capturée crashe le process).
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error(`[api/qlik/sync] job=${job.jobId} crash non géré`, msg);
|
||||
job.status = "error";
|
||||
job.error = msg;
|
||||
job.finishedAt = new Date().toISOString();
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, ...publicJob(job) });
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Loader2, RefreshCw, CheckCircle, AlertCircle } from "lucide-react";
|
||||
|
||||
@@ -11,6 +11,23 @@ interface SyncQlikButtonProps {
|
||||
lastUpdate?: string | null;
|
||||
}
|
||||
|
||||
/** État serveur renvoyé par GET/POST /api/qlik/sync */
|
||||
interface SyncJobState {
|
||||
jobId: string | null;
|
||||
fournisseur: string;
|
||||
status: "idle" | "running" | "success" | "error";
|
||||
requested: number;
|
||||
fetched: number;
|
||||
upserted: number;
|
||||
periode?: string;
|
||||
dateDebut?: string;
|
||||
dateFin?: string;
|
||||
startedAt?: string;
|
||||
finishedAt?: string;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
function formatDate(iso?: string | null): string {
|
||||
if (!iso) return "jamais";
|
||||
const d = new Date(iso);
|
||||
@@ -21,34 +38,213 @@ function formatDate(iso?: string | null): string {
|
||||
/**
|
||||
* Bouton admin : synchronise depuis Qlik les métriques réseau du fournisseur affiché
|
||||
* (POST /api/qlik/sync?fournisseur=…) puis recharge la grille. Affiche la dernière maj.
|
||||
*
|
||||
* UX : le POST démarre un job serveur en arrière-plan et revient immédiatement.
|
||||
* Le composant poll ensuite GET pour suivre l'avancement. Au mount, on appelle
|
||||
* GET pour reprendre l'état d'un éventuel job en cours (par ex. après reload de
|
||||
* la page) et continuer à afficher le spinner correctement.
|
||||
*/
|
||||
export function SyncQlikButton({ codeFournisseur, lastUpdate }: SyncQlikButtonProps) {
|
||||
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
const [status, setStatus] = useState<"idle" | "running" | "success" | "error">("idle");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
/** true tant qu'on doit suivre le job courant (false après succès/erreur traité). */
|
||||
const trackingRef = useRef<boolean>(false);
|
||||
|
||||
const clearPollTimer = useCallback(() => {
|
||||
if (pollTimerRef.current) {
|
||||
clearTimeout(pollTimerRef.current);
|
||||
pollTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Met à jour le label visible à droite du bouton selon l'état courant.
|
||||
*/
|
||||
const applyJobToUi = useCallback((j: SyncJobState | null) => {
|
||||
if (!j) return;
|
||||
if (j.status === "running") {
|
||||
setStatus("running");
|
||||
// Texte informatif pendant l'extraction
|
||||
const requestedTxt = j.requested > 0 ? ` (${j.requested} codes)` : "";
|
||||
setMessage(`Extraction Qlik en cours${requestedTxt}…`);
|
||||
} else if (j.status === "success") {
|
||||
setStatus("success");
|
||||
if (j.message) setMessage(j.message);
|
||||
else setMessage(`${j.upserted} produits réseau synchronisés`);
|
||||
} else if (j.status === "error") {
|
||||
setStatus("error");
|
||||
setMessage(j.error || "Erreur sync Qlik");
|
||||
} else {
|
||||
setStatus("idle");
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Force le refresh de la grille (avec cache-buster) et de la barre de résumé.
|
||||
* C'est l'équivalent de ce que faisait l'ancien code après un POST réussi.
|
||||
*/
|
||||
const refreshGrid = useCallback(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("_refresh", String(Date.now()));
|
||||
router.replace(`${pathname}?${params.toString()}`);
|
||||
router.refresh();
|
||||
}, [pathname, router, searchParams]);
|
||||
|
||||
/**
|
||||
* Polling GET pendant qu'un job tourne. S'arrête tout seul quand le job
|
||||
* passe en success/error, ou après un timeout de sécurité.
|
||||
*/
|
||||
const pollOnce = useCallback(async (): Promise<boolean> => {
|
||||
if (!codeFournisseur) return false;
|
||||
if (!trackingRef.current) return false;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/qlik/sync?fournisseur=${encodeURIComponent(codeFournisseur)}`,
|
||||
{ method: "GET", cache: "no-store" },
|
||||
);
|
||||
if (!res.ok) {
|
||||
// En cas d'erreur HTTP transitoire on retente au tour suivant.
|
||||
return true;
|
||||
}
|
||||
const data: SyncJobState = await res.json();
|
||||
applyJobToUi(data);
|
||||
|
||||
if (data.status === "running") {
|
||||
// Continue à poller
|
||||
return true;
|
||||
}
|
||||
if (data.status === "success" || data.status === "error") {
|
||||
// Terminé : on rafraîchit la grille, on coupe le polling.
|
||||
trackingRef.current = false;
|
||||
if (data.status === "success") {
|
||||
refreshGrid();
|
||||
}
|
||||
// Petit délai pour laisser l'utilisateur lire le message vert/rouge
|
||||
setTimeout(() => {
|
||||
setStatus((cur) => (cur === "success" || cur === "error" ? "idle" : cur));
|
||||
}, 4000);
|
||||
return false;
|
||||
}
|
||||
// idle : rien à faire
|
||||
trackingRef.current = false;
|
||||
return false;
|
||||
} catch {
|
||||
// Erreur réseau transitoire, on laisse le polling réessayer.
|
||||
return true;
|
||||
}
|
||||
}, [applyJobToUi, codeFournisseur, refreshGrid]);
|
||||
|
||||
/**
|
||||
* Boucle de polling : s'auto-réarme tant que pollOnce indique qu'il faut
|
||||
* continuer. Délai un peu plus long que d'habitude (3 s) pour ne pas
|
||||
* marteler le serveur pendant une extraction qui dure plusieurs minutes.
|
||||
*/
|
||||
const startPolling = useCallback(() => {
|
||||
clearPollTimer();
|
||||
const tick = async () => {
|
||||
const keepGoing = await pollOnce();
|
||||
if (keepGoing && trackingRef.current) {
|
||||
pollTimerRef.current = setTimeout(tick, 3000);
|
||||
} else {
|
||||
pollTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
// Premier appel quasi immédiat
|
||||
pollTimerRef.current = setTimeout(tick, 200);
|
||||
}, [clearPollTimer, pollOnce]);
|
||||
|
||||
/**
|
||||
* Au mount : si on a un fournisseur, on interroge l'API pour savoir si
|
||||
* un job tourne déjà (ex. reload de la page pendant l'extraction). Si
|
||||
* oui, on reprend l'état et on relance le polling.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!codeFournisseur) return;
|
||||
let cancelled = false;
|
||||
trackingRef.current = false;
|
||||
clearPollTimer();
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/qlik/sync?fournisseur=${encodeURIComponent(codeFournisseur)}`,
|
||||
{ method: "GET", cache: "no-store" },
|
||||
);
|
||||
if (cancelled) return;
|
||||
if (!res.ok) return;
|
||||
const data: SyncJobState = await res.json();
|
||||
if (cancelled) return;
|
||||
if (data.status === "running" && data.jobId) {
|
||||
trackingRef.current = true;
|
||||
applyJobToUi(data);
|
||||
startPolling();
|
||||
} else if (data.status === "success" || data.status === "error") {
|
||||
// Job terminé en amont (avant le mount) : on l'affiche brièvement
|
||||
applyJobToUi(data);
|
||||
setTimeout(() => {
|
||||
setStatus((cur) => (cur === "success" || cur === "error" ? "idle" : cur));
|
||||
}, 4000);
|
||||
}
|
||||
} catch {
|
||||
/* ignore : on n'empêche pas l'UI d'afficher le bouton */
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
trackingRef.current = false;
|
||||
clearPollTimer();
|
||||
};
|
||||
// On veut interroger une seule fois au mount (et quand le fournisseur change).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [codeFournisseur, clearPollTimer]);
|
||||
|
||||
/**
|
||||
* Cleanup : annule un timer de polling éventuel au démontage.
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearPollTimer();
|
||||
};
|
||||
}, [clearPollTimer]);
|
||||
|
||||
const handleSync = async () => {
|
||||
if (!codeFournisseur) {
|
||||
setStatus("error");
|
||||
setMessage("Aucun fournisseur sélectionné");
|
||||
setTimeout(() => setStatus("idle"), 6000);
|
||||
return;
|
||||
}
|
||||
setStatus("loading");
|
||||
setMessage("");
|
||||
// Empêche un double-clic pendant qu'un job tourne déjà.
|
||||
if (status === "running") return;
|
||||
|
||||
setStatus("running");
|
||||
setMessage("Démarrage de l'extraction Qlik…");
|
||||
try {
|
||||
const res = await fetch(`/api/qlik/sync?fournisseur=${encodeURIComponent(codeFournisseur)}`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) throw new Error(data.error || `HTTP ${res.status}`);
|
||||
setStatus("success");
|
||||
setMessage(`${data.upserted} produits réseau synchronisés`);
|
||||
// Bump _refresh → grid-client refetch /api/grid/rows avec refresh=1 (forceRefresh,
|
||||
// bypass cache 10 min de getProductRows) pour afficher les nouvelles métriques réseau.
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("_refresh", String(Date.now()));
|
||||
router.replace(`${pathname}?${params.toString()}`);
|
||||
router.refresh();
|
||||
const res = await fetch(
|
||||
`/api/qlik/sync?fournisseur=${encodeURIComponent(codeFournisseur)}`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
const data: SyncJobState & { success?: boolean } = await res.json();
|
||||
if (!res.ok || (data as { success?: boolean }).success === false) {
|
||||
throw new Error(data.error || `HTTP ${res.status}`);
|
||||
}
|
||||
// Si le serveur nous dit que c'est déjà en cours (jobId existant),
|
||||
// on reprend juste le suivi.
|
||||
if (data.status === "running" && data.jobId) {
|
||||
trackingRef.current = true;
|
||||
applyJobToUi(data);
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
// Status terminal direct (cas rare : 0 codes → success immédiat)
|
||||
applyJobToUi(data);
|
||||
if (data.status === "success") {
|
||||
refreshGrid();
|
||||
}
|
||||
setTimeout(() => setStatus("idle"), 4000);
|
||||
} catch (e) {
|
||||
setStatus("error");
|
||||
@@ -62,16 +258,21 @@ export function SyncQlikButton({ codeFournisseur, lastUpdate }: SyncQlikButtonPr
|
||||
<span
|
||||
className="text-[10px] leading-tight text-right whitespace-nowrap hidden lg:block"
|
||||
style={{ color: status === "error" ? "var(--accent-error)" : "var(--text-muted)" }}
|
||||
title={message || undefined}
|
||||
>
|
||||
{status === "error" ? message : `MAJ Qlik · ${formatDate(lastUpdate)}`}
|
||||
{status === "error"
|
||||
? message
|
||||
: status === "running"
|
||||
? (message || `Extraction Qlik…`)
|
||||
: `MAJ Qlik · ${formatDate(lastUpdate)}`}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleSync}
|
||||
disabled={status === "loading"}
|
||||
disabled={status === "running"}
|
||||
className="btn-action btn-action-secondary flex items-center gap-1.5 disabled:opacity-60"
|
||||
title={message || `Synchroniser les données réseau Qlik · MAJ ${formatDate(lastUpdate)}`}
|
||||
>
|
||||
{status === "loading" ? (
|
||||
{status === "running" ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : status === "success" ? (
|
||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
|
||||
@@ -80,7 +281,7 @@ export function SyncQlikButton({ codeFournisseur, lastUpdate }: SyncQlikButtonPr
|
||||
) : (
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
Sync Qlik
|
||||
{status === "running" ? "Sync en cours…" : "Sync Qlik"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user