();
for (const c of data.done_cards) {
if (c.assignee_id) m.set(c.assignee_id, c.assignee_name || c.assignee_id);
}
return Array.from(m.entries()).map(([value, label]) => ({ value, label }));
}, [data]);
const filteredDoneCards = useMemo(() => {
if (!data) return [];
return data.done_cards.filter((c) => {
if (filterRequester && c.requester !== filterRequester) return false;
if (filterAssignee && c.assignee_id !== filterAssignee) return false;
return true;
});
}, [data, filterRequester, filterAssignee]);
const exportPDF = () => {
if (!data) return;
const win = window.open("", "_blank");
if (!win) return;
const origin = window.location.origin;
const dateLabel = (() => {
try {
return new Date(data.date + "T00:00:00").toLocaleDateString("es-ES", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
});
} catch {
return data.date;
}
})();
const filterSub: string[] = [];
if (filterRequester) filterSub.push(`solicitante=${filterRequester}`);
if (filterAssignee) {
const a = assigneeOptions.find((o) => o.value === filterAssignee);
filterSub.push(`asignado=${a?.label || filterAssignee}`);
}
const escape = (s: string) =>
s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
const rows = filteredDoneCards
.map((c) => {
const tags = (c.tags || []).map(escape).join(", ");
const link = `${origin}/?card=${c.id}`;
return `
| ${String(c.seq_num).padStart(5, "0")} |
${escape(c.title)} |
${escape(c.requester || "")} |
${escape(c.assignee_name || "")} |
${escape(tags)} |
${formatDuration(c.lead_time_ms)} |
`;
})
.join("");
const html = `
Reporte ${data.date}
Reporte diario · ${escape(dateLabel)}
${escape(data.date)} · ${escape(data.tz)}${
filterSub.length ? " · filtros: " + filterSub.map(escape).join(", ") : ""
}
Hechas
${filteredDoneCards.length}
Lead time avg
${formatDuration(data.lead_time.avg_ms)}
Deadlines on-time
${data.deadlines.met}/${data.deadlines.met + data.deadlines.missed}
Reabiertas
${data.kpis.reopened}
${summary?.summary ? `${escape(summary.summary)}
` : ""}
| # |
Titulo |
Solicitante |
Asignado |
Tags |
Lead time |
${rows || '| Sin tareas que cumplan el filtro. |
'}
`;
win.document.write(html);
win.document.close();
};
if (err) {
return (
}>
{err}
);
}
if (!data) {
return (
);
}
const k = data.kpis;
const onTimePct =
k.deadlines_met + k.deadlines_missed > 0
? Math.round((k.deadlines_met / (k.deadlines_met + k.deadlines_missed)) * 100)
: null;
return (
Reporte diario
{fmtDate(data.date)}
} />
} />
} />
}
/>
0 ? "orange" : undefined}
icon={}
/>
= 80 ? "teal" : "red"}
sub={`${k.deadlines_met} on-time / ${k.deadlines_missed} vencidos`}
icon={}
/>
{/* Bocadillo de agente — encima de tareas hechas */}
{summaryErr && (
}>
{summaryErr}
)}
{summaryLoading ? (
Generando resumen…
) : summary?.summary ? (
<>
{summary.summary}
{summary.generated_at && (
Generado {new Date(summary.generated_at).toLocaleString()} · {summary.model}
)}
>
) : (
Aun no hay resumen del dia. Pulsa "Generar".
)}
Tareas hechas
N {filteredDoneCards.length}
{filteredDoneCards.length !== data.done_cards.length ? ` / ${data.done_cards.length}` : ""}
Lead time avg {data.lead_time.samples > 0 ? formatDuration(data.lead_time.avg_ms) : "—"} · p50{" "}
{data.lead_time.samples > 0 ? formatDuration(data.lead_time.p50_ms) : "—"} · p95{" "}
{data.lead_time.samples > 0 ? formatDuration(data.lead_time.p95_ms) : "—"}
}
variant="light"
onClick={exportPDF}
data-test="daily-report-pdf"
>
PDF
{filteredDoneCards.length === 0 ? (
Sin hechas en este dia.
) : (
#
Titulo
Solicitante
Asignado
Tags
Lead time
{filteredDoneCards.map((c) => (
{String(c.seq_num).padStart(5, "0")}
onJumpToCard?.(c.id)} style={{ textAlign: "left" }}>
{c.title}
{c.requester || "—"}
{c.assignee_name || "—"}
{(c.tags || []).slice(0, 3).map((t) => (
{t}
))}
{formatDuration(c.lead_time_ms)}
))}
)}
Movimientos por hora
{k.moves}
{k.moves === 0 ? (
Sin movimientos.
) : (
String(v)}
/>
)}
Tags trabajadas
{data.tags_done.length === 0 ? (
Sin tags.
) : (
{data.tags_done.map((t) => (
{t.name} · {t.count}
))}
)}
{data.reopened_cards.length > 0 && (
Reabiertas (Done → otra)
{data.reopened_cards.length}
{data.reopened_cards.map((r) => (
onJumpToCard?.(r.card_id)} style={{ minWidth: 0, flex: 1 }}>
{r.title}
{r.from_column} → {r.to_column}
{r.actor_name && (
{r.actor_name}
)}
))}
)}
{(data.deadlines.missed > 0 || data.deadlines.met > 0) && (
Deadlines
{data.deadlines.met} on-time
{data.deadlines.missed} vencidos
{data.deadlines.list.length > 0 && (
{data.deadlines.list.map((d) => (
onJumpToCard?.(d.card_id)} style={{ minWidth: 0, flex: 1 }}>
{d.title}
+{formatDuration(d.late_ms)} tarde
))}
)}
)}
Cards estancadas (al final del dia)
{data.stale_cards.d7.length}d7
{data.stale_cards.d14.length}d14
{data.stale_cards.d30.length}d30
7-13 dias
{data.stale_cards.d7.slice(0, 8).map((s) => (
onJumpToCard?.(s.card_id)}>
{s.title}{" "}
· {s.column_name} · {s.days}d
))}
{data.stale_cards.d7.length === 0 && (
Ninguna.
)}
14-29 dias
{data.stale_cards.d14.slice(0, 8).map((s) => (
onJumpToCard?.(s.card_id)}>
{s.title}{" "}
· {s.column_name} · {s.days}d
))}
{data.stale_cards.d14.length === 0 && (
Ninguna.
)}
30+ dias
{data.stale_cards.d30.slice(0, 8).map((s) => (
onJumpToCard?.(s.card_id)}>
{s.title}{" "}
· {s.column_name} · {s.days}d
))}
{data.stale_cards.d30.length === 0 && (
Ninguna.
)}
TZ: {data.tz} · cards archivadas hoy: {data.archived_today}
setSettingsOpen(false)} title="Prompt del agente diario" size="lg" zIndex={500}>
Plantilla que el agente recibe junto al JSON del reporte. Compartida por todos los usuarios.
);
}