nexabase-report 0.4.21 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,108 +1,59 @@
1
1
  # nexabase-report
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/nexabase-report)](https://www.npmjs.com/package/nexabase-report)
4
- [![license](https://img.shields.io/npm/l/nexabase-report)](LICENSE)
5
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue)](https://www.typescriptlang.org/)
6
- [![Vue 3](https://img.shields.io/badge/Vue-3.5-green)](https://vuejs.org/)
7
-
8
- > Librería Vue 3 + TypeScript para diseñar y visualizar reportes tipo banded (inspirada en Stimulsoft / DevExpress). Incluye **diseñador WYSIWYG** multilingüe (ES/EN/PT), **visor como Custom Element** framework-agnostic, y **exportación 100% cliente** a PDF, Excel, Word y CSV.
9
-
10
- ---
11
-
12
- ## Características
13
-
14
- | Característica | Descripción |
15
- |----------------|-------------|
16
- | **Custom Element** | `<nexa-viewer>` funciona en Vue, React, Angular, Svelte, Blazor o HTML puro |
17
- | **Diseñador WYSIWYG** | `NexaDesigner.vue` — ribbon, lienzo con snap-to-grid, sidebars, i18n |
18
- | **Visor standalone** | Sin dependencia de Vue en el consumidor |
19
- | **Exportación 100% cliente** | PDF (html2pdf.js + fallback jsPDF vectorial), Excel (SheetJS), Word (docx), CSV (UTF-8 BOM) |
20
- | **Motor de paginación** | Algoritmo en dos fases: streaming + particionado por página, con splitting de tablas a través de páginas |
21
- | **Renderers** | Texto, imágenes, barras, QR, shapes, drill-down, charts (ECharts), crosstabs, subreportes, widgets |
22
- | **Watermarks** | Modo tile (texto repetido en grilla) o single posicionado (center, top-left, bottom-right, etc.) |
23
- | **TOC** | Tabla de contenidos auto-generada en página 1 con enlaces a secciones |
24
- | **Dashboard** | Modo de visualización con grilla de widgets |
25
- | **Gráficos** | ECharts (barras, líneas, pastel, área, scatter, radar, etc.) |
26
- | **Tablas dinámicas** | Crosstabs con agregaciones (sum, count, avg, min, max) |
27
- | **Códigos QR / Barras** | jsbarcode + qrcode con bindings a datos |
28
- | **Formato condicional** | Reglas por operador (eq, gt, contains, between, regex, etc.) |
29
- | **Master-Detail** | DataBand con DetailBand hijo, filtro por campo clave |
30
- | **Drill-Down** | Navegación a reporte destino con paso de parámetros |
31
- | **Subreportes** | Anidamiento vía definición embebida o remota (API REST) |
32
- | **Motor de expresiones** | Seguro (sin `eval`): `FormatNumber`, `FormatDate`, `IIF`, `ISNULL`, agregaciones, `SUBSTRING`, etc. |
33
- | **Parámetros** | Diálogo de entrada con tipos (text, number, date, boolean, select), validación, skip opcional |
34
- | **Shapes** | Rectángulos, elipses, líneas y flechas |
35
- | **Búsqueda** | Highlight inline con navegación entre resultados |
36
- | **Zoom** | Zoom +/- 10%, fitToWidth, fitToPage, modo continuo/página simple |
37
- | **i18n** | Visor y diseñador en español, inglés, portugués |
38
-
39
- ---
40
-
41
- ## Instalación
3
+ > Professional report designer and viewer — framework-agnostic, client-side PDF/Excel/Word export, banded report model inspired by Stimulsoft & DevExpress.
42
4
 
43
- ```bash
44
- pnpm install nexabase-report
45
- ```
5
+ [![npm](https://img.shields.io/npm/v/nexabase-report.svg)](https://www.npmjs.com/package/nexabase-report)
6
+ [![npm downloads](https://img.shields.io/npm/dm/nexabase-report.svg)](https://www.npmjs.com/package/nexabase-report)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/nexabase-report)](https://bundlephobia.com/package/nexabase-report)
8
+ [![license](https://img.shields.io/npm/l/nexabase-report.svg)](https://github.com/nexabase/nexabase-report/blob/main/LICENSE)
46
9
 
47
- Importar estilos una sola vez al inicio:
10
+ ## Features
48
11
 
49
- ```ts
50
- import 'nexabase-report/style.css';
51
- ```
12
+ - **WYSIWYG Designer** — drag & drop bands, elements, charts, crosstabs, barcodes, QR codes
13
+ - **Framework-agnostic Viewer** — Custom Element (`<nexa-viewer>`) works with Vue, React, Angular, Blazor, jQuery, or plain HTML
14
+ - **Client-side Export** — PDF (selectable text), Excel, Word, CSV — no server required
15
+ - **Banded Report Model** — ReportHeader, PageHeader/Footer, DataBand, GroupHeader/Footer, ReportFooter
16
+ - **Charts & Crosstabs** — ECharts integration, dynamic pivot tables with aggregations
17
+ - **Dashboard Designer** — widgets (chart, gauge, indicator, table, filter, pivot, text, image)
18
+ - **Expression Engine** — safe evaluation without `eval` — `FormatNumber`, `IIF`, `ISNULL`, `UPPER`, etc.
19
+ - **Master-Detail & Drill-down** — nested data sources, subreports, clickable drill-through
20
+ - **Conditional Formatting** — dynamic styles based on data values
21
+ - **Parameters** — typed report parameters with validation dialog
22
+ - **i18n** — built-in Spanish, English, Portuguese
52
23
 
53
- ---
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install nexabase-report
28
+ ```
54
29
 
55
- ## Uso rápido
30
+ ## Quick Start
56
31
 
57
- ### HTML puro (sin framework)
32
+ ### Plain HTML / Any Framework
58
33
 
59
34
  ```html
60
35
  <!DOCTYPE html>
61
36
  <html>
62
37
  <head>
63
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nexabase-report/dist/style.css">
38
+ <link rel="stylesheet" href="node_modules/nexabase-report/dist/style.css">
64
39
  </head>
65
40
  <body>
66
41
  <nexa-viewer id="viewer" minimal></nexa-viewer>
67
42
 
68
- <script src="https://cdn.jsdelivr.net/npm/nexabase-report/dist/nexabase-report.umd.js"></script>
43
+ <script src="node_modules/nexabase-report/dist/nexabase-report.umd.js"></script>
69
44
  <script>
70
- NexaReport.registerNexaReport();
71
-
72
45
  const viewer = document.getElementById('viewer');
73
- viewer.definition = { /* definición del reporte */ };
74
- viewer.data = [ /* datos */ ];
46
+ viewer.definition = { /* report JSON */ };
47
+ viewer.data = [ /* array of records */ ];
75
48
  </script>
76
49
  </body>
77
50
  </html>
78
51
  ```
79
52
 
80
- ### React
81
-
82
- ```tsx
83
- import { useEffect, useRef } from 'react';
84
- import { registerNexaReport } from 'nexabase-report';
85
- import 'nexabase-report/style.css';
86
-
87
- registerNexaReport();
88
-
89
- export function ReportViewer({ definition, data }: { definition: any; data: any[] }) {
90
- const ref = useRef<any>(null);
91
-
92
- useEffect(() => {
93
- if (!ref.current) return;
94
- ref.current.definition = definition;
95
- ref.current.data = data;
96
- }, [definition, data]);
97
-
98
- return <nexa-viewer ref={ref} minimal />;
99
- }
100
- ```
101
-
102
53
  ### Vue 3
103
54
 
104
55
  ```vue
105
- <script setup lang="ts">
56
+ <script setup>
106
57
  import { registerNexaReport } from 'nexabase-report';
107
58
  import 'nexabase-report/style.css';
108
59
 
@@ -110,473 +61,234 @@ registerNexaReport();
110
61
  </script>
111
62
 
112
63
  <template>
113
- <nexa-viewer
114
- :definition="definition"
115
- :data="data"
116
- minimal
117
- style="display:block;height:700px"
118
- />
64
+ <nexa-viewer :definition="reportDef" :data="reportData" minimal />
119
65
  </template>
120
66
  ```
121
67
 
122
- ### Angular
68
+ ### React
123
69
 
124
- ```ts
125
- import { registerNexaReport } from 'nexabase-report';
70
+ ```tsx
71
+ import { useEffect, useRef } from 'react';
72
+ import 'nexabase-report';
126
73
  import 'nexabase-report/style.css';
127
74
 
128
- registerNexaReport();
75
+ function ReportViewer({ definition, data }: { definition: any; data: any[] }) {
76
+ const ref = useRef<any>(null);
129
77
 
130
- @Component({
131
- template: '<nexa-viewer #viewer [definition]="reportDef" [data]="reportData"></nexa-viewer>',
132
- })
133
- export class ReportComponent {
134
- @ViewChild('viewer') viewerRef!: ElementRef;
135
- reportDef: any = null;
136
- reportData: any[] = [];
137
-
138
- async ngOnInit() {
139
- this.reportDef = await fetch('/assets/report.json').then(r => r.json());
140
- this.reportData = await fetch('/api/data').then(r => r.json());
141
- }
78
+ useEffect(() => {
79
+ if (ref.current) {
80
+ ref.current.definition = definition;
81
+ ref.current.data = data;
82
+ }
83
+ }, [definition, data]);
84
+
85
+ return <nexa-viewer ref={ref} minimal style={{ width: '100%', height: '100%' }} />;
142
86
  }
143
87
  ```
144
88
 
145
- ### Blazor (.NET 8+)
89
+ ### Blazor
146
90
 
147
91
  ```razor
148
- @page "/reporte/{Id:int}"
149
- @inject IJSRuntime JS
150
-
151
- <link rel="stylesheet" href="_content/nexabase-report/style.css" />
152
- <script src="_content/nexabase-report/nexabase-report.umd.js"></script>
153
-
154
92
  <nexa-viewer id="viewer" minimal></nexa-viewer>
155
93
 
156
94
  @code {
157
- [Parameter] public int Id { get; set; }
158
-
159
95
  protected override async Task OnAfterRenderAsync(bool firstRender)
160
96
  {
161
- if (!firstRender) return;
162
- var def = await Http.GetFromJsonAsync<object>($"/api/reportes/{Id}");
163
- var data = await Http.GetFromJsonAsync<List<object>>($"/api/reportes/{Id}/datos");
164
- await JS.InvokeVoidAsync("renderNexaReport", def, data);
97
+ if (firstRender)
98
+ {
99
+ await JS.InvokeVoidAsync("initViewer", reportDef, data);
100
+ }
165
101
  }
166
102
  }
103
+ ```
167
104
 
168
- <script>
169
- window.renderNexaReport = function(definition, data) {
170
- if (!window.__nexaRegistered) {
171
- NexaReport.registerNexaReport();
172
- window.__nexaRegistered = true;
173
- }
174
- const v = document.getElementById('viewer');
175
- v.definition = definition;
176
- v.data = data;
177
- };
178
- </script>
105
+ ```javascript
106
+ // wwwroot/report.js
107
+ function initViewer(def, data) {
108
+ const viewer = document.getElementById('viewer');
109
+ viewer.definition = def;
110
+ viewer.data = data;
111
+ }
179
112
  ```
180
113
 
181
- ---
114
+ ## Viewer API
182
115
 
183
- ## Diseñador (NexaDesigner)
116
+ ### Props
184
117
 
185
- Diseñador WYSIWYG como componente Vue 3:
118
+ | Prop | Type | Description |
119
+ |------|------|-------------|
120
+ | `definition` | `NexaReportDefinition \| string` | Report definition (object or JSON string) |
121
+ | `data` | `any[] \| Record<string, any[]>` | Data — array for single datasource, object for multiple |
122
+ | `minimal` | `boolean` | Hide toolbar and sidebar |
123
+ | `parameters` | `Record<string, any>` | Pre-fill parameter values (skips dialog) |
124
+ | `skipParamsDialog` | `boolean` | Don't show parameter input dialog |
125
+ | `currentPage` | `number` | Externally controlled page number |
126
+ | `locale` | `'es' \| 'en' \| 'pt'` | UI language |
127
+ | `licenseKey` | `string` | License key (optional) |
186
128
 
187
- - **Ribbon** — formato de texto (fuente, tamaño, color, negrita, cursiva, alineación)
188
- - **Lienzo** — snap-to-grid (5 px), rulers, selección múltiple, arrastrar/redimensionar
189
- - **Panel de propiedades** — secciones: apariencia, datos, bordes, formato condicional
190
- - **Diccionario de datos** — campos del datasource con drag al lienzo
191
- - **Multilingüe** — español, inglés, portugués, cambiable desde la barra
192
- - **Import/Export** — definiciones JSON
193
- - **Deshacer/Rehacer** — Ctrl+Z / Ctrl+Y
194
- - **Clipboard** — copiar/pegar elementos entre bandas
129
+ ### Methods
195
130
 
196
- ### Uso
131
+ | Method | Returns | Description |
132
+ |--------|---------|-------------|
133
+ | `exportPdf()` | `Promise<void>` | Export to PDF and download |
134
+ | `exportPdfAsBlob()` | `Promise<Blob>` | Export to PDF as Blob (for ZIP, upload, etc.) |
135
+ | `exportExcel()` | `Promise<void>` | Export to Excel and download |
136
+ | `exportWord()` | `Promise<void>` | Export to Word and download |
137
+ | `exportCsv()` | `Promise<void>` | Export to CSV and download |
138
+ | `printReport()` | `void` | Print the report |
139
+ | `updateData(data)` | `void` | Update data without re-rendering definition |
197
140
 
198
- ```vue
199
- <script setup lang="ts">
200
- import { NexaDesigner } from 'nexabase-report';
201
- import 'nexabase-report/style.css';
202
- import { ref } from 'vue';
141
+ ### Events
203
142
 
204
- const designerRef = ref(null);
205
- const locale = ref('es');
143
+ | Event | Payload | Description |
144
+ |-------|---------|-------------|
145
+ | `data-request` | `{ alias: string }` | Request data for a datasource |
146
+ | `drill-click` | `{ element, sourceData, targetReportId, parameters }` | Drill-down element clicked |
147
+ | `subreport-toggle` | `{ subReportId, collapsed }` | Subreport expanded/collapsed |
148
+ | `page-change` | `{ page: number }` | Page changed |
206
149
 
207
- function onSave(reportDef: any) {
208
- console.log('Reporte guardado:', reportDef);
209
- }
210
- </script>
150
+ ## Batch Export (ZIP)
211
151
 
212
- <template>
213
- <NexaDesigner ref="designerRef" :locale="locale" @save="onSave" style="height: 100vh" />
214
- </template>
215
- ```
152
+ Export multiple reports to a single ZIP file without showing the viewer:
216
153
 
217
- ### Props
154
+ ```javascript
155
+ import { registerNexaReport } from 'nexabase-report';
156
+ import JSZip from 'jszip';
218
157
 
219
- | Prop | Tipo | Default | Descripción |
220
- |------|------|---------|-------------|
221
- | `locale` | `'es' \| 'en' \| 'pt'` | `'es'` | Idioma del diseñador |
222
- | `initialReport` | `NexaReportDefinition` | — | Reporte a editar (nuevo si no se pasa) |
158
+ registerNexaReport();
223
159
 
224
- ### Eventos
160
+ async function exportBatch(reportDef, invoices) {
161
+ // Create hidden viewer
162
+ const viewer = document.createElement('nexa-viewer');
163
+ viewer.style.display = 'none';
164
+ viewer.minimal = true;
165
+ viewer.definition = reportDef;
166
+ document.body.appendChild(viewer);
225
167
 
226
- | Evento | Payload | Descripción |
227
- |--------|---------|-------------|
228
- | `save` | `NexaReportDefinition` | Usuario presionó guardar |
168
+ await customElements.whenDefined('nexa-viewer');
169
+ await new Promise(r => setTimeout(r, 300));
229
170
 
230
- ---
171
+ const zip = new JSZip();
231
172
 
232
- ## Visor (`<nexa-viewer>`)
173
+ for (const invoice of invoices) {
174
+ viewer.updateData(invoice.items);
175
+ await new Promise(r => setTimeout(r, 500));
176
+ const blob = await viewer.exportPdfAsBlob();
177
+ zip.file(invoice.name + '.pdf', blob);
178
+ }
233
179
 
234
- El visor se registra como Custom Element y funciona sin Vue en el proyecto consumidor.
180
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
235
181
 
236
- ### Formas de uso
182
+ // Download
183
+ const url = URL.createObjectURL(zipBlob);
184
+ const a = document.createElement('a');
185
+ a.href = url;
186
+ a.download = 'invoices.zip';
187
+ a.click();
188
+ URL.revokeObjectURL(url);
237
189
 
238
- ```html
239
- <nexa-viewer
240
- minimal
241
- showToolbar
242
- showThumbs
243
- locale="es"
244
- currentPage="1"
245
- skipParamsDialog
246
- ></nexa-viewer>
190
+ // Cleanup
191
+ document.body.removeChild(viewer);
192
+ }
247
193
  ```
248
194
 
249
- Como Vue SFC directamente:
195
+ ## Designer
196
+
197
+ The designer is a Vue 3 component for building reports visually:
250
198
 
251
199
  ```vue
252
- <script setup lang="ts">
253
- import { NexaViewerElement, registerNexaReport } from 'nexabase-report';
200
+ <script setup>
201
+ import NexaDesigner from 'nexabase-report/src/lib/designer/NexaDesigner.vue';
254
202
  import 'nexabase-report/style.css';
255
-
256
- registerNexaReport();
257
203
  </script>
258
204
 
259
205
  <template>
260
- <nexa-viewer
261
- :definition="reportDef"
262
- :data="reportData"
263
- :parameters="params"
264
- locale="es"
265
- @page-change="onPageChange"
266
- @drill-click="onDrillClick"
267
- />
206
+ <NexaDesigner :report="reportDefinition" @save="handleSave" />
268
207
  </template>
269
208
  ```
270
209
 
271
- ### Props
272
-
273
- | Prop | Tipo | Default | Descripción |
274
- |------|------|---------|-------------|
275
- | `definition` | `string \| NexaReportDefinition` | — | Definición del reporte (objeto o JSON string) |
276
- | `data` | `string \| any[] \| Record<string, any[]>` | — | Datos: array simple (alias "main"), objeto multi-alias, o JSON string |
277
- | `parameters` | `Record<string, any>` | — | Valores iniciales de parámetros |
278
- | `minimal` | `boolean` | `false` | Oculta toolbar y thumbnails |
279
- | `showToolbar` | `boolean` | `true` | Forzar toolbar visible (útil en minimal) |
280
- | `showThumbs` | `boolean` | `true` | Forzar miniaturas visibles (útil en minimal) |
281
- | `skipParamsDialog` | `boolean` | `false` | Salta el diálogo de parámetros al cargar |
282
- | `currentPage` | `number` | `1` | Página inicial (soporta v-model) |
283
- | `locale` | `string` | `'es'` | Idioma: `'es'`, `'en'`, `'pt'` |
284
- | `apiBaseUrl` | `string` | — | URL base para subreportes/drill-down remotos |
285
- | `apiKey` | `string` | — | API Key para requests al backend |
286
-
287
- ### Métodos (acceso vía ref o DOM)
288
-
289
- ```ts
290
- const v = document.querySelector('nexa-viewer');
291
-
292
- // Exportación
293
- await v.exportPdf(); // pdf (html2pdf.js — fallback jsPDF vectorial)
294
- await v.exportPdfWithBookmarks(); // pdf con marcadores
295
- await v.exportExcel(); // .xlsx (SheetJS)
296
- await v.exportWord(); // .docx
297
- await v.exportCsv(); // .csv con UTF-8 BOM
298
-
299
- // Navegación
300
- v.goToPage(5);
301
-
302
- // Datos
303
- v.updateData(nuevosDatos);
304
-
305
- // Parámetros
306
- v.applyParams();
307
- v.applyParamsWithValidation();
308
- v.validateParams(); // retorna errores
309
-
310
- // Utilidad
311
- v.getDictionaryFields(); // retorna campos disponibles
312
-
313
- // Propiedades de lectura
314
- v.pageNumber; // número actual
315
- v.totalPages; // total de páginas
316
- v.paramValues; // valores actuales de parámetros
317
- v.showParamsDialog; // estado del diálogo
318
- v.paramValidationErrors; // errores de validación
319
- ```
320
-
321
- ### Eventos
322
-
323
- ```ts
324
- v.addEventListener('page-change', e => console.log(e.detail.page));
325
- v.addEventListener('drill-click', e => console.log(e.detail));
326
- v.addEventListener('subreport-toggle', e => console.log(e.detail));
327
- v.addEventListener('data-request', e => console.log(e.detail.alias));
328
- ```
329
-
330
- ---
331
-
332
- ## Arquitectura del visor
210
+ ## Report Model
333
211
 
334
- ### Template
212
+ Reports use a banded structure:
335
213
 
214
+ ```json
215
+ {
216
+ "metadata": { "name": "Sales Report", "version": "1.0" },
217
+ "layout": {
218
+ "page": { "format": "A4", "orientation": "portrait", "margins": { "top": 2, "right": 2, "bottom": 2, "left": 2 } },
219
+ "bands": [
220
+ { "id": "header", "type": "ReportHeader", "height": 60, "elements": [...] },
221
+ { "id": "data", "type": "DataBand", "height": 30, "dataSource": "main", "elements": [...] },
222
+ { "id": "footer", "type": "PageFooter", "height": 30, "elements": [...] }
223
+ ]
224
+ },
225
+ "dataSources": [{ "id": "ds1", "alias": "main", "collection": "sales" }]
226
+ }
336
227
  ```
337
- <nexa-viewer>
338
- ├── Toolbar (condicional)
339
- │ ├── Exportación: PDF, Excel, Word, CSV, imprimir
340
- │ ├── Zoom: −, %, +, fitToWidth, fitToPage
341
- │ └── Paginación: ◀, input/total, ▶, búsqueda, info
342
-
343
- ├── Sidebar (condicional, 220px)
344
- │ ├── Pestaña Pages — miniaturas vía html2canvas
345
- │ ├── Pestaña TOC — tabla de contenidos con navegación
346
- │ └── Pestaña Params — formulario de parámetros (text, number, date, boolean, select)
347
-
348
- ├── Viewport (scrollable)
349
- │ └── Pages root (transform: scale(Z%) )
350
- │ ├── Dashboard: grilla de DashboardWidgetRenderer
351
- │ └── Reporte: v-for pages
352
- │ └── report-page (sombra, fondo blanco)
353
- │ ├── Watermark (tile o single posicionado)
354
- │ ├── TOC page (si página 1 y generateTOC)
355
- │ └── Bands
356
- │ ├── DataBand tabular → Table, Chart, Crosstab, SubReport
357
- │ └── DataBand iterativa → Text, Image, Barcode, QR, Shape, DrillDown
358
-
359
- ├── Modal DrillDown (reporte anidado vía API)
360
- └── Modal About
361
- ```
362
-
363
- ### PaginationEngine
364
-
365
- El motor de paginación es una clase (`src/lib/viewer/services/PaginationEngine.ts`) que opera en **dos fases**:
366
228
 
367
- **Fase 1 — generateStream()**: Generator que produce un stream de `PageBandDef`:
368
- 1. Emite `ReportHeader` primero
369
- 2. Bandas estáticas (ni DataBand ni DetailBand)
370
- 3. Para cada `DataBand`:
371
- - **Tabular** (Table, Chart, Crosstab, SubReport): emite la banda completa con todas las filas en `rows`
372
- - **Iterativa** (una instancia por fila): emite una banda por fila, intercalando GroupHeader/GroupFooter según agrupación y procesando DetailBand hijos (master-detail)
373
- 4. Emite `ReportFooter` al final
229
+ ### Band Types
374
230
 
375
- **Fase 2 generatePages()**: Algoritmo de particionado:
376
- - Calcula altura disponible: `pageHeight - pageHeader - pageFooter`
377
- - Itera el stream, acumulando bandas
378
- - Si una banda **tabular** no cabe:
379
- - **Tabla**: calcula filas que entran (`(h disponible - h header) / h fila`), corta en chunks, coloca cada chunk en página nueva
380
- - **Tabla agrupada**: corta por grupos completos (respeta `tableShowGroupHeader`/`tableShowGroupFooter`)
381
- - Charts/Crosstabs: saltan a página nueva enteros
382
- - Respeta `pageBreakBefore` / `pageBreakAfter`
383
- - Asigna coordenadas Y absolutas para posicionamiento CSS
384
- - Cada ~10 páginas cede el hilo (`setTimeout`) para no bloquear en datasets grandes
231
+ | Type | Description |
232
+ |------|-------------|
233
+ | `ReportHeader` | Renders once, on the first page |
234
+ | `PageHeader` | Renders at the top of every page |
235
+ | `DataBand` | Iterates over datasource records |
236
+ | `GroupHeader` / `GroupFooter` | Renders when group key changes |
237
+ | `PageFooter` | Renders at the bottom of every page |
238
+ | `ReportFooter` | Renders once, on the last page |
385
239
 
386
- Métodos estáticos auxiliares: `applyFilters`, `applySort`, `applyJoins`, `matchesFilterValue` (soporta eq, ne, gt, gte, lt, lte, contains, starts_with, ends_with, is_null, is_not_null, between, regex).
240
+ ### Element Types
387
241
 
388
- ### Renderers
242
+ `Text`, `Image`, `Barcode`, `QRCode`, `Rectangle`, `Ellipse`, `Line`, `Arrow`, `Table`, `Chart`, `Crosstab`, `SubReport`, `DrillDown`
389
243
 
390
- | Renderer | Archivo | Descripción |
391
- |----------|---------|-------------|
392
- | TextRenderer | `viewer/renderers/TextRenderer.vue` | Texto plano con soporte de highlight de búsqueda |
393
- | ImageRenderer | `viewer/renderers/ImageRenderer.vue` | `<img>` con resolución de binding/URL |
394
- | BarcodeRenderer | `viewer/renderers/BarcodeRenderer.vue` | SVG vía jsbarcode (CODE128 default) |
395
- | QRCodeRenderer | `viewer/renderers/QRCodeRenderer.vue` | QR vía librería qrcode |
396
- | ShapeRenderer | `viewer/renderers/ShapeRenderer.vue` | Rectángulo, elipse, línea, flecha (CSS inline) |
397
- | DrillDownRenderer | `viewer/renderers/DrillDownRenderer.vue` | Elemento clickeable que emite `drill-click` |
398
- | FallbackRenderer | `viewer/renderers/FallbackRenderer.vue` | Muestra tipo no soportado |
399
- | SubreportRenderer | `viewer/renderers/SubreportRenderer.vue` | `<nexa-viewer>` anidado (embebido o remoto) |
400
- | ChartRenderer | `viewer/ChartRenderer.vue` | ECharts (barras, líneas, pastel, etc.) |
401
- | CrosstabRenderer | `viewer/CrosstabRenderer.vue` | Tabla dinámica con agrupación fila/columna |
402
- | DashboardWidgetRenderer | `viewer/DashboardWidgetRenderer.vue` | Widgets en modo dashboard |
244
+ ## Expression Engine
403
245
 
404
- Total: **11 renderers** (8 en `viewer/renderers/` + 3 en `viewer/`).
246
+ Safe evaluation without `eval`. Supports:
405
247
 
406
- ### Zoom (`useZoom`)
248
+ - **Bindings:** `{{field}}` — resolves from data row
249
+ - **Expressions:** `{[expr]}` — evaluated with context
250
+ - **System variables:** `{[Page]}`, `{[TotalPages]}`, `{[Today]}`, `{[RowNumber]}`, `{[TotalRows]}`, `{[EvenRow]}`, `{[OddRow]}`
251
+ - **Functions:** `FormatNumber`, `FormatDate`, `FormatCurrency`, `IIF`, `ISNULL`, `UPPER`, `LOWER`, `SUBSTRING`, `ABS`, `CEIL`, `FLOOR`, `ROUND`, `LEN`, `TRIM`, `CONCAT`
407
252
 
408
- - Rango: 25%–250%, default 100%
409
- - `zoomIn()` / `zoomOut()` — ±10 puntos
410
- - `fitToWidth()` — escala al ancho del viewport
411
- - `fitToPage()` — escala al alto y ancho
412
- - Modos: `continuous` (scroll vertical) / `single` (página por página)
413
- - Atajo: Ctrl+`+`, Ctrl+`-`, Ctrl+`0`
253
+ ## Integration with NexaBase
414
254
 
415
- ### Búsqueda (`useSearch`)
255
+ Works out of the box with [NexaBase](https://nexabase.dev) backend:
416
256
 
417
- - `searchQuery` con debounce de 200ms
418
- - Escanea hasta 250 páginas, case-insensitive, máx 500 resultados
419
- - Navegación: `nextMatch()` / `prevMatch()` con scroll automático
420
- - Highlight: `getHighlightParts(text)` retorna segmentos marcados/no marcados
421
- - Input de búsqueda en toolbar con atajo de teclado
257
+ ```javascript
258
+ import { nexaService } from 'nexabase-report';
422
259
 
423
- ### Watermarks
260
+ // Connect
261
+ nexaService.connect('https://your-nexabase.url', 'your-api-key');
424
262
 
425
- - **Tile**: texto repetido en grilla (20 spans CSS grid), opacidad configurable, rotado
426
- - **Single**: posicionado absoluto (center, top-left, top-right, bottom-left, bottom-right, top-center, bottom-center)
427
- - Opacidad, color, tamaño, rotación por configuración
428
-
429
- ### TOC (Tabla de Contenidos)
430
-
431
- - Generación automática si `generateTOC: true` en la definición
432
- - Se Renderiza en página 1
433
- - Cada ítem: texto + dots + número de página
434
- - Navegación: click → `goToPage(n)` con scroll
435
-
436
- ### Dashboard
437
-
438
- Si `documentType === 'Dashboard'`, el reporte se Renderiza como grilla de widgets (`DashboardWidgetRenderer`) en lugar de páginas con bandas.
439
-
440
- ---
441
-
442
- ## Formato de datos
443
-
444
- ### Array simple
445
-
446
- ```json
447
- [
448
- { "id": 1, "nombre": "Juan Pérez", "total": 150000 },
449
- { "id": 2, "nombre": "María López", "total": 230000 }
450
- ]
263
+ // Fetch data and render
264
+ const data = await nexaService.listDocuments('sales');
265
+ viewer.data = data;
451
266
  ```
452
267
 
453
- El DataSource se asocia automáticamente al alias `"main"`.
268
+ ## Examples
454
269
 
455
- ### Múltiples DataSources
270
+ See `examples/` directory in the repository for working samples:
271
+ - `integration-vue.html` — Vue 3 + UMD build
272
+ - `integration-react.html` — React + UMD build
273
+ - `integration-plain.html` — Plain HTML + UMD build
456
274
 
457
- ```json
458
- {
459
- "clientes": [
460
- { "id": 1, "nombre": "Juan", "ciudad": "Bogotá" }
461
- ],
462
- "pedidos": [
463
- { "cliente_id": 1, "producto": "Camisa", "cantidad": 3 }
464
- ]
465
- }
466
- ```
275
+ ## Development
467
276
 
468
- Cada banda referencia su DataSource por alias: `dataSource: "clientes"`.
469
-
470
- ### Parámetros
471
-
472
- ```ts
473
- viewer.parameters = {
474
- fechaInicio: '2024-01-01',
475
- fechaFin: '2024-12-31',
476
- categoria: 'Electronics'
477
- };
277
+ ```bash
278
+ pnpm install
279
+ pnpm run dev # Start dev server
280
+ pnpm run build # Build library
281
+ pnpm run test # Run tests (199 tests)
282
+ pnpm run test:watch # Watch mode
478
283
  ```
479
284
 
480
- ---
481
-
482
- ## Variables y expresiones
483
-
484
- ### Variables de sistema
485
-
486
- | Variable | Descripción | Ejemplo |
487
- |----------|-------------|---------|
488
- | `{{Page}}` | Página actual | `1` |
489
- | `{{TotalPages}}` | Total de páginas | `5` |
490
- | `{{Today}}` | Fecha actual (YYYY-MM-DD) | `2026-05-13` |
491
- | `{{Now}}` | Fecha y hora actual | `2026-05-13 14:30:00` |
492
- | `{{Year}}` | Año actual | `2026` |
493
- | `{{Month}}` | Mes actual | `5` |
494
- | `{{Day}}` | Día actual | `13` |
495
- | `{{ReportName}}` | Nombre del reporte | `Ventas Mensuales` |
496
- | `{{RowNumber}}` | Número de fila (1-based) | `1` |
497
- | `{{TotalRows}}` | Total de filas | `50` |
498
- | `{{EvenRow}}` / `{{OddRow}}` | Fila par / impar | `true` / `false` |
499
- | `{{FirstRow}}` / `{{LastRow}}` | Primera / última fila | `true` |
500
- | `{{GroupKey}}` | Clave del grupo actual | `Electronics` |
501
- | `{{GroupCount}}` | Registros en el grupo actual | `12` |
502
-
503
- ### Expresiones `{[...]}`
504
-
505
- | Expresión | Resultado |
506
- |-----------|-----------|
507
- | `{[FormatNumber(precio, 'es-ES')]}` | `1.234,56` |
508
- | `{[FormatDate(fecha, 'dd/MM/yyyy')]}` | `15/01/2024` |
509
- | `{[FormatCurrency(total, 'USD')]}` | `$1,234.56` |
510
- | `{[IIF(total > 100000, 'ALTO', 'BAJO')]}` | `ALTO` |
511
- | `{[ISNULL(cliente, 'Sin nombre')]}` | `Sin nombre` |
512
- | `{[UPPER(nombre)]}` | `JUAN PÉREZ` |
513
- | `{[LOWER(nombre)]}` | `juan pérez` |
514
- | `{[SUBSTRING(texto, 0, 3)]}` | `Hel` |
515
- | `{[LENGTH(nombre)]}` | `10` |
516
- | `{[TRIM(texto)]}` | sin espacios |
517
- | `{[ABS(-5)]}` | `5` |
518
- | `{[CEIL(5.3)]}` | `6` |
519
- | `{[FLOOR(5.9)]}` | `5` |
520
- | `{[SUM(cantidad)]}` | suma |
521
- | `{[COUNT(id)]}` | conteo |
522
- | `{[AVG(precio)]}` | promedio |
523
- | `{[MIN(precio)]}` | mínimo |
524
- | `{[MAX(precio)]}` | máximo |
525
- | `{[CONCAT(nombre, ' - ', ciudad)]}` | `Juan - Bogotá` |
526
- | `{[REPLACE(texto, 'a', 'o')]}` | reemplazo |
527
-
528
- ---
529
-
530
- ## Ejemplos
531
-
532
- Los reportes de ejemplo están en `examples/` y son compatibles con diseñador y visor.
533
-
534
- | Archivo | Descripción |
535
- |---------|-------------|
536
- | `report-factura.json` | Factura con ítems, cliente y totales |
537
- | `factura_de_recolección_de_residuos.json` | Factura ambiental multi-DataSource |
538
- | `report-factura-residuos.json` | Factura de residuos con GroupHeader/Footer |
539
- | `report-anexo-factura.json` | Anexo con tabla expandida |
540
- | `report-ventas-logo-tabla.json` | Corporativo con logo y tabla agrupada |
541
- | `report-agrupado-por-cliente.json` | GroupHeader + agrupación por cliente |
542
- | `report-productos.json` | Lista simple de productos |
543
- | `report-crosstab-categoria-mes.json` | Crosstab categoría × mes |
544
- | `report-grafico-ventas-por-mes.json` | Gráfico de barras ECharts |
545
- | `report-master-detail-ordenes.json` | Master-Detail con subreportes |
546
- | `report-codigos-qr-barcode.json` | Códigos QR y barras |
547
-
548
- ---
549
-
550
- ## Publicación
285
+ ## Publishing
551
286
 
552
287
  ```bash
553
- pnpm run build
554
- pnpm version patch|minor|major
288
+ pnpm run release:minor # Bump minor version
555
289
  pnpm publish --access public
556
290
  ```
557
291
 
558
- CDN automático:
559
-
560
- ```html
561
- <script src="https://cdn.jsdelivr.net/npm/nexabase-report/dist/nexabase-report.umd.js"></script>
562
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nexabase-report/dist/style.css">
563
- ```
564
-
565
- Ver [`docs/PUBLISHING.md`](docs/PUBLISHING.md) para CI/CD y registro privado.
566
-
567
- ---
568
-
569
- ## Documentación adicional
570
-
571
- - [API completa del Viewer](docs/VIEWER_API.md) — props, métodos, eventos, tipos
572
- - [API REST](docs/API_REST.md) — endpoints serverless
573
- - [Integración externa](docs/EXTERNAL_INTEGRATION.md) — ASP.NET, Django, Laravel, Rails
574
- - [Plan de QA](docs/QA_PLAN.md) — checklist de pruebas manuales
575
- - [Checklist de regresión exportación](docs/EXPORT_REGRESSION.md) — PDF, Excel, Word, CSV
576
- - [Guía de publicación npm](docs/PUBLISHING.md) — build, versionado, CI/CD
577
-
578
- ---
579
-
580
- ## Licencia
292
+ ## License
581
293
 
582
294
  MIT © NexaBase Team