+
+
+ CTE {result.index}/{result.total}
+
+ {result.name}
+ {statusLabel(result.status)}
+ {result.execution_ms !== undefined && (
+ {result.execution_ms} ms
+ )}
+ {result.row_sample !== undefined && (
+ {result.row_sample} rows
+ )}
+
+
+ {result.warnings && result.warnings.length > 0 && (
+
+ {result.warnings.map((w, i) => (
+
+ {w}
+
+ ))}
+
+ )}
+
+ {(result.purpose || result.rationale || dependsOn.length > 0) && (
+
+ {result.purpose &&
{result.purpose}
}
+ {result.rationale &&
{result.rationale}
}
+ {dependsOn.length > 0 && (
+
+ {dependsOn.map((d) => (
+ {d}
+ ))}
+
+ )}
+
+ )}
+
+
+
+ {hasPreviewRows ? (
+
+ ) : (
+
No preview rows
+ )}
+
+ {showColumns && (
+
+ {result.columns!.map((c) => (
+
+
- {c.name}
+ - {c.description}
+
+ ))}
+
+ )}
+
+ {result.note &&
{result.note}
}
+
+ );
+}
diff --git a/frontend/src/viewers/PhaseSummaryViewer.test.tsx b/frontend/src/viewers/PhaseSummaryViewer.test.tsx
new file mode 100644
index 00000000..d3380f1a
--- /dev/null
+++ b/frontend/src/viewers/PhaseSummaryViewer.test.tsx
@@ -0,0 +1,92 @@
+import { render, screen } from "@testing-library/react";
+import { PhaseSummaryViewer } from "./PhaseSummaryViewer";
+import type { PhaseSummaryV2 } from "./artifactV2";
+
+const phase: PhaseSummaryV2 = {
+ schema_version: 2,
+ phase: { id: "F5", num: 5, name: "sintesi" },
+ summary: "## Recap\nLa catena copre pazienti e impianti.",
+ checks: [
+ { label: "Schema linking completo", status: "ok" },
+ { label: "Join validati", status: "warn", detail: "Un join manuale non testato" },
+ { label: "SQL eseguibile", status: "fail", detail: "Errore di sintassi" },
+ ],
+ sections: [
+ {
+ title: "Criteri di selezione",
+ items: [
+ {
+ label: "Pazienti attivi",
+ table: "pazienti",
+ column: "stato",
+ value: "IS TRUE",
+ kind: "filter",
+ description: "Flag di stato attivo",
+ rationale: "Solo pazienti in follow-up",
+ },
+ ],
+ },
+ ],
+ tables: [
+ {
+ name: "pazienti",
+ role: "promoted",
+ description: "Anagrafica pazienti",
+ columns: [{ name: "stato", description: "Flag attivo", value_filter: "IS TRUE" }],
+ },
+ { name: "log_accessi", role: "excluded", description: "Non pertinente" },
+ ],
+ open_questions: ["Serve confermare la finestra temporale?"],
+};
+
+test("renders phase id/name header and markdown summary", () => {
+ render(
+
+ {table.name}
+ {table.role}
+
+ {table.description &&
{table.description}
}
+ {table.columns && table.columns.length > 0 && (
+
+ {table.columns.map((c) => (
+
+ {c.name}
+ {c.description && {c.description}}
+ {c.value_filter && {c.value_filter}}
+
+ ))}
+
+ )}
+
+ );
+}
+
+export function PhaseSummaryViewer({ phase }: { phase: PhaseSummaryV2 }) {
+ return (
+
+
+ {phase.phase.id} — {phase.phase.name}
+
+
+ {phase.summary &&
}
+
+ {phase.checks && phase.checks.length > 0 && (
+
+ {phase.checks.map((c, i) => (
+
+ ))}
+
+ )}
+
+ {phase.sections && phase.sections.length > 0 && (
+
+ {phase.sections.map((s, i) => (
+
+ ))}
+
+ )}
+
+ {phase.tables && phase.tables.length > 0 && (
+
+ {phase.tables.map((t) => (
+
+ ))}
+
+ )}
+
+ {phase.open_questions && phase.open_questions.length > 0 && (
+
+
Open questions
+
+ {phase.open_questions.map((q, i) => (
+ - {q}
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/viewers/PreviewGrid.test.tsx b/frontend/src/viewers/PreviewGrid.test.tsx
new file mode 100644
index 00000000..8251ce2d
--- /dev/null
+++ b/frontend/src/viewers/PreviewGrid.test.tsx
@@ -0,0 +1,13 @@
+import { render, screen } from "@testing-library/react";
+import { PreviewGrid } from "./PreviewGrid";
+
+test("renders grid cells for the given columns and rows", () => {
+ render(
+