import { ActionIcon, Alert, Avatar, Badge, Box, Button, Card as MCard, Divider, Group, Loader, Modal, Paper, ScrollArea, Select, SimpleGrid, Stack, Table, Text, Textarea, Title, Tooltip, UnstyledButton, } from "@mantine/core"; import { BarChart } from "@mantine/charts"; import { IconAlertTriangle, IconArrowBackUp, IconCalendarStats, IconCheck, IconClock, IconDownload, IconHourglass, IconLock, IconPlus, IconRefresh, IconSettings, IconSparkles, IconTrendingUp, } from "@tabler/icons-react"; import { useEffect, useMemo, useState } from "react"; import { dailyReport, generateDailySummary, getDailySummary, getSetting, setSetting, type DailyReport as Report, type DailySummary, } from "../api"; import { formatDuration } from "./format"; import { tagColor } from "./colors"; interface Props { date: string; // YYYY-MM-DD onJumpToCard?: (cardId: string) => void; } const PROMPT_KEY = "daily_report_prompt"; const PROMPT_DEFAULT = "Eres un coach de equipo. Resume el reporte diario en un MAXIMO de 4 frases cortas, mencionando: (1) total de tareas hechas y quien destaco, (2) cualquier card reabierta o deadline vencido que merezca atencion, (3) cards estancadas criticas (30+ dias) si las hay, (4) una frase corta de animo o aviso si toca. Tono natural, primera persona del plural, sin emojis. No inventes datos; usa solo los del JSON del reporte."; function fmtDate(s: string): string { try { const d = new Date(s + "T00:00:00"); return d.toLocaleDateString("es-ES", { weekday: "long", day: "2-digit", month: "long", year: "numeric", }); } catch { return s; } } function KPI({ label, value, color, icon, sub, }: { label: string; value: string | number; color?: string; icon?: React.ReactNode; sub?: string; }) { return ( {icon} {label} {value} {sub && ( {sub} )} ); } function RankingList({ title, rows, emptyText, withAvatar = false, }: { title: string; rows: T[]; emptyText: string; withAvatar?: boolean; }) { return ( {title} {rows.length === 0 ? ( {emptyText} ) : ( {rows.map((r, i) => ( {withAvatar && ( {(r.name || "?").slice(0, 2).toUpperCase()} )} {r.name || "(sin nombre)"} {r.count} ))} )} ); } export function DailyReportView({ date, onJumpToCard }: Props) { const [data, setData] = useState(null); const [err, setErr] = useState(null); const [summary, setSummary] = useState(null); const [summaryLoading, setSummaryLoading] = useState(false); const [summaryErr, setSummaryErr] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [promptDraft, setPromptDraft] = useState(""); const [filterRequester, setFilterRequester] = useState(null); const [filterAssignee, setFilterAssignee] = useState(null); useEffect(() => { setData(null); setErr(null); dailyReport(date) .then(setData) .catch((e) => setErr((e as Error).message)); setSummary(null); setSummaryErr(null); getDailySummary(date) .then((s) => setSummary(s.exists ? s : null)) .catch(() => {}); }, [date]); const regenerateSummary = async () => { setSummaryLoading(true); setSummaryErr(null); try { const s = await generateDailySummary(date); setSummary({ ...s, exists: true }); } catch (e) { setSummaryErr((e as Error).message); } finally { setSummaryLoading(false); } }; const openSettings = async () => { try { const s = await getSetting(PROMPT_KEY); setPromptDraft(s.value || PROMPT_DEFAULT); } catch { setPromptDraft(PROMPT_DEFAULT); } setSettingsOpen(true); }; const saveSettings = async () => { await setSetting(PROMPT_KEY, promptDraft); setSettingsOpen(false); }; const resetSettings = () => setPromptDraft(PROMPT_DEFAULT); const hourlyChartData = useMemo(() => { if (!data) return []; return data.hourly_moves.map((n, h) => ({ hora: String(h).padStart(2, "0") + ":00", movimientos: n, })); }, [data]); const requesterOptions = useMemo(() => { if (!data) return []; const set = new Set(); for (const c of data.done_cards) if (c.requester) set.add(c.requester); return Array.from(set).sort(); }, [data]); const assigneeOptions = useMemo(() => { if (!data) return []; const m = new Map(); 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)}

` : ""} ${rows || ''}
# Titulo Solicitante Asignado Tags Lead time
Sin tareas que cumplan el filtro.
Generado por kanban · ${escape(origin)}
`; 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) : "—"} {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.