Ir al contenido
EN

Report Studio

Report Studio es el motor de reportes propio de Zentto, diseñado como reemplazo de Crystal Reports. Permite visualizar, editar y crear reportes personalizados directamente desde el ERP web, sin dependencias externas de terceros.


El sistema se compone de paquetes npm independientes que se conectan en cascada:

@zentto/report-core Motor puro: tipos, engine 3-pass, expresiones, charts SVG
|
+-- @zentto/report-viewer Web component <zentto-report-viewer>
+-- @zentto/report-designer Web component <zentto-report-designer>
|
v
@zentto/shared-reports React wrappers: ReportViewer, ReportDesigner,
| PrintButton, layouts centralizados
v
@zentto/shared-api Hooks y servicios API (fetch, auth)
|
v
zentto-cache (Redis) Persistencia de layouts guardados
  1. Se carga un Layout JSON (desde codigo o desde BD via API).
  2. Si el layout define endpoint en sus dataSources, el DataFetchProvider obtiene datos reales de la API.
  3. El Band Engine ejecuta un renderizado de 3 pasadas:
    • Pass 1: Calcula dimensiones, resuelve grupos y running totals.
    • Pass 2: Pagina el contenido, calcula totalPages.
    • Pass 3: Renderiza el HTML final con page numbers correctos.
  4. El resultado (RenderResult) contiene un arreglo de paginas HTML.
  5. El ReportViewer muestra las paginas con toolbar de navegacion, zoom, impresion y descarga.

Un layout es un objeto JSON compatible con la interfaz ReportLayout de @zentto/report-core.

interface ReportLayout {
version: string; // "1.0"
name: string; // "Listado de Asientos Contables"
description?: string; // Descripcion del reporte
pageSize: PageSize; // { width: 210, height: 297, unit: "mm" }
margins: Margins; // { top: 12, right: 12, bottom: 12, left: 12 }
orientation?: "portrait" | "landscape";
dataSources: DataSourceDef[]; // Fuentes de datos
relations?: RelationDef[]; // JOINs entre fuentes
bands: Band[]; // Bandas del reporte
groups?: GroupDef[]; // Agrupaciones
sorting?: SortDef[]; // Ordenamiento
defaultStyle?: ElementStyle; // Estilos por defecto
styles?: Record<string, ElementStyle>; // Estilos nombrados
parameters?: ParameterDef[]; // Parametros de usuario
runningTotals?: RunningTotalDef[]; // Totales acumulados
conditionalFormats?: ConditionalFormatRule[];
subreports?: SubreportDef[]; // Sub-reportes
crossTabs?: CrossTabDef[]; // Tablas dinamicas
recordSelectionFormula?: string; // Filtro WHERE-style
metadata?: Record<string, unknown>;
}

Cada fuente de datos define de donde vienen los datos del reporte:

interface DataSourceDef {
id: string; // Identificador unico, ej: "header", "items"
name: string; // Nombre legible
type: "object" | "array"; // object = registro unico, array = lista
fields?: FieldDef[]; // Metadatos de campos
endpoint?: string; // Endpoint API para fetch automatico
endpointParams?: Record<string, string>; // Params para URLs con :id
}

Las bandas definen las secciones del reporte. Tipos disponibles:

BandaDescripcion
reportHeaderEncabezado del reporte (una vez al inicio)
pageHeaderEncabezado de cada pagina
groupHeaderEncabezado de grupo (al cambiar el campo agrupado)
columnHeaderTitulos de columnas
detailCuerpo: se repite por cada registro del dataSource
columnFooterPie de columnas
groupFooterPie de grupo (totales parciales)
pageFooterPie de cada pagina
reportFooterPie del reporte (totales generales, una vez al final)

Cada banda contiene elementos posicionados con coordenadas x/y/width/height:

TipoDescripcionProps clave
textTexto estaticocontent
fieldCampo vinculado a datosdataSource, field, format, expression, aggregate
imageImagen (URL o expresion)src, fit
lineLinea decorativax2, y2, lineStyle
rectRectangulolineStyle, fill, cornerRadius
barcodeCodigo de barras/QRbarcodeType (qr, code128, ean13, code39), value
chartGrafico SVGchartType, dataSource, labelField, valueFields
pageNumberNumero de paginaformat (ej: "Pagina {page} de {pages}")
currentDateFecha/hora actualformat (ej: "dd/MM/yyyy HH:mm")
subreportSub-reporte embebidosubreportId
crossTabTabla dinamica (pivot)crossTabId

Todos los elementos aceptan un objeto style opcional:

interface ElementStyle {
fontFamily?: string;
fontSize?: number; // en puntos
fontWeight?: "normal" | "bold" | number;
fontStyle?: "normal" | "italic";
textAlign?: "left" | "center" | "right" | "justify";
color?: string; // "#1a1a1a"
backgroundColor?: string;
borderTop?: string; // "1px solid #ccc"
borderBottom?: string;
padding?: number | string;
opacity?: number;
wordWrap?: boolean;
}

El campo format en elementos field acepta patrones:

PatronEjemploResultado
#,##0.001234.51,234.50
$#,##0.001234.5$1,234.50
dd/MM/yyyy2026-03-3030/03/2026
dd/MM/yyyy HH:mm2026-03-30T14:3030/03/2026 14:30
0.00%0.18518.50%

Los layouts del sistema se definen como constantes TypeScript en shared-reports:

web/modular-frontend/packages/shared-reports/src/layouts/
index.ts # Barrel: re-exporta todos los layouts
contabilidad/
asientos-list.ts # ASIENTOS_LIST_LAYOUT
libro-mayor.ts # LIBRO_MAYOR_LAYOUT
balance-comprobacion.ts # BALANCE_COMPROBACION_LAYOUT
libro-diario.ts # LIBRO_DIARIO_LAYOUT
plan-cuentas.ts # PLAN_CUENTAS_LAYOUT
inventario/
articulos.ts # ARTICULOS_LAYOUT
movimientos.ts # MOVIMIENTOS_INVENTARIO_LAYOUT
bancos/
bancos-list.ts # BANCOS_LIST_LAYOUT
cuentas-bancarias.ts # CUENTAS_BANCARIAS_LAYOUT
movimientos-bancarios.ts # MOVIMIENTOS_BANCARIOS_LAYOUT
caja-chica.ts # CAJA_CHICA_LAYOUT
ventas/
documentos-venta.ts # DOCUMENTOS_VENTA_LAYOUT
clientes.ts # CLIENTES_LAYOUT
cxc.ts # CXC_DOCUMENTOS_LAYOUT
compras/
documentos-compra.ts # DOCUMENTOS_COMPRA_LAYOUT
proveedores.ts # PROVEEDORES_LAYOUT
cxp.ts # CXP_DOCUMENTOS_LAYOUT
nomina/
empleados.ts # EMPLEADOS_LAYOUT
nominas.ts # NOMINAS_LAYOUT
conceptos.ts # CONCEPTOS_NOMINA_LAYOUT
vacaciones.ts # VACACIONES_LAYOUT
crm/
leads.ts # LEADS_LAYOUT
actividades.ts # ACTIVIDADES_CRM_LAYOUT
maestros/
categorias.ts # CATEGORIAS_LAYOUT
vendedores.ts # VENDEDORES_LAYOUT
almacenes.ts # ALMACENES_LAYOUT
centro-costo.ts # CENTRO_COSTO_LAYOUT
  • Archivo: kebab-case.ts — ej: asientos-list.ts
  • Export: UPPER_SNAKE_CASE — ej: ASIENTOS_LIST_LAYOUT
  • Cada archivo exporta exactamente 1 constante const
  1. Crear archivo en shared-reports/src/layouts/{modulo}/{nombre}.ts
  2. Definir la constante con la estructura ReportLayout
  3. Re-exportar desde shared-reports/src/layouts/index.ts
  4. Ejecutar npm run seed:reports en web/api/ para sincronizar con la BD

Ejemplo minimo:

shared-reports/src/layouts/ventas/pedidos.ts
export const PEDIDOS_LAYOUT = {
version: "1.0",
name: "Listado de Pedidos",
description: "Pedidos pendientes con totales",
pageSize: { width: 210, height: 297, unit: "mm" },
margins: { top: 12, right: 12, bottom: 12, left: 12 },
orientation: "portrait" as const,
dataSources: [
{
id: "header",
name: "Encabezado",
type: "object" as const,
endpoint: "/v1/config/empresa",
fields: [
{ name: "empresa", label: "Empresa", type: "string" },
],
},
{
id: "pedidos",
name: "Pedidos",
type: "array" as const,
endpoint: "/v1/ventas/pedidos",
fields: [
{ name: "id", label: "ID", type: "number" },
{ name: "fecha", label: "Fecha", type: "date" },
{ name: "cliente", label: "Cliente", type: "string" },
{ name: "total", label: "Total", type: "currency" },
],
},
],
bands: [
{
id: "rh", type: "reportHeader", height: 14,
elements: [
{
id: "rh-title", type: "text",
content: "LISTADO DE PEDIDOS",
x: 0, y: 0, width: 186, height: 9,
style: { fontSize: 14, fontWeight: "bold", textAlign: "center" },
},
],
},
{
id: "ch", type: "columnHeader", height: 7,
elements: [
{ id: "ch-id", type: "text", content: "ID", x: 0, y: 0, width: 20, height: 5, style: { fontSize: 8, fontWeight: "bold" } },
{ id: "ch-fecha", type: "text", content: "Fecha", x: 22, y: 0, width: 30, height: 5, style: { fontSize: 8, fontWeight: "bold" } },
{ id: "ch-cliente", type: "text", content: "Cliente", x: 54, y: 0, width: 90, height: 5, style: { fontSize: 8, fontWeight: "bold" } },
{ id: "ch-total", type: "text", content: "Total", x: 146, y: 0, width: 40, height: 5, style: { fontSize: 8, fontWeight: "bold", textAlign: "right" } },
],
},
{
id: "dt", type: "detail", height: 6, dataSource: "pedidos",
elements: [
{ id: "dt-id", type: "field", dataSource: "pedidos", field: "id", x: 0, y: 0, width: 20, height: 5, style: { fontSize: 8 } },
{ id: "dt-fecha", type: "field", dataSource: "pedidos", field: "fecha", format: "dd/MM/yyyy", x: 22, y: 0, width: 30, height: 5, style: { fontSize: 8 } },
{ id: "dt-cliente", type: "field", dataSource: "pedidos", field: "cliente", x: 54, y: 0, width: 90, height: 5, style: { fontSize: 8 } },
{ id: "dt-total", type: "field", dataSource: "pedidos", field: "total", format: "$#,##0.00", x: 146, y: 0, width: 40, height: 5, style: { fontSize: 8, textAlign: "right" } },
],
},
{
id: "pf", type: "pageFooter", height: 8,
elements: [
{ id: "pf-page", type: "pageNumber", format: "Pagina {page} de {pages}", x: 146, y: 0, width: 40, height: 5, style: { fontSize: 7, textAlign: "right" } },
],
},
],
};

El script seed:reports sincroniza los layouts de codigo hacia la base de datos (zentto-cache / Redis).

Ventana de terminal
# Desarrollo local
cd web/api
npm run seed:reports
# Produccion (via Docker)
docker exec zentto-api npm run seed:reports

Los layouts en codigo (shared-reports/src/layouts/) son el respaldo canonico. En runtime, el sistema lee layouts desde la BD. Si un usuario modifica un layout via el Designer, los cambios se persisten en BD sin afectar el codigo fuente.


Ejemplo completo basado en la pagina de Asientos Contables:

"use client";
import { useMemo, useState } from "react";
import { Button, Dialog, DialogContent } from "@mui/material";
import { ReportViewer, ASIENTOS_LIST_LAYOUT } from "@zentto/shared-reports";
import type { ReportLayout, DataSet } from "@zentto/report-core";
export default function AsientosListPage() {
const [reportOpen, setReportOpen] = useState(false);
// Supongamos que `rows` viene de un hook useAsientos()
const rows = useAsientos();
// 1. Mapear datos al DataSet esperado por el layout
const reportData = useMemo((): DataSet => ({
header: {
empresa: "Mi Empresa S.A.",
fechaDesde: "01/03/2026",
fechaHasta: "31/03/2026",
totalDebe: rows.reduce((s, r) => s + r.totalDebe, 0),
totalHaber: rows.reduce((s, r) => s + r.totalHaber, 0),
totalRegistros: rows.length,
},
asientos: rows.map((r, i) => ({
num: i + 1,
id: r.id,
fecha: r.fecha,
tipoAsiento: r.tipo,
concepto: r.concepto,
referencia: r.referencia,
totalDebe: r.totalDebe,
totalHaber: r.totalHaber,
estado: r.estado,
})),
}), [rows]);
return (
<>
{/* 2. Boton para abrir el reporte */}
<Button
variant="outlined"
startIcon={<PrintIcon />}
onClick={() => setReportOpen(true)}
>
Reporte
</Button>
{/* 3. Dialog con el ReportViewer */}
<Dialog
open={reportOpen}
onClose={() => setReportOpen(false)}
maxWidth={false}
fullWidth
>
<DialogContent sx={{ height: "85vh", p: 0 }}>
<ReportViewer
layout={ASIENTOS_LIST_LAYOUT as unknown as ReportLayout}
data={reportData}
showToolbar
viewMode="all"
/>
</DialogContent>
</Dialog>
</>
);
}

El componente PrintButton encapsula todo el flujo anterior:

import { PrintButton } from "@zentto/shared-reports";
<PrintButton
layout={ASIENTOS_LIST_LAYOUT}
data={reportData}
label="Imprimir"
/>

El ReportDesigner es un editor visual WYSIWYG tipo Figma para crear y modificar layouts.

PropTipoDefaultDescripcion
layoutReportLayout | nullLayout inicial a editar
sampleDataDataSet | nullnullDatos de ejemplo para preview
dataSourcesDataSourceDef[][]Fuentes de datos disponibles en el toolbox
showPreviewbooleantrueMostrar panel de preview en tiempo real
gridSnapnumber1Ajuste a cuadricula en mm
autoSaveMsnumber3000Intervalo de auto-guardado (ms)
onLayoutChange(layout: ReportLayout) => voidCallback al modificar el layout
styleCSSPropertiesEstilos CSS del contenedor
classNamestringClase CSS
"use client";
import { useState } from "react";
import { ReportDesigner } from "@zentto/shared-reports";
import type { ReportLayout } from "@zentto/report-core";
export default function DesignerPage() {
const [layout, setLayout] = useState<ReportLayout | null>(null);
const handleSave = async (updated: ReportLayout) => {
await fetch(`/api/v1/reportes/saved/${layout?.name}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ layout: updated }),
});
};
return (
<ReportDesigner
layout={layout}
sampleData={null}
showPreview
gridSnap={1}
onLayoutChange={handleSave}
style={{ height: "100vh" }}
/>
);
}

Si necesitas usar el designer como web component puro:

<zentto-report-designer id="designer"></zentto-report-designer>
<script type="module">
import "@zentto/report-designer";
const el = document.getElementById("designer");
el.layout = { /* ReportLayout JSON */ };
el.sampleData = { /* DataSet */ };
el.showPreview = true;
el.addEventListener("layout-change", (e) => {
console.log("Layout modificado:", e.detail.layout);
});
</script>

PropTipoDefaultDescripcion
layoutReportLayout | nullLayout del reporte a renderizar
dataDataSet | nullDatos para llenar el reporte
zoomnumber100Nivel de zoom (%)
showToolbarbooleantrueMostrar barra de herramientas
theme"light" | "dark""light"Tema visual
viewMode"single" | "all""single"single = una pagina a la vez, all = scroll continuo
showThumbnailsbooleanfalseMostrar panel de miniaturas
toolbarItemsstring[]ver abajoItems visibles en la toolbar
styleCSSPropertiesEstilos CSS del contenedor
classNamestringClase CSS
["navigation", "zoom", "view-mode", "fit-width", "print", "download-html", "theme"]

Cuando un DataSourceDef incluye endpoint, el viewer puede obtener datos automaticamente usando un DataFetchProvider:

interface DataFetchProvider {
fetch(
endpoint: string,
params?: Record<string, string>
): Promise<Record<string, unknown> | Record<string, unknown>[]>;
}
{
id: "factura",
name: "Factura",
type: "object",
endpoint: "/v1/documentos-venta/:id",
endpointParams: { id: ":documentId" },
fields: [
{ name: "numero", label: "Numero", type: "string" },
{ name: "fecha", label: "Fecha", type: "date" },
{ name: "total", label: "Total", type: "currency" },
],
}

El provider reemplaza :id en la URL con el valor del parametro documentId proporcionado al momento de renderizar.

  1. El layout declara endpoint en cada dataSource.
  2. Al renderizar, el motor invoca DataFetchProvider.fetch(endpoint, params).
  3. La API ejecuta el SP optimizado correspondiente.
  4. Los datos se inyectan en el DataSet antes de pasar al Band Engine.

Esto garantiza:

  • Seguridad: sin SQL injection, respeta permisos del usuario autenticado.
  • Performance: SPs optimizados con indices.
  • Compatibilidad: funciona con SQL Server y PostgreSQL.

El motor de expresiones es seguro (sin eval). La sintaxis usa llaves para campos y funciones tipo Crystal Reports.

={campo} Referencia a campo
={precio} * {cantidad} Expresion aritmetica
=IF({qty} > 10, "BULK", "RETAIL") Condicional
=FORMAT({total}, "$#,##0.00") Formato
=SUM({monto}) Agregado sobre todos los registros

LEFT, RIGHT, MID, LEN, TRIM, LTRIM, RTRIM, UPPER, LOWER, PROPERCASE, REPLACE, INSTR, SPLIT, JOIN, CHR, ASC, SPACE, REPLICATESTRING, STRREVERSE, CONTAINS, STARTSWITH, ENDSWITH, PADLEFT, PADRIGHT, TOTEXT, TOWORDS, CONCAT

ABS, ROUND, TRUNCATE, FLOOR, CEILING, CEIL, REMAINDER, MOD, SGN, SQRT, EXP, LOG, LOG10, PI, POWER, RANDOM, MIN2, MAX2

NOW, TODAY, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DATEADD, DATEDIFF, DAYOFWEEK, MONTHNAME, DAYNAME, ISDATE, FORMATDATE, DATESERIAL

TONUMBER, TOSTRING, TODATE, TOBOOLEAN, ISNUMBER, ISNULL, COALESCE

SUM, AVG, COUNT, DISTINCTCOUNT, MIN, MAX, MEDIAN, STDDEV, VARIANCE, PERCENTOFSUM, NTHLARGEST, NTHSMALLEST

SUM_GROUP, AVG_GROUP, COUNT_GROUP, SUM_ALL, AVG_ALL, COUNT_ALL

PREVIOUS, NEXT, RECORDNUMBER, GROUPNUMBER, PAGENUMBER, TOTALPAGECOUNT, ONFIRSTRECORD, ONLASTRECORD, INREPEATEDGROUPHEADER

IF, IIF, SWITCH, CHOOSE

SETVAR, GETVAR

FORMAT, FORMATCURRENCY, FORMATPERCENT


Todos los endpoints estan bajo /v1/reportes y requieren autenticacion JWT.

MetodoRutaDescripcion
GET/v1/reportes/savedListar reportes guardados del usuario
GET/v1/reportes/saved/:idObtener layout + sampleData de un reporte
PUT/v1/reportes/saved/:idGuardar/actualizar un reporte
DELETE/v1/reportes/saved/:idEliminar un reporte guardado
MetodoRutaDescripcion
GET/v1/reportes/publicListar reportes publicos de la empresa
PUT/v1/reportes/public/:idGuardar un reporte publico
MetodoRutaDescripcion
POST/v1/reportes/pdfGenerar PDF desde layout + data
POST/v1/reportes/pdf?format=base64Generar PDF en formato base64 (para APIs externas)
MetodoRutaDescripcion
GET/v1/reportes/enginesEstado de todos los motores (Crystal, jsreport, SSRS)
GET/v1/reportes/crystal/catalogoCatalogo de reportes Crystal
POST/v1/reportes/crystal/renderRenderizar reporte Crystal
POST/v1/reportes/jsreport/renderRenderizar reporte jsreport

Los reportes guardados se persisten en zentto-cache (Redis) con claves compuestas por companyId + userId + reportId. Los reportes publicos usan solo companyId + reportId y son visibles para todos los usuarios de la empresa.


El motor soporta graficos SVG embebidos en cualquier banda:

chartTypeDescripcion
barBarras verticales
lineLineas
piePastel
areaArea
donutDona
scatterDispersion
stackedBarras apiladas
comboCombinado (barras + linea)
{
id: "chart-ventas",
type: "chart",
chartType: "bar",
dataSource: "ventas_mensuales",
labelField: "mes",
valueFields: ["total", "meta"],
title: "Ventas vs Meta",
colors: ["#3b82f6", "#ef4444"],
x: 10, y: 5, width: 160, height: 80,
}

PaqueteDescripcionRegistro
@zentto/report-coreMotor puro: tipos, engine 3-pass, expresiones, charts SVG, templatesnpm
@zentto/report-designerWeb component <zentto-report-designer> — editor WYSIWYGnpm
@zentto/report-viewerWeb component <zentto-report-viewer> — visualizacionnpm
@zentto/shared-reportsReact wrappers, hooks, layouts centralizados, PrintButtonMonorepo interno
Ventana de terminal
npm install @zentto/report-core @zentto/report-viewer @zentto/report-designer
// Los web components Lit requieren transpilacion en Next.js
transpilePackages: [
'@zentto/report-core',
'@zentto/report-viewer',
'@zentto/report-designer',
'lit',
]