# nexabase-report

> Professional report designer and viewer — framework-agnostic, client-side PDF/Excel/Word export, banded report model inspired by Stimulsoft & DevExpress.

[![npm](https://img.shields.io/npm/v/nexabase-report.svg)](https://www.npmjs.com/package/nexabase-report)
[![npm downloads](https://img.shields.io/npm/dm/nexabase-report.svg)](https://www.npmjs.com/package/nexabase-report)
[![bundle size](https://img.shields.io/bundlephobia/minzip/nexabase-report)](https://bundlephobia.com/package/nexabase-report)
[![license](https://img.shields.io/npm/l/nexabase-report.svg)](https://github.com/nexabase/nexabase-report/blob/main/LICENSE)

## Features

- **WYSIWYG Designer** — drag & drop bands, elements, charts, crosstabs, barcodes, QR codes
- **Framework-agnostic Viewer** — Custom Element (`<nexa-viewer>`) works with Vue, React, Angular, Blazor, jQuery, or plain HTML
- **Client-side Export** — PDF (selectable text), Excel, Word, CSV — no server required
- **Banded Report Model** — ReportHeader, PageHeader/Footer, DataBand, GroupHeader/Footer, ReportFooter
- **Charts & Crosstabs** — ECharts integration, dynamic pivot tables with aggregations
- **Dashboard Designer** — widgets (chart, gauge, indicator, table, filter, pivot, text, image)
- **Expression Engine** — safe evaluation without `eval` — `FormatNumber`, `IIF`, `ISNULL`, `UPPER`, etc.
- **Master-Detail & Drill-down** — nested data sources, subreports, clickable drill-through
- **Conditional Formatting** — dynamic styles based on data values
- **Parameters** — typed report parameters with validation dialog
- **i18n** — built-in Spanish, English, Portuguese

## Installation

```bash
npm install nexabase-report
```

## Quick Start

### Plain HTML / Any Framework

```html
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="node_modules/nexabase-report/dist/style.css">
</head>
<body>
  <nexa-viewer id="viewer" minimal></nexa-viewer>

  <script src="node_modules/nexabase-report/dist/nexabase-report.umd.js"></script>
  <script>
    const viewer = document.getElementById('viewer');
    viewer.definition = { /* report JSON */ };
    viewer.data = [ /* array of records */ ];
  </script>
</body>
</html>
```

### Vue 3

```vue
<script setup>
import { registerNexaReport } from 'nexabase-report';
import 'nexabase-report/style.css';

registerNexaReport();
</script>

<template>
  <nexa-viewer :definition="reportDef" :data="reportData" minimal />
</template>
```

### React

```tsx
import { useEffect, useRef } from 'react';
import 'nexabase-report';
import 'nexabase-report/style.css';

function ReportViewer({ definition, data }: { definition: any; data: any[] }) {
  const ref = useRef<any>(null);

  useEffect(() => {
    if (ref.current) {
      ref.current.definition = definition;
      ref.current.data = data;
    }
  }, [definition, data]);

  return <nexa-viewer ref={ref} minimal style={{ width: '100%', height: '100%' }} />;
}
```

### Blazor

```razor
<nexa-viewer id="viewer" minimal></nexa-viewer>

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync("initViewer", reportDef, data);
        }
    }
}
```

```javascript
// wwwroot/report.js
function initViewer(def, data) {
    const viewer = document.getElementById('viewer');
    viewer.definition = def;
    viewer.data = data;
}
```

## Viewer API

### Props

| Prop | Type | Description |
|------|------|-------------|
| `definition` | `NexaReportDefinition \| string` | Report definition (object or JSON string) |
| `data` | `any[] \| Record<string, any[]>` | Data — array for single datasource, object for multiple |
| `minimal` | `boolean` | Hide toolbar and sidebar |
| `parameters` | `Record<string, any>` | Pre-fill parameter values (skips dialog) |
| `skipParamsDialog` | `boolean` | Don't show parameter input dialog |
| `currentPage` | `number` | Externally controlled page number |
| `locale` | `'es' \| 'en' \| 'pt'` | UI language |
| `licenseKey` | `string` | License key (optional) |

### Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `exportPdf()` | `Promise<void>` | Export to PDF and download |
| `exportPdfAsBlob()` | `Promise<Blob>` | Export to PDF as Blob (for ZIP, upload, etc.) |
| `exportExcel()` | `Promise<void>` | Export to Excel and download |
| `exportWord()` | `Promise<void>` | Export to Word and download |
| `exportCsv()` | `Promise<void>` | Export to CSV and download |
| `printReport()` | `void` | Print the report |
| `updateData(data)` | `void` | Update data without re-rendering definition |

### Events

| Event | Payload | Description |
|-------|---------|-------------|
| `data-request` | `{ alias: string }` | Request data for a datasource |
| `drill-click` | `{ element, sourceData, targetReportId, parameters }` | Drill-down element clicked |
| `subreport-toggle` | `{ subReportId, collapsed }` | Subreport expanded/collapsed |
| `page-change` | `{ page: number }` | Page changed |

## Batch Export (ZIP)

Export multiple reports to a single ZIP file without showing the viewer:

```javascript
import { registerNexaReport } from 'nexabase-report';
import JSZip from 'jszip';

registerNexaReport();

async function exportBatch(reportDef, invoices) {
    // Create hidden viewer
    const viewer = document.createElement('nexa-viewer');
    viewer.style.display = 'none';
    viewer.minimal = true;
    viewer.definition = reportDef;
    document.body.appendChild(viewer);

    await customElements.whenDefined('nexa-viewer');
    await new Promise(r => setTimeout(r, 300));

    const zip = new JSZip();

    for (const invoice of invoices) {
        viewer.updateData(invoice.items);
        await new Promise(r => setTimeout(r, 500));
        const blob = await viewer.exportPdfAsBlob();
        zip.file(invoice.name + '.pdf', blob);
    }

    const zipBlob = await zip.generateAsync({ type: 'blob' });

    // Download
    const url = URL.createObjectURL(zipBlob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'invoices.zip';
    a.click();
    URL.revokeObjectURL(url);

    // Cleanup
    document.body.removeChild(viewer);
}
```

## Designer

The designer is a Vue 3 component for building reports visually:

```vue
<script setup>
import NexaDesigner from 'nexabase-report/src/lib/designer/NexaDesigner.vue';
import 'nexabase-report/style.css';
</script>

<template>
  <NexaDesigner :report="reportDefinition" @save="handleSave" />
</template>
```

## Report Model

Reports use a banded structure:

```json
{
  "metadata": { "name": "Sales Report", "version": "1.0" },
  "layout": {
    "page": { "format": "A4", "orientation": "portrait", "margins": { "top": 2, "right": 2, "bottom": 2, "left": 2 } },
    "bands": [
      { "id": "header", "type": "ReportHeader", "height": 60, "elements": [...] },
      { "id": "data", "type": "DataBand", "height": 30, "dataSource": "main", "elements": [...] },
      { "id": "footer", "type": "PageFooter", "height": 30, "elements": [...] }
    ]
  },
  "dataSources": [{ "id": "ds1", "alias": "main", "collection": "sales" }]
}
```

### Band Types

| Type | Description |
|------|-------------|
| `ReportHeader` | Renders once, on the first page |
| `PageHeader` | Renders at the top of every page |
| `DataBand` | Iterates over datasource records |
| `GroupHeader` / `GroupFooter` | Renders when group key changes |
| `PageFooter` | Renders at the bottom of every page |
| `ReportFooter` | Renders once, on the last page |

### Element Types

`Text`, `Image`, `Barcode`, `QRCode`, `Rectangle`, `Ellipse`, `Line`, `Arrow`, `Table`, `Chart`, `Crosstab`, `SubReport`, `DrillDown`

## Expression Engine

Safe evaluation without `eval`. Supports:

- **Bindings:** `{{field}}` — resolves from data row
- **Expressions:** `{[expr]}` — evaluated with context
- **System variables:** `{[Page]}`, `{[TotalPages]}`, `{[Today]}`, `{[RowNumber]}`, `{[TotalRows]}`, `{[EvenRow]}`, `{[OddRow]}`
- **Functions:** `FormatNumber`, `FormatDate`, `FormatCurrency`, `IIF`, `ISNULL`, `UPPER`, `LOWER`, `SUBSTRING`, `ABS`, `CEIL`, `FLOOR`, `ROUND`, `LEN`, `TRIM`, `CONCAT`

## Integration with NexaBase

Works out of the box with [NexaBase](https://nexabase.dev) backend:

```javascript
import { nexaService } from 'nexabase-report';

// Connect
nexaService.connect('https://your-nexabase.url', 'your-api-key');

// Fetch data and render
const data = await nexaService.listDocuments('sales');
viewer.data = data;
```

## Examples

See `examples/` directory in the repository for working samples:
- `integration-vue.html` — Vue 3 + UMD build
- `integration-react.html` — React + UMD build
- `integration-plain.html` — Plain HTML + UMD build

## Development

```bash
pnpm install
pnpm run dev          # Start dev server
pnpm run build        # Build library
pnpm run test         # Run tests (199 tests)
pnpm run test:watch   # Watch mode
```

## Publishing

```bash
pnpm run release:minor   # Bump minor version
pnpm publish --access public
```

## License

MIT © NexaBase Team
