nexabase-report 0.2.13 → 0.3.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
@@ -2,24 +2,10 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/nexabase-report)](https://www.npmjs.com/package/nexabase-report)
4
4
  [![license](https://img.shields.io/npm/l/nexabase-report)](LICENSE)
5
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue)](https://www.typescriptlang.org/)
6
6
  [![Vue 3](https://img.shields.io/badge/Vue-3.5-green)](https://vuejs.org/)
7
7
 
8
- > Librería Vue 3 + TypeScript para diseñar y visualizar reportes tipo banded (inspirada en Stimulsoft / DevExpress). Incluye **diseñador WYSIWYG**, **visor framework-agnostic** como Custom Element, y **exportación 100% cliente** a PDF, Excel, Word y CSV.
9
-
10
- ---
11
-
12
- ## Tabla de contenidos
13
-
14
- - [Características](#características)
15
- - [Instalación](#instalación)
16
- - [Uso rápido](#uso-rápido)
17
- - [Diseñador vs Viewer](#diseñador-vs-viewer)
18
- - [API del Viewer](#api-del-viewer)
19
- - [Formato de datos](#formato-de-datos)
20
- - [Variables y expresiones](#variables-y-expresiones)
21
- - [Ejemplos de integración](#ejemplos-de-integración)
22
- - [Documentación adicional](#documentación-adicional)
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.
23
9
 
24
10
  ---
25
11
 
@@ -28,17 +14,27 @@
28
14
  | Característica | Descripción |
29
15
  |----------------|-------------|
30
16
  | **Custom Element** | `<nexa-viewer>` funciona en Vue, React, Angular, Svelte, Blazor o HTML puro |
31
- | **Diseñador WYSIWYG** | `NexaDesigner.vue` — arrastra/suelta bandas, textos, tablas, imágenes, gráficos |
32
- | **Visor standalone** | `<nexa-viewer>` no requiere Vue en el proyecto consumidor |
33
- | **Exportación cliente** | PDF vectorial (jsPDF), Excel (SheetJS), Word (docx), CSV |
34
- | **Gráficos** | ECharts integrado (barras, líneas, pastel, area, scatter) |
35
- | **Tablas dinámicas** | Crosstabs nativos con agregaciones |
36
- | **Códigos QR / Barras** | jsbarcode + qrcode |
37
- | **Formato condicional** | Reglas visuales por campo y operador |
38
- | **Master-Detail** | Subreportes anidados con datos relacionados |
39
- | **Motor de expresiones** | Seguro (sin `eval`): `FormatNumber`, `FormatDate`, `IIF`, `ISNULL`, `sum`, `count`, etc. |
40
- | **Parámetros** | Dilogo de entrada, valores por props, skipParamsDialog |
41
- | **Paginación** | Automática por altura de página, thumbnails, navegación |
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 |
42
38
 
43
39
  ---
44
40
 
@@ -48,7 +44,7 @@
48
44
  npm install nexabase-report
49
45
  ```
50
46
 
51
- Importar estilos **una sola vez** al inicio de la app:
47
+ Importar estilos una sola vez al inicio:
52
48
 
53
49
  ```ts
54
50
  import 'nexabase-report/style.css';
@@ -64,18 +60,18 @@ import 'nexabase-report/style.css';
64
60
  <!DOCTYPE html>
65
61
  <html>
66
62
  <head>
67
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nexabase-report/dist/style.css">
63
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nexabase-report@0.2/dist/style.css">
68
64
  </head>
69
65
  <body>
70
66
  <nexa-viewer id="viewer" minimal></nexa-viewer>
71
67
 
72
- <script src="https://cdn.jsdelivr.net/npm/nexabase-report/dist/nexabase-report.umd.js"></script>
68
+ <script src="https://cdn.jsdelivr.net/npm/nexabase-report@0.2/dist/nexabase-report.umd.js"></script>
73
69
  <script>
74
70
  NexaReport.registerNexaReport();
75
71
 
76
72
  const viewer = document.getElementById('viewer');
77
- viewer.definition = { /* tu definición de reporte */ };
78
- viewer.data = [ /* array de datos */ ];
73
+ viewer.definition = { /* definición del reporte */ };
74
+ viewer.data = [ /* datos */ ];
79
75
  </script>
80
76
  </body>
81
77
  </html>
@@ -90,8 +86,8 @@ import 'nexabase-report/style.css';
90
86
 
91
87
  registerNexaReport();
92
88
 
93
- export function ReportViewer({ definition, data }) {
94
- const ref = useRef<HTMLElement>(null);
89
+ export function ReportViewer({ definition, data }: { definition: any; data: any[] }) {
90
+ const ref = useRef<any>(null);
95
91
 
96
92
  useEffect(() => {
97
93
  if (!ref.current) return;
@@ -106,16 +102,11 @@ export function ReportViewer({ definition, data }) {
106
102
  ### Vue 3
107
103
 
108
104
  ```vue
109
- <script setup>
105
+ <script setup lang="ts">
110
106
  import { registerNexaReport } from 'nexabase-report';
111
107
  import 'nexabase-report/style.css';
112
108
 
113
109
  registerNexaReport();
114
-
115
- const props = defineProps<{
116
- definition: object;
117
- data: any[];
118
- }>();
119
110
  </script>
120
111
 
121
112
  <template>
@@ -136,158 +127,321 @@ import 'nexabase-report/style.css';
136
127
 
137
128
  registerNexaReport();
138
129
 
139
- // En tu componente
140
130
  @Component({
141
131
  template: '<nexa-viewer #viewer [definition]="reportDef" [data]="reportData"></nexa-viewer>',
142
132
  })
143
133
  export class ReportComponent {
144
134
  @ViewChild('viewer') viewerRef!: ElementRef;
145
-
146
- reportDef = null;
135
+ reportDef: any = null;
147
136
  reportData: any[] = [];
148
137
 
149
138
  async ngOnInit() {
150
- const res = await fetch('/assets/report.json');
151
- this.reportDef = await res.json();
139
+ this.reportDef = await fetch('/assets/report.json').then(r => r.json());
152
140
  this.reportData = await fetch('/api/data').then(r => r.json());
153
141
  }
154
142
  }
155
143
  ```
156
144
 
157
- ### Blazor Razor (.NET 8)
145
+ ### Blazor (.NET 8+)
158
146
 
159
147
  ```razor
160
148
  @page "/reporte/{Id:int}"
161
- @inject HttpClient Http
162
149
  @inject IJSRuntime JS
163
150
 
164
151
  <link rel="stylesheet" href="_content/nexabase-report/style.css" />
165
152
  <script src="_content/nexabase-report/nexabase-report.umd.js"></script>
166
153
 
167
- <div style="height:700px">
168
- <nexa-viewer id="viewer" minimal></nexa-viewer>
169
- </div>
154
+ <nexa-viewer id="viewer" minimal></nexa-viewer>
170
155
 
171
156
  @code {
172
157
  [Parameter] public int Id { get; set; }
173
158
 
174
- protected override async Task OnInitializedAsync()
159
+ protected override async Task OnAfterRenderAsync(bool firstRender)
175
160
  {
176
- var definition = await Http.GetFromJsonAsync<dynamic>($"/api/reportes/{Id}");
177
- var data = await Http.GetFromJsonAsync<List<dynamic>>($"/api/reportes/{Id}/datos");
178
-
179
- await JS.InvokeVoidAsync("renderNexaReport", definition, data);
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);
180
165
  }
181
166
  }
182
167
 
183
- @section Scripts {
184
- <script>
185
- function renderNexaReport(definition, data) {
186
- if (!window.nexaRegistered) {
187
- NexaReport.registerNexaReport();
188
- window.nexaRegistered = true;
189
- }
190
- const v = document.getElementById('viewer');
191
- v.definition = definition;
192
- v.data = data;
168
+ <script>
169
+ window.renderNexaReport = function(definition, data) {
170
+ if (!window.__nexaRegistered) {
171
+ NexaReport.registerNexaReport();
172
+ window.__nexaRegistered = true;
193
173
  }
194
- </script>
195
- }
174
+ const v = document.getElementById('viewer');
175
+ v.definition = definition;
176
+ v.data = data;
177
+ };
178
+ </script>
196
179
  ```
197
180
 
198
- O si prefieres usar un componente wrapper en Razor:
181
+ ---
199
182
 
200
- ```razor
201
- @* Shared/ReportViewer.razor *@
202
- @inject IJSRuntime JS
183
+ ## Diseñador (NexaDesigner)
203
184
 
204
- <div style="height: @Height">
205
- <nexa-viewer @ref="viewerRef" id="@Id" minimal></nexa-viewer>
206
- </div>
185
+ Diseñador WYSIWYG como componente Vue 3:
207
186
 
208
- @code {
209
- [Parameter] public string Id { get; set; } = "viewer";
210
- [Parameter] public string Height { get; set; } = "700px";
211
- [Parameter] public object Definition { get; set; }
212
- [Parameter] public object Data { get; set; }
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
213
195
 
214
- private ElementReference viewerRef;
196
+ ### Uso
215
197
 
216
- protected override async Task OnAfterRenderAsync(bool firstRender)
217
- {
218
- if (firstRender)
219
- {
220
- await JS.InvokeVoidAsync("nexaReportInterop.init", viewerRef, Definition, Data);
221
- }
222
- }
198
+ ```vue
199
+ <script setup lang="ts">
200
+ import { NexaDesigner } from 'nexabase-report';
201
+ import 'nexabase-report/style.css';
202
+ import { ref } from 'vue';
203
+
204
+ const designerRef = ref(null);
205
+ const locale = ref('es');
223
206
 
224
- public async Task ExportPdf() => await JS.InvokeVoidAsync("nexaReportInterop.exportPdf", viewerRef);
225
- public async Task ExportExcel() => await JS.InvokeVoidAsync("nexaReportInterop.exportExcel", viewerRef);
207
+ function onSave(reportDef: any) {
208
+ console.log('Reporte guardado:', reportDef);
226
209
  }
210
+ </script>
211
+
212
+ <template>
213
+ <NexaDesigner ref="designerRef" :locale="locale" @save="onSave" style="height: 100vh" />
214
+ </template>
227
215
  ```
228
216
 
229
- ---
217
+ ### Props
230
218
 
231
- ## Diseñador vs Viewer
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) |
232
223
 
233
- | Aspecto | NexaDesigner | NexaViewer |
234
- |---------|-------------|------------|
235
- | **Tag** | `<NexaReportDesigner>` | `<nexa-viewer>` |
236
- | **Framework** | Requiere Vue 3 | Framework-agnostic |
237
- | **Uso** | Crear/editar reportes | Solo visualización |
238
- | **Exportación** | No | Sí (PDF, Excel, Word, CSV) |
239
- | **Props data** | No | Sí |
224
+ ### Eventos
240
225
 
241
- **Flujo típico:**
242
- 1. Diseñador crea → guarda JSON (definición del reporte)
243
- 2. Viewer consume esa definición + datos del backend
244
- 3. Usuario exporta / imprime desde el viewer
226
+ | Evento | Payload | Descripción |
227
+ |--------|---------|-------------|
228
+ | `save` | `NexaReportDefinition` | Usuario presionó guardar |
245
229
 
246
230
  ---
247
231
 
248
- ## API del Viewer
232
+ ## Visor (`<nexa-viewer>`)
233
+
234
+ El visor se registra como Custom Element y funciona sin Vue en el proyecto consumidor.
235
+
236
+ ### Formas de uso
237
+
238
+ ```html
239
+ <nexa-viewer
240
+ minimal
241
+ showToolbar
242
+ showThumbs
243
+ locale="es"
244
+ currentPage="1"
245
+ skipParamsDialog
246
+ ></nexa-viewer>
247
+ ```
248
+
249
+ Como Vue SFC directamente:
250
+
251
+ ```vue
252
+ <script setup lang="ts">
253
+ import { NexaViewerElement, registerNexaReport } from 'nexabase-report';
254
+ import 'nexabase-report/style.css';
255
+
256
+ registerNexaReport();
257
+ </script>
258
+
259
+ <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
+ />
268
+ </template>
269
+ ```
249
270
 
250
271
  ### Props
251
272
 
252
273
  | Prop | Tipo | Default | Descripción |
253
274
  |------|------|---------|-------------|
254
- | `definition` | `string \| object` | — | Definición del reporte (JSON) |
255
- | `data` | `string \| any[] \| object` | — | Datos: `[]` para single DS, `{alias: []}` para multi |
256
- | `parameters` | `object` | `{}` | Valores para parámetros del reporte |
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 |
257
278
  | `minimal` | `boolean` | `false` | Oculta toolbar y thumbnails |
258
- | `showToolbar` | `boolean` | `true` | Muestra/oculta barra superior |
259
- | `showThumbs` | `boolean` | `true` | Muestra panel de miniaturas |
260
- | `skipParamsDialog` | `boolean` | `false` | Salta el diálogo de parámetros |
261
- | `currentPage` | `number` | `1` | Página inicial |
262
- | `apiBaseUrl` | `string` | | URL base de NexaBase |
263
- | `apiKey` | `string` | — | API key para NexaBase |
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 |
264
286
 
265
- ### Métodos (DOM)
287
+ ### Métodos (acceso vía ref o DOM)
266
288
 
267
289
  ```ts
268
290
  const v = document.querySelector('nexa-viewer');
269
291
 
270
- await v.exportPdf(); // PDF vectorial
271
- await v.exportPdfWithBookmarks(); // PDF con marcadores
292
+ // Exportación
293
+ await v.exportPdf(); // pdf (html2pdf.js — fallback jsPDF vectorial)
294
+ await v.exportPdfWithBookmarks(); // pdf con marcadores
272
295
  await v.exportExcel(); // .xlsx (SheetJS)
273
296
  await v.exportWord(); // .docx
274
- await v.exportCsv(); // .csv con BOM UTF-8
275
- v.goToPage(3); // Navegar a página
276
- v.updateData(nuevosDatos); // Actualizar datos
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
277
319
  ```
278
320
 
279
321
  ### Eventos
280
322
 
281
323
  ```ts
282
- v.addEventListener('page-change', e => console.log(e.detail.page));
283
- v.addEventListener('drill-click', e => console.log(e.detail));
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));
284
328
  ```
285
329
 
286
330
  ---
287
331
 
332
+ ## Arquitectura del visor
333
+
334
+ ### Template
335
+
336
+ ```
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
+
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
374
+
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
385
+
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).
387
+
388
+ ### Renderers
389
+
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 |
403
+
404
+ Total: **11 renderers** (8 en `viewer/renderers/` + 3 en `viewer/`).
405
+
406
+ ### Zoom (`useZoom`)
407
+
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`
414
+
415
+ ### Búsqueda (`useSearch`)
416
+
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
422
+
423
+ ### Watermarks
424
+
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
+
288
442
  ## Formato de datos
289
443
 
290
- ### Single DataSource (array simple)
444
+ ### Array simple
291
445
 
292
446
  ```json
293
447
  [
@@ -296,9 +450,9 @@ v.addEventListener('drill-click', e => console.log(e.detail));
296
450
  ]
297
451
  ```
298
452
 
299
- El reporte usa `dataSource: ""` (vacío) para tomar este array como `"main"`.
453
+ El DataSource se asocia automáticamente al alias `"main"`.
300
454
 
301
- ### Multiple DataSources (objeto con alias)
455
+ ### Múltiples DataSources
302
456
 
303
457
  ```json
304
458
  {
@@ -311,16 +465,15 @@ El reporte usa `dataSource: ""` (vacío) para tomar este array como `"main"`.
311
465
  }
312
466
  ```
313
467
 
314
- El reporte usa `dataSource: "clientes"` para referirse a ese alias.
468
+ Cada banda referencia su DataSource por alias: `dataSource: "clientes"`.
315
469
 
316
- ### Con parámetros
470
+ ### Parámetros
317
471
 
318
472
  ```ts
319
- // Pasando parameters al viewer
320
473
  viewer.parameters = {
321
474
  fechaInicio: '2024-01-01',
322
475
  fechaFin: '2024-12-31',
323
- categoria: ' electronics'
476
+ categoria: 'Electronics'
324
477
  };
325
478
  ```
326
479
 
@@ -334,98 +487,96 @@ viewer.parameters = {
334
487
  |----------|-------------|---------|
335
488
  | `{{Page}}` | Página actual | `1` |
336
489
  | `{{TotalPages}}` | Total de páginas | `5` |
337
- | `{{Today}}` | Fecha actual (YYYY-MM-DD) | `2024-01-15` |
338
- | `{{Now}}` | Fecha y hora actual | `2024-01-15 14:30:00` |
339
- | `{{Year}}` | Año actual | `2024` |
340
- | `{{Month}}` | Mes actual | `1` |
341
- | `{{Day}}` | Día actual | `15` |
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` |
342
495
  | `{{ReportName}}` | Nombre del reporte | `Ventas Mensuales` |
343
496
  | `{{RowNumber}}` | Número de fila (1-based) | `1` |
344
497
  | `{{TotalRows}}` | Total de filas | `50` |
345
- | `{{EvenRow}}` | `true` si fila es par | `true` |
346
- | `{{OddRow}}` | `true` si fila es impar | `false` |
347
- | `{{FirstRow}}` / `{{LastRow}}` | Primera/última fila | `true` |
348
- | `{{GroupKey}}` | Clave de agrupación actual | `electronics` |
349
- | `{{GroupCount}}` | Conteo del grupo actual | `12` |
350
-
351
- ### Expresiones con `{[...]}`
352
-
353
- ```
354
- {[FormatNumber(precio, 'es-ES')]} 1.234,56
355
- {[FormatDate(fecha, 'dd/MM/yyyy')]} 15/01/2024
356
- {[FormatCurrency(total, 'USD')]} $1,234.56
357
- {[IIF(total > 100000, 'ALTO', 'BAJO')]} ALTO
358
- {[ISNULL(cliente, 'Sin nombre')]} Sin nombre
359
- {[UPPER(nombre)]} JUAN PÉREZ
360
- {[SUBSTRING(texto, 0, 3)]} "Hel" (primeros 3)
361
- {[ABS(-5)]} 5
362
- {[CEIL(5.3)]} 6
363
- {[FLOOR(5.9)]} 5
364
- ```
365
-
366
- ### Agregaciones en tablas
367
-
368
- En `footerText` de columnas:
369
- ```
370
- Total: {[sum(cantidad)]}
371
- Cantidad: {[count(id)]}
372
- Promedio: {[avg(precio)]}
373
- Máximo: {[max(total)]}
374
- Mínimo: {[min(total)]}
375
- Primero: {[first(nombre)]}
376
- Último: {[last(apellido)]}
377
- ```
378
-
379
- ### Bindings mixtos (content + binding)
380
-
381
- ```json
382
- {
383
- "type": "Text",
384
- "content": "Cliente: {{nombre}} ({{ciudad}})",
385
- "binding": "nombre"
386
- }
387
- ```
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 |
388
527
 
389
528
  ---
390
529
 
391
- ## Ejemplos de integración
530
+ ## Ejemplos
392
531
 
393
- Los ejemplos JSON en `examples/` son reportes completos que puedes cargar:
532
+ Los reportes de ejemplo están en `examples/` y son compatibles con diseñador y visor.
394
533
 
395
534
  | Archivo | Descripción |
396
535
  |---------|-------------|
397
- | `report-factura.json` | Factura con tabla de items, cliente, totales |
398
- | `factura_de_recolección_de_residuos.json` | Factura ambiental con 3 DataSources relacionados |
399
- | `report-ventas-logo-tabla.json` | Reporte con logo, tabla agrupada, resumen |
400
- | `report-agrupado-por-cliente.json` | GroupHeader + Table con agrupación |
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 |
401
542
  | `report-productos.json` | Lista simple de productos |
402
- | `report-crosstab-categoria-mes.json` | Tabla dinámica (crosstab) |
403
- | `report-grafico-ventas-por-mes.json` | Gráfico de barras con datos mensuales |
404
- | `report-master-detail-ordenes.json` | Master-detail con subreportes |
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 |
405
546
  | `report-codigos-qr-barcode.json` | Códigos QR y barras |
406
547
 
407
- ### Cargar un ejemplo
548
+ ---
408
549
 
409
- ```ts
410
- const res = await fetch('/examples/report-factura.json');
411
- const definition = await res.json();
412
- const data = [ /* tus datos */ ];
550
+ ## Publicación
413
551
 
414
- viewer.definition = definition;
415
- viewer.data = data;
552
+ ```bash
553
+ npm run build
554
+ npm version patch|minor|major
555
+ npm publish --access public
556
+ ```
557
+
558
+ CDN automático:
559
+
560
+ ```html
561
+ <script src="https://cdn.jsdelivr.net/npm/nexabase-report@0.2/dist/nexabase-report.umd.js"></script>
562
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nexabase-report@0.2/dist/style.css">
416
563
  ```
417
564
 
565
+ Ver [`docs/PUBLISHING.md`](docs/PUBLISHING.md) para CI/CD y registro privado.
566
+
418
567
  ---
419
568
 
420
569
  ## Documentación adicional
421
570
 
422
- - [API del Viewer](docs/VIEWER_API.md) — Métodos, eventos, props detallados
423
- - [Integración con sistemas externos](docs/EXTERNAL_INTEGRATION.md) — ASP.NET, Django, Rails, etc.
424
- - [Guía de publicación npm](docs/PUBLISHING.md) — CI/CD, versiones, npmjs
425
- - [Plan de QA](docs/QA_PLAN.md) — Checklist de pruebas manuales
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
426
577
 
427
578
  ---
428
579
 
429
580
  ## Licencia
430
581
 
431
- MIT © NexaBase Team
582
+ MIT © NexaBase Team