import { Anchor, Box, Group, HoverCard, Stack, Text } from "@mantine/core"; import { IconBrandJira, IconAlertCircle } from "@tabler/icons-react"; import { CSSProperties, useEffect, useState } from "react"; import * as api from "../api"; import type { CardJiraSyncState } from "../api"; import { formatDateTimeShort } from "./format"; // Adaptive polling: 5s steady-state, 1s while the card is mid-sync. The fast // cadence catches the yellow → green transition right after a column drag; // the slow cadence keeps the per-card load manageable when the board is idle. const POLL_MS_STEADY = 5000; const POLL_MS_INFLIGHT = 1000; type Tone = "gray" | "yellow" | "green" | "red"; function tone(state: CardJiraSyncState): Tone { if (state.inflight) return "yellow"; if (state.last_error) return "red"; if (state.jira_key) return "green"; return "gray"; } const TONE_COLOR: Record = { gray: "var(--mantine-color-gray-5)", yellow: "var(--mantine-color-yellow-5)", green: "var(--mantine-color-green-5)", red: "var(--mantine-color-red-6)", }; const TONE_LABEL: Record = { gray: "Sin sincronizar con Jira", yellow: "Sincronizando...", green: "Sincronizada con Jira", red: "Error de sincronizacion", }; interface Props { cardId: string; // Pollen-down so the parent can refresh when needed (e.g. after a move // animation finishes) without waiting for the next tick. refreshTick?: number; } export function JiraSyncIndicator({ cardId, refreshTick }: Props) { const [state, setState] = useState(null); const [err, setErr] = useState(null); useEffect(() => { let cancelled = false; let timer: ReturnType | null = null; const schedule = (ms: number) => { if (cancelled) return; if (timer) clearTimeout(timer); timer = setTimeout(load, ms); }; const load = async () => { try { const s = await api.getCardJiraSync(cardId); if (cancelled) return; setState(s); setErr(null); // Adaptive cadence: fast while the dispatcher is actively processing // this card, slow otherwise. Re-arms on every fetch so the moment // inflight flips off we drop back to the steady cadence. schedule(s.inflight ? POLL_MS_INFLIGHT : POLL_MS_STEADY); } catch (e) { if (cancelled) return; setErr((e as Error).message); schedule(POLL_MS_STEADY); } }; // Window event fired by the App's drag-end handler so the indicator // refetches immediately after a move (and so the user sees yellow within // ~200ms instead of waiting up to POLL_MS_STEADY). const onMoved = (e: Event) => { const detail = (e as CustomEvent).detail as { cardId?: string } | undefined; if (detail?.cardId === cardId) { schedule(150); } }; window.addEventListener("kanban-card-moved", onMoved); load(); return () => { cancelled = true; if (timer) clearTimeout(timer); window.removeEventListener("kanban-card-moved", onMoved); }; }, [cardId, refreshTick]); if (err && !state) { return ( ); } if (!state) { // Initial render — fade in a placeholder dot so the layout does not shift // when the fetch resolves. return ; } const t = tone(state); return ( e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} style={dotStyle(TONE_COLOR[t])} /> e.stopPropagation()}> {TONE_LABEL[t]} {state.issue_url && ( e.stopPropagation()} > Abrir en Jira )} {state.jira_key && ( Issue:{" "} {state.jira_key} )} {state.last_status && ( Status:{" "} {state.last_status} )} {state.last_sync_at && ( Ultimo sync:{" "} {formatDateTimeShort(state.last_sync_at)} )} {state.last_error && ( {state.last_error} )} {!state.jira_key && ( La card todavia no se ha empujado a Jira. Editala o muevela para disparar el sync, o usa la opcion "Importar de Jira" si ya existe alli. )} ); } function dotStyle(color: string): CSSProperties { return { width: 10, height: 10, borderRadius: "50%", background: color, cursor: "default", boxShadow: "0 0 0 2px var(--mantine-color-body)", transition: "background 120ms ease", }; }