commit f5d231ab8f45b08320a7a1bc9bb4bb6abb76b4c5 Author: Matheus Date: Wed Sep 23 12:29:08 2026 -0300 initial diff --git a/.claude/commands/vitruvio-adicionar-menu.md b/.claude/commands/vitruvio-adicionar-menu.md new file mode 100644 index 0000000..32074a9 --- /dev/null +++ b/.claude/commands/vitruvio-adicionar-menu.md @@ -0,0 +1,121 @@ +--- +name: vitruvio-adicionar-menu +description: > + Use ONLY when the user explicitly asks to add a menu item or menu entry in a Vitruvio repository. + Triggers: "add menu item", "add to menu", "adicionar ao menu", "criar item de menu", + "add panel to menu", "adicionar painel no menu", or any explicit request to register + something in the vitruvio.json "menu" array. + Do NOT trigger for general panel/process/script creation — menu entries are separate. +--- + +# Add Vitruvio Menu Item + +> All messages shown to the user must be written in Portuguese. + +You are adding an entry to the `menu` array in `vitruvio.json`. Menu entries are **never created automatically** — only when the user explicitly requests it. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Show the current menu structure + +Read `vitruvio.json` and print the menu tree as a readable outline. Example format: + +``` +menu: + [0] menu-root "Meu Módulo" (MENU) + [0] menu-panel "Meu Painel" (PAINEL → my-panel) + [1] menu-report "Meu Relatório" (RELATORIO → my-report) +``` + +If the `menu` array is empty or absent, say so. + +## Step 3 — Collect item details + +Ask the user (in a single message, only ask what is missing): + +- **Type** — one of: + - `PAINEL` — links to a panel (`panelKey`) + - `RELATORIO` — links to a report (`reportKey`) + - `PROCESSO` — links to a process (`processKey`) + - `MENU` — a submenu/folder (no artifact link, has `children`) +- **Target artifact key** — the `key` of the panel/report/process to link (skip if type is `MENU`) +- **Name** — the label shown in the menu +- **Key** — unique key for this menu entry (suggest `menu-` as default) +- **Parent** — root level, or inside which existing `MENU` item? Show the options from Step 2. +- **Order** — integer position within its parent (suggest next available based on existing siblings) +- **Icon** — only relevant for root-level `MENU` items. Vitruvio uses FontAwesome numeric codes (e.g. `61441` = fa-adjust). For children, always `null`. + +## Step 4 — Build the JSON entry + +### Type: PAINEL +```json +{ + "key": "", + "name": "", + "icon": null, + "order": , + "type": "PAINEL", + "panelKey": "", + "children": [] +} +``` + +### Type: RELATORIO +```json +{ + "key": "", + "name": "", + "icon": null, + "order": , + "type": "RELATORIO", + "reportKey": "", + "children": [] +} +``` + +### Type: PROCESSO +```json +{ + "key": "", + "name": "", + "icon": null, + "order": , + "type": "PROCESSO", + "processKey": "", + "children": [] +} +``` + +### Type: MENU (submenu / root folder) +```json +{ + "key": "", + "name": "", + "icon": , + "order": , + "type": "MENU", + "children": [] +} +``` + +## Step 5 — Insert into vitruvio.json + +- Read `vitruvio.json`. +- If `menu` array does not exist, create it as an empty array first. +- If the user chose **root level**: append the entry to the top-level `menu` array. +- If the user chose a **parent item**: find the parent entry by key inside `menu` (search recursively if needed) and append to its `children` array. +- Check that the chosen `key` is not already used anywhere in the menu tree before inserting. +- Write the updated file back preserving formatting (2-space indent). + +## Step 6 — Report + +Tell the user: +- Added `` entry `` ("Name") at `` with order `` +- Remind them: menu order is relative within siblings — reorder adjacent items if needed +- Remind them: `icon` values are FontAwesome numeric codes; use `null` for leaf items diff --git a/.claude/commands/vitruvio-criar-biblioteca.md b/.claude/commands/vitruvio-criar-biblioteca.md new file mode 100644 index 0000000..21ce9ee --- /dev/null +++ b/.claude/commands/vitruvio-criar-biblioteca.md @@ -0,0 +1,100 @@ +# Create Vitruvio Library + +> All messages shown to the user must be written in Portuguese. + +You are creating a new static file library inside a Vitruvio repository. Libraries are static file bundles (JS, CSS, images) served by the platform as HTTP resources. They are different from scripts — scripts run server-side on Rhino; libraries are served to clients as-is. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect library details + +Ask the user (in a single message, only ask what is missing from their original request): + +- **Key (sigla)** — kebab-case unique identifier. Used to reference the library and build its endpoint URL. Stable — changing it breaks any code that references it. +- **Name** — human-readable label shown in the Vitruvio UI. +- **Description** — one sentence about what this library provides (optional). +- **Auth mode** — one of: + - `PUBLIC` — accessible without authentication (default; use for most JS/CSS bundles) + - `STATIC_TOKEN` — token required; use when files must not be publicly accessible +- **Mobile enabled?** — `true` if mobile clients need to load these files; `false` otherwise (default: `false`). +- **What files will this library contain?** — brief description so you can create a useful starting placeholder (e.g. "custom JS utilities for panels", "CSS theme overrides", "image assets"). + +## Step 3 — Create the directory and placeholder file + +```bash +mkdir -p libraries/ +``` + +Create a placeholder file appropriate to what the user described: + +- For a **JS library**: `libraries//.js` +- For a **CSS library**: `libraries//.css` +- For an **image/mixed library**: `libraries//README.md` explaining what belongs here + +### JS placeholder + +```javascript +/** + * Library: + * Key: + * Description: + * + * These files are served as static HTTP resources. + * Access URL: vBibliotecaService.buildEndpointUrl('', '.js') + */ + +// Add your client-side JavaScript here. +// This runs in the browser — full ES6+ is supported (unlike server-side Rhino scripts). +``` + +### CSS placeholder + +```css +/** + * Library: + * Key: + * Description: + * + * These files are served as static HTTP resources. + * Access URL: vBibliotecaService.buildEndpointUrl('', '.css') + */ + +/* Add your styles here */ +``` + +## Step 4 — Register in vitruvio.json + +Read `vitruvio.json`, find or create the `"libraries"` array, and add: + +```json +{ + "key": "", + "name": "", + "description": "", + "type": "LOCAL", + "authMode": "", + "authToken": null, + "mobileEnabled": , + "files": "libraries//" +} +``` + +Omit `"description"` if not provided. Preserve the existing file structure and all other entries. + +## Step 5 — Report + +Tell the user: +- Directory created: `libraries//` +- Placeholder file(s) created +- Registered in `vitruvio.json` with key `` +- How to get the serving URL at runtime: + ```javascript + var url = vBibliotecaService.buildEndpointUrl('', 'filename.js'); + ``` +- Remind them: files in this directory are served as-is — client-side JS here can use modern ES6+, unlike server-side Rhino scripts diff --git a/.claude/commands/vitruvio-criar-endpoint.md b/.claude/commands/vitruvio-criar-endpoint.md new file mode 100644 index 0000000..f8edab2 --- /dev/null +++ b/.claude/commands/vitruvio-criar-endpoint.md @@ -0,0 +1,109 @@ +# Create Vitruvio Endpoint + +> All messages shown to the user must be written in Portuguese. + +You are creating a new REST endpoint inside a Vitruvio repository. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect endpoint details + +Ask the user (in a single message, only ask what is missing from their original request): + +- **Key** — kebab-case unique identifier. Forms the URL path. Changing it later breaks external integrations. +- **Name** — human-readable label shown in the Vitruvio UI. +- **Description** — one sentence about what this endpoint does. +- **HTTP verbs** — which methods to implement: GET, POST, PUT, PATCH, DELETE. Only scaffold the ones actually needed. +- **Auth mode** — one of: + - `PUBLIC` — no authentication (default) + - `STATIC_TOKEN` — token in query param `_x_token_auth` or header `X-WS-TOKEN-AUTH` + - `VITRUVIO_WS_USER_AUTH` — Vitruvio bearer token; `loggedUser` is available in the script + - `HTTP_BASIC_AUTH` — HTTP basic auth; `loggedUser` is available; roles validated by Vitruvio admin + +## Step 3 — Create the file + +Target path: `endpoints/.js` + +URL after deploy: + +| authMode | URL | +|---|---| +| `PUBLIC` | `/api/integration/public/` | +| `STATIC_TOKEN` | `/api/integration/tokenauth/` | +| `VITRUVIO_WS_USER_AUTH` | `/api/integration/bearerauth/` | +| `HTTP_BASIC_AUTH` | `/api/integration/bauth/` | + +Template (include only the requested verbs): + +```javascript +/** + * Nome: + * Sigla: + * Descrição: + * Auth: + */ +function WebService() { + + // this.onGet = function(params) { ... } ← GET / DELETE: params has .headers and .query + // this.onPost = function(params) { ... } ← POST / PUT / PATCH: params also has .requestBody (string, always JSON.parse before use) + + this.onPost = function(params) { + try { + var body = JSON.parse(params.requestBody); + if (!body.id) throw 'Missing required field: id'; + + // implementation here + + return JSON.stringify({ success: true }); + } catch (e) { + return JSON.stringify({ error: e.toString() }); + } + }; + +} + +module.exports = new WebService(); +``` + +Rules (Rhino ES5 — no exceptions): +- No `let`, `const`, arrow functions, template literals, destructuring, spread, `class`, or `import/export` +- Use `var` everywhere +- Always `JSON.parse(params.requestBody)` before accessing the body — never trust it raw +- Always return strings — `JSON.stringify(obj)`, not raw objects +- Return `null` or nothing for `204 No Content`; return a string for `200 OK` +- Remove unused verb stubs entirely — don't leave placeholder bodies +- Never concatenate user input into SQL strings; use named bind params (`:paramName`) +- Don't hardcode datasource names or tokens — read from `vConfigService` or a DB config table +- Put heavy logic in a separate script loaded via `libService.loadScript`, not inline in the endpoint + +## Step 4 — Register in vitruvio.json + +Read `vitruvio.json`, find or create the `"endpoints"` array, and add: + +```json +{ + "key": "", + "name": "", + "description": "", + "language": "javascript", + "authMode": "", + "active": true, + "source": "endpoints/.js" +} +``` + +Preserve the existing file structure and all other entries. Write the updated `vitruvio.json` back. + +## Step 5 — Report + +Tell the user: +- File created: `endpoints/.js` +- Registered in `vitruvio.json` with key `` +- URL once deployed (based on authMode) +- Which verbs were scaffolded diff --git a/.claude/commands/vitruvio-criar-form-desktop.md b/.claude/commands/vitruvio-criar-form-desktop.md new file mode 100644 index 0000000..a11edd1 --- /dev/null +++ b/.claude/commands/vitruvio-criar-form-desktop.md @@ -0,0 +1,279 @@ +# Create Vitruvio Desktop Form + +> All messages shown to the user must be written in Portuguese. + +Desktop forms are Vaadin 8 forms defined in XML and rendered by the Vitruvio engine. This +skill creates the **desktop** form. There are two variants that share almost all of their +component vocabulary but differ in their root element and how variables flow — see +**Differences: panel vs process** below. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Determine variant and target + +Ask (only what is missing): + +- **Panel or process form?** (decides root element / file location — see below) +- **Key** — the panel or process key (the folder name). +- For a **process**, which `formKey`(s) are needed — they must match the `activiti:formKey` + values in `processes//.bpmn`. +- **What should the form show/do?** — fields and behaviour, so you can scaffold something useful. + +## Differences: panel vs process + +| | Panel | Process | +|----------------|-------|---------| +| File | `panels//-desktop.xml` | `processes//-desktop.xml` | +| Root element | `` | `` (with `processKey` attribute optional) | +| Namespace | `http://www.davinti.com.br/vitruvio/form/panel` | `http://www.davinti.com.br/vitruvio/form` | +| XSD | `vitruvio-panel-form.xsd` | `vitruvio-form.xsd` | +| Forms per file | exactly one `
` | **one `` per BPMN `activiti:formKey`** | +| Variables | none built-in; use `engine.getGlobalVariable` | process variables: `engine.getVariable`/`setVariable`; submitted field `id="X"` in `formKey="A"` → variable `A_X` | +| `` | not used | optional: shared `` reused via `` | + +Everything below (components, DBTable, engine API, ES5 rules) is **identical** for both. + +## Step 3 — Scaffold the file + +### Panel variant — `panels//-desktop.xml` + +```xml + + + + + + + + + + + + + + + + + + +``` + +### Process variant — `processes//-desktop.xml` + +```xml + + + + + + + + + +
+ Abertura + Abertura do processo + + + + + + + + +
+ +
+ Executar + Etapa de execução + + + + + + + + + + + + +
+ +
+``` + +## Components (78 desktop components) + +Before using a component you are unsure about, read its reference: +`~/.local/share/vitruvio-platform/docs/components/desktop/.md` +(full list in `docs/components/INDEX.md`). + +### Layout + +| Component | Common attributes | +|---|---| +| `VerticalLayout` | `spacing`, `margin`, `width`, `height`, `expandRatio` | +| `HorizontalLayout` | same as above | +| `Panel` | `id`, `caption`, `width`, `height`, `margin` | +| `TabLayout` | `id`, `width`, `framed` — contains `` children | + +### Widgets + +| Component | Key attributes | +|---|---| +| `TextField` | `id`, `type` (`string`/`number`), `caption`, `width`, `required` | +| `NumericField` | `id`, `type`, `caption`, `width`, `visible` | +| `DateField` | `id`, `type` (`date`/`datetime`), `caption`, `resolution` (`DAY`/`MINUTE`) | +| `ComboBox` | `id`, `type`, `caption`, `allowNullSelection` — children: `` | +| `Label` | `id`, `width`, `contentMode` (`HTML`/`TEXT`) — child: `...` | +| `ButtonWidget` | `id`, `caption`, `style` (`GREEN`/`RED`/`DEFAULT`), `defaultIcon` — child: `` | +| `RichTextArea` | `id`, `type`, `caption`, `width`, `height` | +| `ImageWidget` | `id`, `width`, `height` — child: `...` | + +### DBTable (data grid) + +```xml + + + + + + + + + + + + + CHAVE + + + + + + + + + + + + + +``` + +**SQL in datasources:** use `${paramName}` for substitution — NOT `:paramName`. Named +params (`:paramName`) are only for `queries/*.sql` files. + +## engine API + +```javascript +// Fields +engine.getField('id').getValue() +engine.getField('id').getConvertedValue() // typed value (number, date, etc.) +engine.getField('id').setValue(value) +engine.getField('id').setEnabled(bool) +engine.getField('id').setVisible(bool) +engine.getField('id').setRequired(bool) +engine.getField('id').setCaption('new caption') +engine.getField('id').refresh() // DBTable — re-run its query + +// Widgets / layouts +engine.getWidgetController('id').getButton() +engine.getLayout('id').getSelectedTab() + +// User +engine.getLoggedUser().getLogin() +engine.getLoggedUser().getNome() + +// Global variables (survive tab changes within a session) +engine.setGlobalVariable('key', value) +engine.getGlobalVariable('key') + +// Process forms only — process variables: +engine.getVariable('varName') +engine.setVariable('varName', value) +engine.getProcessDefinitionId() +engine.formKey() // current form's formKey +engine.getFormName() + +// Open another panel / load a library +var vUI = libService.loadScript('vUI'); +vUI.showPanel('panelKey', { param1: value1 }); +var lib = libService.loadScript('scriptKey'); +``` + +## Script rules (Rhino ES5) + +Desktop form scripts run on **Rhino ES5** — no `let`/`const`, arrow functions, template +literals, destructuring, `class`, `import/export`. Use `var`, string `+` concat, +`JSON.parse`/`JSON.stringify`, `importClass(Packages.some.java.Class)` for Java interop. +(See the repo `CLAUDE.md` "JavaScript — ES5 / Rhino Engine" section.) + +> Note: this ES5 rule is for **desktop**. Mobile forms use modern JS on the client — see +> **vitruvio-criar-form-mobile**. + +## Step 4 — Manifest + +If run **standalone**, set `"forms"."desktop"` to the file path on the existing panel or +process entry in `vitruvio.json`. Full entry creation is handled by **vitruvio-criar-painel** +/ **vitruvio-criar-processo**. + +## Step 5 — Report + +Tell the user: +- File created/updated and which variant (panel/process). +- For processes: each `
` must match an `activiti:formKey` in the BPMN, and + submitted field `id="X"` in `formKey="A"` becomes process variable `A_X`. +- `run()` in `` is called every time the form opens. +- `${paramName}` for SQL substitution in datasources; `:paramName` only in named query files. diff --git a/.claude/commands/vitruvio-criar-form-mobile.md b/.claude/commands/vitruvio-criar-form-mobile.md new file mode 100644 index 0000000..ff89878 --- /dev/null +++ b/.claude/commands/vitruvio-criar-form-mobile.md @@ -0,0 +1,255 @@ +# Create Vitruvio Mobile Form + +> All messages shown to the user must be written in Portuguese. + +Mobile forms are **not** desktop forms with a different skin. They use a different schema, +a smaller component set, a different JavaScript runtime, and an explicit server↔client data +contract. Read this whole skill before scaffolding — the mistakes here are not the desktop +mistakes. + +## The four things that make mobile different + +1. **Different schema.** Root is ``, namespace + `http://www.davinti.com.br/vitruvio/mobile-form`, XSD `vitruvio-mobile-form.xsd`. +2. **Only 18 components** (vs 78 desktop). Do **not** assume a desktop widget exists on + mobile. The full list: + `CheckBox, ComboBox, GoogleMapsField, HorizontalLayout, ImageLibraryByFieldValue, + ImageWidget, Label, MaskedField, MoneyField, NumericField, OptionGroup, + ProgressBarWidget, RatingStars, SignaturePadField, TabLayout, TextField, Toggle, + VerticalLayout` (plus structural `SubForm`/`ItemList`). Read + `~/.local/share/vitruvio-platform/docs/components/mobile/.md` before using one. +3. **Two JavaScript runtimes.** + - **Client-side** (`initScript`, `discoveryScript`, validators, component event scripts): + **modern React-Native JS** — arrow functions, Promises, `.then()/.catch()` are fine and + expected. Most APIs are **async** and return Promises. + - **Server-side** (`` `execute(...)` bodies): **Rhino ES5**, same rules + as scripts/desktop. This is the only place with access to platform libs and services + (`libService`, `runtimeService`, db, etc.). +4. **Data is explicit.** Nothing is auto-injected the way desktop process variables are. Every + piece of server data the form needs must be declared — via an `` variable, a + `` (synced to the device, works offline), or fetched on demand from a + named `` with `vCommunicationService.executeOnServer(...)`. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Determine variant and gather the data contract + +Ask (only what is missing): + +- **Panel or process mobile form?** (see **Differences: panel vs process** below) +- **Key** — the panel or process key (folder name). +- For a **process**: which `formKey`(s) — they must match the `activiti:formKey` in + `processes//.bpmn`. +- **Which variables does the form need?** Because nothing is auto-injected, ask explicitly: + - process variables to read (each becomes an `` `` and/or a Bridge call) + - reference/lookup data (each becomes a `` backed by a `queries/*.sql`) + - any server-only logic / libs needed (each becomes a ``) +- **Offline?** Whether the data sources must be available without connectivity (affects + `autoSyncOnInit` / `autoSyncOnDiscovery`). + +## Differences: panel vs process + +The schema, component set, ServerSide/Bridge mechanics and JS runtimes below are **identical** +for both. Only these differ: + +| | Panel mobile form | Process mobile form | +|----------------|-------------------|---------------------| +| File | `panels//-mobile.xml` | `processes//-mobile.xml` | +| Root attribute | `` (no `processKey`) | `` | +| Forms per file | one `` | **one `` per BPMN `activiti:formKey`** (must match) | +| Manifest | set `forms.mobile` **and** `showInMobileList: true` on the panel entry | set `forms.mobile` (and optionally `forms.mobileAlternative`) on the process entry | +| Variables | use `engine.getGlobalVariable` / Autoload | process variables are fetched server-side via a Bridge (`runtimeService.getVariable`) and/or declared in `` | + +> Filename: name files by the **artifact key + suffix** — `-mobile.xml` (and +> `-desktop.xml`, `.bpmn`). This keeps every form searchable by its key instead of +> dozens of identical `form-mobile.xml` tabs. Older content used `form-mobile.xml` / +> `form_web_mobile.xml`; the real path is whatever `forms.mobile` points to, so legacy files +> still work — but new scaffolds use `-mobile.xml`. + +## Step 3 — Scaffold the file + +### Skeleton (process variant shown; for a panel drop `processKey` and use a single ``) + +```xml + + + + + Coleta + Etapa de coleta no app + + + + + + + + + { + var n = parseInt(result, 10); + execution.setVariable('numeroCarga', n).then(ok => {}).catch(err => { + console.log('Erro ao setar numeroCarga localmente', err); + }); + }).catch(err => { + console.log('Erro ao coletar numeroCarga no server', err); + }); + } + ]]> + + + + + + + + + + + + + + + + + + + + + + + + nomeLoja + numeroCarga + + + + + + + + + + + + + + + + + + + + +``` + +## How libs and process variables reach the mobile form + +The mobile app cannot call `libService.loadScript(...)` or read process variables directly — +those live on the server. The pattern is always **declare a Bridge, call it from the client**: + +```xml + + + + +``` + +```javascript +// CLIENT-SIDE (modern JS): call the bridge, use the Promise result +vCommunicationService.executeOnServer('imagemCodBarras', codigoBarras).then(base64 => { + engine.getField('codigoImagem').setValue(base64); +}).catch(err => console.log('Erro no bridge imagemCodBarras', err)); +``` + +Rules of thumb: +- Anything needing a **platform lib, the database, or a platform service** → put it in a + **Bridge** (server, ES5) and call it with `vCommunicationService.executeOnServer`. +- **Reference/lookup tables** the form reads repeatedly → a **`QueryDataSource`** backed by a + `queries/*.sql` (works offline once synced). +- **Process variables** the form needs → either declare them in `` or fetch them + in a Bridge via `runtimeService.getVariable(processInstanceId, 'varName')` and store locally + with `execution.setVariable(...)`. +- Client-side `execution.getVariable(...)` / `execution.setVariable(...)` are **async** and + return Promises — use `.then()`. + +## List-based entry: SubForm + ItemList + +For "add many items" screens (collect a list of rows), use a `SubForm` with an ``: + +```xml + + Executar AÇÃO + + + + + + + + + + + + +``` + +Validate list completeness with `engine.getSubFormListSizeByStatus(function(size){ ... }, "all")` +inside a `COMPLETE` validator. + +## Step 4 — Manifest + +If run **standalone**, update the matching entry in `vitruvio.json`: +- **Panel**: set `forms.mobile = "panels//-mobile.xml"` and `showInMobileList: true`. +- **Process**: set `forms.mobile = "processes//-mobile.xml"`. + +Full entry creation is handled by **vitruvio-criar-painel** / **vitruvio-criar-processo**. + +## Step 5 — Report + +Tell the user: +- File created/updated and variant (panel/process). +- Which variables/queries/bridges were declared, and that **only declared data is available** + on the device — anything else must be added as an Autoload variable, QueryDataSource, or Bridge. +- Reminder: client scripts are modern JS (Promises); Bridge bodies are server-side Rhino ES5. +- For processes: each `
` must match an `activiti:formKey` in the BPMN. +- Suggest reading `docs/components/mobile/` and an example + (`~/.local/share/vitruvio-platform/examples/processes/*/form_web_mobile.xml`) for richer screens. diff --git a/.claude/commands/vitruvio-criar-painel.md b/.claude/commands/vitruvio-criar-painel.md new file mode 100644 index 0000000..1c0db0e --- /dev/null +++ b/.claude/commands/vitruvio-criar-painel.md @@ -0,0 +1,92 @@ +# Create Vitruvio Panel (orchestrator) + +> All messages shown to the user must be written in Portuguese. + +A panel is one or two form files plus a manifest entry: + +| Part | Skill that owns it | +|------|--------------------| +| `panels//-desktop.xml` (web form) | **vitruvio-criar-form-desktop** (panel variant) | +| `panels//-mobile.xml` (app form, optional) | **vitruvio-criar-form-mobile** (panel variant) | + +This skill collects the intent once, drives those skills, and registers the panel in +`vitruvio.json`. Do the file work by following the referenced skills — do not re-derive +their templates here. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect panel details + +Ask the user (in a single message, only ask what is missing): + +- **Key** — PascalCase or snake_case unique identifier. Used in `engine.showPanel(key)` and + as the folder name. +- **Name** — human-readable label shown in the Vitruvio menu. +- **Category** — display category, hierarchical with `/` (e.g. `"Comercial"`, + `"Auditoria/Gondola"`). +- **Description** — one sentence (optional). +- **Open in new window?** — `true`/`false`. Default `true`. +- **Show in mobile list?** — `true`/`false`. Default `false`. +- **Needs a mobile form?** — if yes, a `-mobile.xml` is created too. Mobile is a different + schema with explicit data wiring (the mobile skill will ask for variables/lookups/libs). +- **What should the panel do?** — fields/behaviour, so the form scaffold is useful. + +## Step 3 — Scaffold + +```bash +vitruvio new panel --name "" +``` + +This creates `panels//-desktop.xml` and the `vitruvio.json` entry. Then replace the +generated form using the focused skills: + +1. **Desktop form** — follow **vitruvio-criar-form-desktop** (panel variant) to write + `panels//-desktop.xml` from the user's description. +2. **Mobile form (if requested)** — follow **vitruvio-criar-form-mobile** (panel variant) to + write `panels//-mobile.xml`. `vitruvio new` does not create it. + +## Step 4 — Update vitruvio.json entry + +`vitruvio new` already added the entry. Full shape: + +```json +{ + "key": "", + "name": "", + "description": "", + "category": "", + "displayOrder": 0, + "showInPresentation": false, + "openInNewWindow": true, + "showInMobileList": false, + "displayTimeInSeconds": 0, + "allowedGroups": [], + "allowedUsers": [], + "forms": { + "desktop": "panels//-desktop.xml" + } +} +``` + +Fields `vitruvio new` typically leaves at defaults that need updating: +- `"category"` — set to the user's category (default `""`). +- `"description"` — add if provided. +- `"openInNewWindow"` — set to `false` for same-window. +- `"showInMobileList"` — set to `true` for mobile visibility. +- If a mobile form was created, add `"mobile": "panels//-mobile.xml"` inside `"forms"` + (and set `showInMobileList: true`). + +## Step 5 — Report + +Tell the user: +- Files created: `-desktop.xml` (and `-mobile.xml` if applicable). +- Registered in `vitruvio.json` with key ``. +- `run()` in `` is called every time the panel opens. +- `${paramName}` for SQL substitution in datasource blocks; `:paramName` only in named query files. +- For mobile: only explicitly declared data is available on the device — see vitruvio-criar-form-mobile. diff --git a/.claude/commands/vitruvio-criar-patch.md b/.claude/commands/vitruvio-criar-patch.md new file mode 100644 index 0000000..22c8d39 --- /dev/null +++ b/.claude/commands/vitruvio-criar-patch.md @@ -0,0 +1,120 @@ +# Create Vitruvio Patch + +> All messages shown to the user must be written in Portuguese. + +You are creating a new Liquibase database migration patch inside a Vitruvio repository. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Inspect existing patches to determine the next changeset ID + +```bash +find patches/ -name "*.xml" | xargs grep -h 'id="[0-9]' 2>/dev/null | grep -oP 'id="\K[0-9]+' | sort -n | tail -1 +``` + +The next ID = last found ID + 1. If no patches exist yet, start at 1. + +Also check whether oracle and postgresql subdirectories already exist: + +```bash +ls patches/ +``` + +## Step 3 — Collect migration details + +Ask the user (in a single message, only ask what is missing): + +- **What this migration does** — describe the change (e.g. "add column STATUS to table PEDIDO", "create table AUDIT_LOG", "insert config rows"). +- **Author** — Vitruvio username (e.g. `joao.felix`). Use `git config user.name` if unsure. +- **Module key** — used in the filename (e.g. `GO`, `checklist`, `faturamento`). Default: the repo's `metadata.key` from `vitruvio.json`. + +## Step 4 — Create the patch files + +**Both oracle/ and postgresql/ files must always be created and kept in sync.** + +Filename convention: `{YYYYMMDDHHmm}_{MODULE_KEY}.xml` (e.g. `202506011430_checklist.xml`). Use the current date and time. + +```bash +mkdir -p patches/oracle patches/postgresql +``` + +### Absolute rules + +- **Append-only.** Never edit or delete existing `` entries — modifying a checksum that Liquibase already recorded breaks deployment. +- **Unique numeric IDs.** Each `` must have a unique ID within the repo. Increment from the last found. +- **Always use ``** — every changeset must be idempotent and safe to re-run on any DB state. +- **Oracle ≠ PostgreSQL.** Write each file for its target DB — data types, sequences, and quoting differ. Never copy-paste blindly. +- **One logical change per changeset** — don't batch unrelated changes into a single ``. + +### File skeleton + +```xml + + + + + + + + + -- SQL for this DB dialect + + + + +``` + +### preConditions reference + +| Operation | Guard to use | +|-----------|-------------| +| CREATE TABLE | `` | +| ADD COLUMN | `` | +| CREATE INDEX | `` | +| ADD CONSTRAINT / FK | `` | +| INSERT row (by PK) | `SELECT COUNT(*) FROM MY_TABLE WHERE ID = 1` | +| DROP TABLE | `` | +| DROP COLUMN | `` | + +### Oracle vs PostgreSQL differences to watch + +| | Oracle | PostgreSQL | +|---|---|---| +| Auto-increment | Separate `CREATE SEQUENCE` + trigger or `DEFAULT seq.NEXTVAL` | `SERIAL` or `GENERATED ALWAYS AS IDENTITY` | +| String type | `VARCHAR2(n)` | `VARCHAR(n)` | +| Boolean | `NUMBER(1)` | `BOOLEAN` | +| Date/time | `DATE`, `TIMESTAMP` | `DATE`, `TIMESTAMP` | +| Current timestamp | `SYSDATE` | `CURRENT_TIMESTAMP` | +| Quoting | `LEGACY` strategy (unquoted) | Same | + +## Step 5 — Check vitruvio.json patches registration + +The patches directory only needs to be registered once. Check if it is already there: + +```bash +grep -A2 '"patches"' vitruvio.json +``` + +If not registered, add to `vitruvio.json`: + +```json +"patches": "patches/" +``` + +## Step 6 — Report + +Tell the user: +- Files created: `patches/oracle/.xml` and `patches/postgresql/.xml` +- Changeset IDs used +- Summary of what each changeset does +- Reminder: never edit existing changesets once committed — add new ones instead diff --git a/.claude/commands/vitruvio-criar-processo-bpmn.md b/.claude/commands/vitruvio-criar-processo-bpmn.md new file mode 100644 index 0000000..664b4d7 --- /dev/null +++ b/.claude/commands/vitruvio-criar-processo-bpmn.md @@ -0,0 +1,216 @@ +# Create Vitruvio Process BPMN + +> All messages shown to the user must be written in Portuguese. + +You are creating the **BPMN workflow file** of a Vitruvio process. This skill is focused +on the `.bpmn` file only — the desktop form is handled by **vitruvio-criar-form-desktop** +(process variant) and the mobile form by **vitruvio-criar-form-mobile** (process variant). + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect flow details + +Ask the user (in a single message, only ask what is missing): + +- **Key** — snake_case or camelCase unique identifier. Becomes the BPMN process ID and the + folder name. Must match the `key` used for the process in `vitruvio.json`. +- **Name** — human-readable label. +- **Who can start it?** — group key(s) allowed to open the process (e.g. `gestao`, + `admin`). Used in `activiti:candidateStarterGroups`. +- **Steps** — the human tasks (user tasks), and which group handles each. A simple linear + flow is enough to start. +- **Decisions / branches?** — any exclusive gateways with conditions. +- **Automatic steps?** — any script tasks running between user tasks. + +## Step 3 — Write the BPMN file: `processes//.bpmn` + +### Critical rules + +- The `` value is the canonical process identity. The importer + reads it from the BPMN, not from vitruvio.json. **It must match the `key`.** +- Every node must appear in a `` / `` **and** in the + `` section — Vitruvio renders the diagram. +- Each `activiti:formKey` on the start event and user tasks must match a + `` in the desktop form XML (and mobile form, if present). +- Process variables from submitted forms are auto-named `{formKey}_{fieldId}` + (e.g. `formAbertura_status`). Gateway conditions reference them. +- Gateway conditions use `#{variable == 'value'}` (JUEL expression language). +- Script tasks call `vScriptService.loadScript('scriptKey', 'javascript')`, **not** + `libService`. + +### Minimal skeleton (start → user task → end, single lane) + +```xml + + + + + + + + + + + inicio + task_executar + fim + + + + + + flow_inicio_task + + + + + flow_inicio_task + flow_task_fim + + + + + + + + flow_task_fim + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Multi-lane pattern (when tasks belong to different roles) + +Add each lane inside ``, list the node IDs inside each lane, and adjust +the BPMNDi bounds: + +```xml + + + inicio + fim + + + task_executar + + +``` + +### Script task — script side + +```javascript +// In inside a scriptTask: +var f = vScriptService.loadScript('meu_script', 'javascript'); +f(execution); + +// In the script file itself (pattern: process/task script — see vitruvio-criar-script): +(function(execution) { + var db = libService.loadScript('db'); + var banco = new db(db.VITRUVIO_DATASOURCE); + var status = execution.getVariable('formAbertura_status'); + // ... +})(execution) +``` + +## Step 4 — Manifest + +If this skill is run **standalone** (the process already exists in `vitruvio.json`), +ensure its entry has `"bpmn": "processes//.bpmn"`. Do not create or modify +the rest of the entry here — full registration is handled by **vitruvio-criar-processo**. + +## Step 5 — Report + +Tell the user: +- File created/updated: `processes//.bpmn` +- The process identity is the `` — it must match the key. +- Each `activiti:formKey` must have a matching `` in the form XML + (create/update it with **vitruvio-criar-form-desktop** / **vitruvio-criar-form-mobile**). +- Submitted field `id="X"` in `formKey="formAbertura"` becomes variable `formAbertura_X`. +- For complex flows, recommend editing the BPMN in bpmn.io or Camunda Modeler before deploying. diff --git a/.claude/commands/vitruvio-criar-processo.md b/.claude/commands/vitruvio-criar-processo.md new file mode 100644 index 0000000..08e6990 --- /dev/null +++ b/.claude/commands/vitruvio-criar-processo.md @@ -0,0 +1,87 @@ +# Create Vitruvio Process (orchestrator) + +> All messages shown to the user must be written in Portuguese. + +A process is made of up to three artifacts plus a manifest entry: + +| Part | Skill that owns it | +|------|--------------------| +| `processes//.bpmn` (workflow) | **vitruvio-criar-processo-bpmn** | +| `processes//-desktop.xml` (web form) | **vitruvio-criar-form-desktop** (process variant) | +| `processes//-mobile.xml` (app form, optional) | **vitruvio-criar-form-mobile** (process variant) | + +This skill collects the intent once, drives those skills, and registers the process in +`vitruvio.json`. Do the file work by following the referenced skills — do not re-derive +their templates here. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect process details + +Ask the user (in a single message, only ask what is missing): + +- **Key** — snake_case or camelCase unique identifier. Becomes the BPMN process ID and the + folder name. +- **Name** — human-readable label. +- **Description** — one sentence (optional). +- **Who can start it?** — group key(s) for `activiti:candidateStarterGroups`. +- **Steps** — the human tasks and which group handles each. A simple linear flow is enough. +- **Needs a script task?** — automatic steps running a script between user tasks. +- **Needs a mobile form?** — if yes, a `-mobile.xml` is created too. Remember mobile is a + different schema with explicit data wiring — variables, lookups and libs must be named + up front (the mobile skill will ask). + +## Step 3 — Scaffold + +```bash +vitruvio new process --name "" +``` + +This creates `processes//.bpmn`, `processes//-desktop.xml`, and the +`vitruvio.json` entry. Then replace the generated files using the focused skills: + +1. **BPMN** — follow **vitruvio-criar-processo-bpmn** to write `processes//.bpmn` + from the user's steps/branches. +2. **Desktop form** — follow **vitruvio-criar-form-desktop** (process variant) to write + `processes//-desktop.xml`, one `` per `activiti:formKey` in the BPMN. +3. **Mobile form (if requested)** — follow **vitruvio-criar-form-mobile** (process variant) to + write `processes//-mobile.xml`. `vitruvio new` does not create it. + +## Step 4 — Update vitruvio.json entry + +`vitruvio new` already added the entry. Full shape: + +```json +{ + "key": "", + "name": "", + "description": "", + "bpmn": "processes//.bpmn", + "forms": { + "desktop": "processes//-desktop.xml" + }, + "schedules": [] +} +``` + +- Add `"description"` if the user provided one — `vitruvio new` does not set it. +- If a mobile form was created, add `"mobile": "processes//-mobile.xml"` inside `"forms"`. +- Add `schedules` only if the process runs on a timer (cron/simple interval). + +## Step 5 — Report + +Tell the user: +- Files created: `.bpmn`, `-desktop.xml` (and `-mobile.xml` if applicable). +- Registered in `vitruvio.json` with key ``. +- The process identity is the `` — it must match the key. +- Each `activiti:formKey` in the BPMN must have a matching `` in **every** + form file (desktop and mobile). +- Submitted field `id="X"` in `formKey="formAbertura"` becomes process variable `formAbertura_X` + (desktop auto-injects these; mobile must declare/fetch them — see vitruvio-criar-form-mobile). +- For complex flows, recommend editing the BPMN in bpmn.io or Camunda Modeler before deploying. diff --git a/.claude/commands/vitruvio-criar-query.md b/.claude/commands/vitruvio-criar-query.md new file mode 100644 index 0000000..ad085db --- /dev/null +++ b/.claude/commands/vitruvio-criar-query.md @@ -0,0 +1,63 @@ +# Create Vitruvio Query + +> All messages shown to the user must be written in Portuguese. + +You are creating a new named SQL query inside a Vitruvio repository. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect query details + +Ask the user (in a single message, only ask what is missing from their original request): + +- **Key** — kebab-case unique identifier. Used to reference this query in reports, DBTable components, and scripts. +- **Name** — human-readable label shown in the Vitruvio UI. +- **SQL** — the query itself, or enough context to write it. +- **Connection** — datasource name (default: `vitruvio_producao`). Ask only if the user mentions a specific datasource. + +## Step 3 — Create the file + +Target path: `queries/.sql` + +Rules: +- One `SELECT` per file. No multiple statements, no DDL, no INSERT/UPDATE/DELETE. +- Named bind parameters use `:paramName` syntax — never concatenate user input into SQL. +- Write ANSI SQL where possible. If DB-specific syntax is unavoidable, note it in a comment. +- Keep Oracle and PostgreSQL compatibility in mind — avoid syntax that only works in one. + +```sql +SELECT col1, + col2 + FROM my_table + WHERE active = 1 + AND id = :id + ORDER BY col1 +``` + +## Step 4 — Register in vitruvio.json + +Read `vitruvio.json`, find or create the `"queries"` array, and add: + +```json +{ + "key": "", + "name": "", + "source": "queries/.sql", + "connection": "" +} +``` + +Preserve the existing file structure and all other entries. Write the updated `vitruvio.json` back. + +## Step 5 — Report + +Tell the user: +- File created: `queries/.sql` +- Registered in `vitruvio.json` with key `` +- How it can be used: as a datasource in a report, in a DBTable component, or loaded in a script via `db.executeNamedQuery('', params)` diff --git a/.claude/commands/vitruvio-criar-relatorio.md b/.claude/commands/vitruvio-criar-relatorio.md new file mode 100644 index 0000000..73748c9 --- /dev/null +++ b/.claude/commands/vitruvio-criar-relatorio.md @@ -0,0 +1,118 @@ +# Create Vitruvio Report + +> All messages shown to the user must be written in Portuguese. + +You are registering a new report inside a Vitruvio repository. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Identify the report type + +Ask the user which type applies (if not already clear from the request): + +| Type | When to use | +|------|-------------| +| **MODELO_ESTATICO** | A Jasper report whose layout is fully designed in a `.jrxml` file (Jaspersoft Studio). The dev provides or will provide the `.jrxml`. | +| **DINAMICO_QUERY_SQL** | A Vitruvio-managed report where data comes from a named query and columns/layout are configured in `vitruvio.json`. The `.jrxml` is still needed but is simpler — Vitruvio drives the structure. | + +## Step 3 — Collect details + +Ask the user (in a single message, only ask what is missing): + +**Both types:** +- **Key** — kebab-case or snake_case unique identifier. +- **Name** — human-readable label shown in the UI. +- **Category** — display category (e.g. `"Compras"`, `"Auditoria/Gondola"`). +- **Owner** — Vitruvio username of the responsible person. +- **Orientation** — `RETRATO` (portrait) or `PAISAGEM` (landscape). Default: `RETRATO`. +- **Allowed groups / users** — who can access this report (can be empty arrays). +- **Has parameters?** — does the user fill in parameters before running it? If yes, a params form is needed. + +**DINAMICO_QUERY_SQL only:** +- **Query key** — the named query that feeds the report (must be registered in `vitruvio.json`). +- **Columns** — list of columns: name (DB column), label, alignment (`LEFT`/`CENTER`/`RIGHT`), width (px), aggregation (`null`, `SUM`, `COUNT`, etc.). + +## Step 4 — Create the files + +### MODELO_ESTATICO + +Files live flat in `reports/`: +- `reports/.jrxml` — **do not generate this file**; tell the user to place the Jaspersoft-designed template here. Must target **JasperReports 6.21.2** — do not save with a newer version. +- `reports/-params.xml` — only if the report has parameters (follows the same Vaadin XML form schema as panels). + +### DINAMICO_QUERY_SQL + +Files live in a subdirectory: +- `reports//template.jrxml` — **do not generate this file**; tell the user to place the template here. +- `reports//params.xml` — only if the report has parameters. + +## Step 5 — Register in vitruvio.json + +Read `vitruvio.json`, find or create the `"reports"` array, and add the entry. + +### MODELO_ESTATICO entry + +```json +{ + "key": "", + "name": "", + "type": "MODELO_ESTATICO", + "category": "", + "owner": "", + "template": "reports/.jrxml", + "parameterForm": "reports/-params.xml", + "orientation": "RETRATO", + "allowedGroups": [], + "allowedUsers": [], + "columns": [], + "schedules": [] +} +``` + +Omit `"parameterForm"` if no params form. + +### DINAMICO_QUERY_SQL entry + +```json +{ + "key": "", + "name": "", + "description": "", + "type": "DINAMICO_QUERY_SQL", + "category": "", + "owner": "", + "template": "reports//template.jrxml", + "parameterForm": "reports//params.xml", + "query": "", + "orientation": "RETRATO", + "allowedGroups": [], + "allowedUsers": [], + "columns": [ + { + "name": "COLUMN_NAME", + "label": "Column Label", + "align": "LEFT", + "width": 100, + "aggregation": null + } + ] +} +``` + +Omit `"parameterForm"` if no params form. + +Preserve the existing file structure and all other entries. Write the updated `vitruvio.json` back. + +## Step 6 — Report + +Tell the user: +- Entry registered in `vitruvio.json` with key `` and type `` +- For MODELO_ESTATICO: remind them to place the `.jrxml` at `reports/.jrxml`, designed in Jaspersoft Studio 6.21.2 +- For DINAMICO_QUERY_SQL: remind them to place the template at `reports//template.jrxml` +- If a params form is needed: what file to create and that it follows the same Vaadin XML schema as panels diff --git a/.claude/commands/vitruvio-criar-script.md b/.claude/commands/vitruvio-criar-script.md new file mode 100644 index 0000000..7ab6573 --- /dev/null +++ b/.claude/commands/vitruvio-criar-script.md @@ -0,0 +1,97 @@ +# Create Vitruvio Script + +> All messages shown to the user must be written in Portuguese. + +You are creating a new script inside a Vitruvio repository. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect script details + +Ask the user (in a single message, only ask what is missing from their original request): + +- **Key (sigla)** — unique identifier used in `libService.loadScript('key')`. Snake_case. Must be unique across all scripts in the repo. If the user already provided a name, suggest a key derived from it. +- **Name** — human-readable label shown in the Vitruvio UI. +- **Pattern** — which of the two patterns applies: + - **Library** — reusable module, loaded by other scripts/endpoints/panels via `libService.loadScript`. Wrap in `({...})`. + - **Process/task script** — runs directly from a process task or scheduler. Top-level execution, no export. +- **Description** — one sentence about what this script does (optional, but ask if not provided). +- **Domain** — `USUARIO` (user-level, default) or `SISTEMA` (system-level). + +## Step 3 — Create the file + +Target path: `scripts/.js` + +### Library template + +```javascript +/** + * Nome: + * Sigla: + * Descrição: + */ +({ + // example function — replace with actual implementation + run: function(params) { + var db = libService.loadScript('db'); + var banco = new db(db.VITRUVIO_DATASOURCE); + + // implementation here + + return {}; + } +}) +``` + +### Process / task script template + +```javascript +/** + * Nome: + * Sigla: + * Descrição: + */ +(function(execution) { + var db = libService.loadScript('db'); + var banco = new db(db.VITRUVIO_DATASOURCE); + + // implementation here +})(execution) +``` + +Rules (Rhino ES5 — no exceptions): +- No `let`, `const`, arrow functions, template literals, destructuring, spread, `class`, or `import/export` +- Use `var` everywhere +- No `require`, `process`, `window`, or Node/browser globals +- String concatenation with `+`, not template literals +- `JSON.parse` / `JSON.stringify` for serialization + +## Step 4 — Register in vitruvio.json + +Read `vitruvio.json`, find or create the `"scripts"` array, and add: + +```json +{ + "key": "", + "name": "", + "description": "", + "language": "javascript", + "domain": "", + "source": "scripts/.js" +} +``` + +Preserve the existing file structure and all other entries. Write the updated `vitruvio.json` back. + +## Step 5 — Report + +Tell the user: +- File created: `scripts/.js` +- Registered in `vitruvio.json` with key `` +- How to load it from another script or endpoint: `var lib = libService.loadScript('');` diff --git a/.claude/skills/vitruvio-adicionar-menu/SKILL.md b/.claude/skills/vitruvio-adicionar-menu/SKILL.md new file mode 100644 index 0000000..32074a9 --- /dev/null +++ b/.claude/skills/vitruvio-adicionar-menu/SKILL.md @@ -0,0 +1,121 @@ +--- +name: vitruvio-adicionar-menu +description: > + Use ONLY when the user explicitly asks to add a menu item or menu entry in a Vitruvio repository. + Triggers: "add menu item", "add to menu", "adicionar ao menu", "criar item de menu", + "add panel to menu", "adicionar painel no menu", or any explicit request to register + something in the vitruvio.json "menu" array. + Do NOT trigger for general panel/process/script creation — menu entries are separate. +--- + +# Add Vitruvio Menu Item + +> All messages shown to the user must be written in Portuguese. + +You are adding an entry to the `menu` array in `vitruvio.json`. Menu entries are **never created automatically** — only when the user explicitly requests it. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Show the current menu structure + +Read `vitruvio.json` and print the menu tree as a readable outline. Example format: + +``` +menu: + [0] menu-root "Meu Módulo" (MENU) + [0] menu-panel "Meu Painel" (PAINEL → my-panel) + [1] menu-report "Meu Relatório" (RELATORIO → my-report) +``` + +If the `menu` array is empty or absent, say so. + +## Step 3 — Collect item details + +Ask the user (in a single message, only ask what is missing): + +- **Type** — one of: + - `PAINEL` — links to a panel (`panelKey`) + - `RELATORIO` — links to a report (`reportKey`) + - `PROCESSO` — links to a process (`processKey`) + - `MENU` — a submenu/folder (no artifact link, has `children`) +- **Target artifact key** — the `key` of the panel/report/process to link (skip if type is `MENU`) +- **Name** — the label shown in the menu +- **Key** — unique key for this menu entry (suggest `menu-` as default) +- **Parent** — root level, or inside which existing `MENU` item? Show the options from Step 2. +- **Order** — integer position within its parent (suggest next available based on existing siblings) +- **Icon** — only relevant for root-level `MENU` items. Vitruvio uses FontAwesome numeric codes (e.g. `61441` = fa-adjust). For children, always `null`. + +## Step 4 — Build the JSON entry + +### Type: PAINEL +```json +{ + "key": "", + "name": "", + "icon": null, + "order": , + "type": "PAINEL", + "panelKey": "", + "children": [] +} +``` + +### Type: RELATORIO +```json +{ + "key": "", + "name": "", + "icon": null, + "order": , + "type": "RELATORIO", + "reportKey": "", + "children": [] +} +``` + +### Type: PROCESSO +```json +{ + "key": "", + "name": "", + "icon": null, + "order": , + "type": "PROCESSO", + "processKey": "", + "children": [] +} +``` + +### Type: MENU (submenu / root folder) +```json +{ + "key": "", + "name": "", + "icon": , + "order": , + "type": "MENU", + "children": [] +} +``` + +## Step 5 — Insert into vitruvio.json + +- Read `vitruvio.json`. +- If `menu` array does not exist, create it as an empty array first. +- If the user chose **root level**: append the entry to the top-level `menu` array. +- If the user chose a **parent item**: find the parent entry by key inside `menu` (search recursively if needed) and append to its `children` array. +- Check that the chosen `key` is not already used anywhere in the menu tree before inserting. +- Write the updated file back preserving formatting (2-space indent). + +## Step 6 — Report + +Tell the user: +- Added `` entry `` ("Name") at `` with order `` +- Remind them: menu order is relative within siblings — reorder adjacent items if needed +- Remind them: `icon` values are FontAwesome numeric codes; use `null` for leaf items diff --git a/.claude/skills/vitruvio-atualizar-manifesto/SKILL.md b/.claude/skills/vitruvio-atualizar-manifesto/SKILL.md new file mode 100644 index 0000000..9e56e65 --- /dev/null +++ b/.claude/skills/vitruvio-atualizar-manifesto/SKILL.md @@ -0,0 +1,96 @@ +--- +name: vitruvio-atualizar-manifesto +description: > + Use when the user wants to update, rename, or remove an existing artifact registration + in vitruvio.json. Triggers: "update the category/description/authMode of", "change the name of", + "rename this panel/script/endpoint", "remove this artifact", "unregister", "atualizar manifesto", + "mudar categoria", "remover painel", or any request to modify fields of an already-registered artifact. + Do NOT trigger for creating new artifacts (use vitruvio-criar-* skills) or adding menu entries (use vitruvio-adicionar-menu). +--- + +# Update Vitruvio Manifest Entry + +> All messages shown to the user must be written in Portuguese. + +You are modifying an existing entry in `vitruvio.json`. No new files — only manifest changes (and optionally file moves/deletions). Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Identify operation and target + +Ask the user (in a single message, only ask what is missing): + +- **Operation** — one of: + - **A — Update fields**: change metadata on an existing entry (category, description, authMode, allowedGroups, displayOrder, etc.) + - **B — Remove entry**: unregister an artifact from `vitruvio.json` + - **C — Rename key**: change the artifact's key (breaking change — see Operation C below) +- **Target artifact** — the key of the artifact to modify, and its section (panel, script, endpoint, query, report, library, process). + +Read `vitruvio.json` and show the user the current entry before proceeding. + +--- + +## Operation A — Update fields + +Ask what fields to change and their new values. Then edit `vitruvio.json` with the updated values, preserving all other fields exactly. + +### Editable fields by artifact type + +**Panel:** `name`, `description`, `category`, `displayOrder`, `showInPresentation`, `openInNewWindow`, `showInMobileList`, `displayTimeInSeconds`, `allowedGroups`, `allowedUsers`, `forms.mobile`, `forms.mobileAlternative`, `defaultState`, `thumbnail` + +**Script:** `name`, `description`, `domain` (`USUARIO` / `SISTEMA`) + +**Endpoint:** `name`, `description`, `authMode` (`PUBLIC`, `STATIC_TOKEN`, `VITRUVIO_WS_USER_AUTH`, `HTTP_BASIC_AUTH`), `active` + +**Query:** `name`, `connection` + +**Report:** `name`, `description`, `category`, `owner`, `orientation`, `parameterForm`, `allowedGroups`, `allowedUsers` + +**Library:** `name`, `authMode`, `mobileEnabled` + +**Process:** `name`, `description`, `forms.mobile`, `forms.mobileAlternative` + +After editing, report each field that changed (old value → new value). + +--- + +## Operation B — Remove entry + +1. Show the full current entry from `vitruvio.json` to the user. +2. Ask for explicit confirmation before proceeding — do not remove without a yes. +3. Remove the entry from its section array in `vitruvio.json`. If the section array becomes empty, remove the array key entirely. +4. Ask the user: **"Do you also want to delete the artifact's files from disk?"** — list which files/directories would be deleted. Do not delete anything without an explicit yes. +5. If yes, delete the files. +6. Report: entry removed from `vitruvio.json`; files deleted if confirmed. + +--- + +## Operation C — Rename key + +> **Warning to show the user before proceeding:** +> Renaming a key is a breaking change. Any code that references the old key — `engine.showPanel('')`, `libService.loadScript('')`, menu entries, process variables, external integrations — will break. Search the repo for the old key before confirming. + +1. Show the warning above and ask the user to confirm they understand. +2. Ask for the new key (kebab-case). Check it is not already used in the same section of `vitruvio.json`. +3. Update the `key` field in `vitruvio.json`. +4. Update any `source`, `bpmn`, `template`, `parameterForm`, `files`, or `forms.*` paths inside the same entry that include the old key in their path. +5. Rename the artifact's directory or file on disk: + - `panels//` → `panels//` + - `scripts/.js` → `scripts/.js` + - `endpoints/.js` → `endpoints/.js` + - `queries/.sql` → `queries/.sql` + - `reports//` → `reports//` + - `libraries//` → `libraries//` + - `processes//` → `processes//` +6. Check if a menu entry references the old key (`panelKey`, `reportKey`, `processKey`) and update it too. +7. Report: what was renamed in `vitruvio.json` and on disk. Remind the user to search the codebase for any remaining hardcoded references to the old key: + +```bash +grep -r "" --include="*.xml" --include="*.js" --include="*.json" . +``` diff --git a/.claude/skills/vitruvio-criar-biblioteca/SKILL.md b/.claude/skills/vitruvio-criar-biblioteca/SKILL.md new file mode 100644 index 0000000..ac4dc39 --- /dev/null +++ b/.claude/skills/vitruvio-criar-biblioteca/SKILL.md @@ -0,0 +1,109 @@ +--- +name: vitruvio-criar-biblioteca +description: > + Use when the user wants to create a new static file library in a Vitruvio repository. + Triggers: "create library", "new library", "criar biblioteca", "nova biblioteca", "add library", + "static files", "JS library", "CSS library", or any request to scaffold a libraries// directory + and register it in vitruvio.json. +--- + +# Create Vitruvio Library + +> All messages shown to the user must be written in Portuguese. + +You are creating a new static file library inside a Vitruvio repository. Libraries are static file bundles (JS, CSS, images) served by the platform as HTTP resources. They are different from scripts — scripts run server-side on Rhino; libraries are served to clients as-is. Follow these steps in order. + +## Step 1 — Confirm you are inside a Vitruvio repo + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo. + +## Step 2 — Collect library details + +Ask the user (in a single message, only ask what is missing from their original request): + +- **Key (sigla)** — kebab-case unique identifier. Used to reference the library and build its endpoint URL. Stable — changing it breaks any code that references it. +- **Name** — human-readable label shown in the Vitruvio UI. +- **Description** — one sentence about what this library provides (optional). +- **Auth mode** — one of: + - `PUBLIC` — accessible without authentication (default; use for most JS/CSS bundles) + - `STATIC_TOKEN` — token required; use when files must not be publicly accessible +- **Mobile enabled?** — `true` if mobile clients need to load these files; `false` otherwise (default: `false`). +- **What files will this library contain?** — brief description so you can create a useful starting placeholder (e.g. "custom JS utilities for panels", "CSS theme overrides", "image assets"). + +## Step 3 — Scaffold and create files + +```bash +vitruvio new library --name "" +``` + +This creates the `libraries//` directory and registers it in `vitruvio.json` with `authMode: "PUBLIC"` and `mobileEnabled: false`. Then create a placeholder file appropriate to what the user described: + +- For a **JS library**: `libraries//.js` +- For a **CSS library**: `libraries//.css` +- For an **image/mixed library**: `libraries//README.md` explaining what belongs here + +### JS placeholder + +```javascript +/** + * Library: + * Key: + * Description: + * + * These files are served as static HTTP resources. + * Access URL: vBibliotecaService.buildEndpointUrl('', '.js') + */ + +// Add your client-side JavaScript here. +// This runs in the browser — full ES6+ is supported (unlike server-side Rhino scripts). +``` + +### CSS placeholder + +```css +/** + * Library: + * Key: + * Description: + * + * These files are served as static HTTP resources. + * Access URL: vBibliotecaService.buildEndpointUrl('', '.css') + */ + +/* Add your styles here */ +``` + +## Step 4 — Update vitruvio.json entry + +`vitruvio new` already added the library entry. The full entry shape is: + +```json +{ + "key": "", + "name": "", + "description": "", + "type": "LOCAL", + "authMode": "PUBLIC", + "authToken": null, + "mobileEnabled": false, + "files": "libraries//" +} +``` + +Update only what differs: set `"authMode"` if not `"PUBLIC"`; set `"mobileEnabled": true` if mobile clients need this library; add `"description"` if provided. + +## Step 5 — Report + +Tell the user: +- Directory created: `libraries//` +- Placeholder file(s) created +- Registered in `vitruvio.json` with key `` +- How to get the serving URL at runtime: + ```javascript + var url = vBibliotecaService.buildEndpointUrl('', 'filename.js'); + ``` +- Remind them: files in this directory are served as-is — client-side JS here can use modern ES6+, unlike server-side Rhino scripts diff --git a/.claude/skills/vitruvio-criar-dashboard-desktop/SKILL.md b/.claude/skills/vitruvio-criar-dashboard-desktop/SKILL.md new file mode 100644 index 0000000..ec4bf2a --- /dev/null +++ b/.claude/skills/vitruvio-criar-dashboard-desktop/SKILL.md @@ -0,0 +1,1614 @@ +--- +name: vitruvio-criar-dashboard-desktop +description: > + Use when creating or adapting the DESKTOP form of a Vitruvio indicator dashboard panel — + KPI cards, SVG charts (line/donut/bar), and a drill-down movimentação table, following the + dashboard-contratos / dashboard-tecnicos pattern. Triggers: "criar dashboard", "criar + indicador", "criar painel indicador", "novo dashboard de KPIs", "dashboard com gráficos", or + adapting an existing panel XML to this pattern. Called by vitruvio-criar-indicador-dashboard + for the desktop part; can also be invoked directly with an optional existing XML path. For + the mobile shell of an existing indicator dashboard use vitruvio-criar-dashboard-mobile. +--- + +# Criar Painel Indicador Dashboard + +> Todas as mensagens ao usuário devem ser em Português. + +Você está criando **ou adaptando** um painel indicador no padrão Vitruvio com gráficos, KPIs e movimentação com drill-down. O padrão é baseado nos painéis `dashboard-contratos` e `dashboard-tecnicos` deste repositório. + +Receba como argumento opcional o caminho de um arquivo XML existente (ex: `panels/meu-painel/meu-painel-desktop.xml`) para adaptar ao padrão. Sem argumento, cria um novo painel do zero. + +--- + +## LEITURA OBRIGATÓRIA AO INICIAR — CONTEXT.md + +**Esta é a primeira coisa a fazer, sempre, antes de qualquer outra ação.** + +Se o argumento fornecido aponta para um painel existente (ex: `panels/meu-painel/meu-painel-desktop.xml`), derive o diretório do painel e tente ler o arquivo de contexto: + +``` +panels//CONTEXT.md +``` + +**Se o CONTEXT.md existir:** +- Leia-o completo antes de abrir qualquer outro arquivo. +- Use-o como fonte primária de verdade sobre o estado atual do painel: queries SQL reais, IDs dos campos, funções JS, pendências, histórico. +- Só leia o `-desktop.xml` depois, para verificar se o CONTEXT.md ainda está em sincronia com o código. Se houver divergência, atualize o CONTEXT.md ao final. +- Informe ao usuário: _"Li o contexto do painel. Última modificação: [data]. Pendências encontradas: [lista do TODO]."_ + +**Se o CONTEXT.md não existir ainda:** +- Informe ao usuário que não há arquivo de contexto e que ele será criado ao final. +- Leia o `-desktop.xml` completo para reconstruir o contexto. + +**Para novo painel (sem argumento):** não há CONTEXT.md ainda — será criado na Fase 8. + +--- + +## Princípios obrigatórios — leia antes de qualquer coisa + +### Nunca invente — pergunte quando tiver dúvida + +- **Não invente nomes de tabelas, colunas, views ou sequências.** Se o usuário não fornecer o DDL ou a estrutura da tabela, pergunte. Uma pergunta custa menos do que um bug em produção. +- Se o usuário mencionar uma tabela mas não os campos, pergunte quais campos existem nela antes de escrever SQL. +- Se houver dúvida sobre tipo de dado (numérico, texto, data), pergunte. +- Baseie-se **sempre** em código real. Os dois painéis de referência canônicos deste padrão estão + empacotados junto com esta skill — leia-os por completo antes de escrever qualquer linha: + - `.claude/skills/vitruvio-criar-dashboard-desktop/dashboard-contrato-desktop.xml` + - `.claude/skills/vitruvio-criar-dashboard-desktop/dashboard-tecnicos-desktop.xml` + - `dashboard-contrato-desktop.xml` é a versão mais recente/evoluída do padrão (ex: usa a global + `_mobPreviewMode` que `dashboard-tecnicos-desktop.xml` ainda não tem) — em caso de divergência + entre os dois, prefira o padrão do `dashboard-contrato-desktop.xml`. + - `.claude/skills/vitruvio-criar-dashboard-desktop/dashboard_ia.js` documenta a API do `dashLib` + (`addSharedCss`, `loadPreferences`, `savePreferences`, `applyTheme`, paleta de cores, etc.). + **É material de consulta apenas** — leia-o para saber que métodos existem e como usá-los, mas + **nunca crie, copie ou registre um `scripts/dashboard_ia.js` no repositório de destino.** O + painel gerado apenas chama `libService.loadScript('dashboard_ia')`; a lib em si é externa ao + escopo desta skill. + +### JavaScript — ES5 Rhino somente + +```javascript +// PROIBIDO: const, let, =>, template literals, destructuring, class, async/await, import/export +// OBRIGATÓRIO: var, function(){}, concatenação de strings, for(var i...) +``` + +### Paleta de cores padrão (do script `dashboard_ia`) + +| Contexto | Cor | +|---|---| +| Fundo dark (wrapper) | `#0f1923` | +| Card/box dark | `#1a2733` | +| Borda/grid dark | `#243447` | +| Texto primário dark | `#fff` / `#ccc` | +| Texto secundário dark | `#8899aa` | +| Azul destaque | `#4a9edd` | +| Verde positivo | `#2ecc71` | +| Vermelho negativo | `#e74c3c` | +| Laranja atenção | `#e67e22` / `#f39c12` | +| Fundo light (wrapper) | `#f0f2f5` | +| Card/box light | `#ffffff` | +| Texto primário light | `#1e293b` / `#334155` | +| Texto secundário light | `#64748b` | + +**Nunca use cores fora desta paleta sem justificativa explícita do usuário.** + +--- + +## FASE 0 — Verificar repositório e modo de operação + +```bash +ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO" +``` + +Se `NOT_A_VITRUVIO_REPO`, pare e peça para o usuário entrar no diretório correto. + +### Se recebeu um arquivo como argumento: + +1. Leia o arquivo XML completo. +2. Identifique o que já existe: `formKey`, layouts, campos, queries SQL, funções JS. +3. Identifique o que está **faltando** em relação ao padrão (topBar com botões padrão, filterBar colapsável, detecção mobile, funções de sort/filter client-side, tema duplo, etc.). +4. Documente o delta antes de fazer perguntas ao usuário. +5. Informe o usuário o que será preservado e o que será alterado — **peça confirmação antes de reescrever qualquer lógica de negócio existente**. + +### Se não recebeu arquivo (novo painel): + +Prossiga direto para a Fase 1. + +--- + +## FASE 1 — Coleta de informações básicas + +Pergunte ao usuário (pode fazer tudo de uma vez em um bloco organizado): + +``` +Preciso de algumas informações para criar o painel. Responda o que souber — +se não souber algo agora, pode deixar em branco e adicionar depois. + +1. IDENTIFICAÇÃO + a) Nome do painel (ex: "Dashboard de Rentabilidade") + b) Chave do painel em kebab-case (ex: "dashboard-rentabilidade") + c) Descrição curta (uma linha) + d) Categoria no menu (ex: "Financeiro/Indicadores") + +2. GRUPOS DE ACESSO + Quais grupos de usuários terão acesso? (ex: "financeiro", "diretoria") + Se não souber agora, pode deixar vazio. +``` + +--- + +## FASE 2 — Coleta da fonte de dados + +**Esta é a fase mais crítica. Nunca avance sem ter as tabelas e campos reais.** + +``` +Preciso entender de onde vêm os dados. + +3. FONTE DE DADOS + a) Quais tabelas ou views serão consultadas? + (Se não tiver certeza dos nomes exatos, cole o DDL das tabelas ou + o resultado de um SELECT * LIMIT 1 para eu ver os campos) + + b) Como as tabelas se relacionam? (JOINs principais) + + c) Qual datasource usar? (padrão: vitruvio_producao) + + d) A query é PostgreSQL ou Oracle? + (Isso afeta funções de data: EXTRACT vs TRUNC, CURRENT_DATE vs SYSDATE, etc.) +``` + +> Independente da resposta em (d), o painel **sempre** detecta o banco em tempo de execução com +> `banco.isOracle()` (no `run()`) para tratar os ícones — ver seção 7.2.2. A pergunta (d) é só +> para escrever o SQL no dialeto certo. + +Se o usuário não fornecer DDL suficiente, pergunte especificamente: +> "Você pode colar o resultado de `\d nome_da_tabela` (PostgreSQL) ou `DESCRIBE nome_da_tabela` (Oracle) para eu ver os campos exatos?" + +--- + +## FASE 3 — Coleta dos filtros + +``` +4. FILTROS (sidebar lateral) + + Para cada filtro, me diga: + - Nome exibido ao usuário + - Tipo: ComboBox (lista fixa), DBComboBox (lista do banco), + DateField (data), NumericField (número), TwinColSelect (multi-seleção) + - Se for DBComboBox: qual SQL gera as opções? + - Valor padrão (se houver) + - O filtro dispara re-render automático ao mudar, + ou precisa de um botão "Pesquisar"? + + Exemplos comuns: + - Ano: DBComboBox com generate_series → auto-reload + - Mês: ComboBox fixo com 12 meses → auto-reload + - Período: DateField De/Até + botão Pesquisar + - Técnico: DBComboBox da tabela de usuários + - Unidade/Filial: DBComboBox +``` + +**Se o filtro vem de banco de dados, solicite o SQL de população antes de escrever.** + +Sempre que houver ao menos um filtro que precisa de botão "Pesquisar", esse botão — ao ser clicado — +aplica os filtros **e recolhe a filterBar ao estado inicial (fechada)**. É o comportamento padrão, +não pergunte ao usuário; ver seção 7.2.1. + +--- + +## FASE 4 — Coleta dos KPIs e gráficos + +``` +5. KPIs (cards de valor no topo do dashboard) + + Quais valores numéricos importantes devem aparecer como KPI cards? + Para cada um: + - Rótulo (ex: "Total de Contratos", "MRR", "Chamados Abertos") + - Cor do valor: azul (#4a9edd), verde (#2ecc71), vermelho (#e74c3c), + laranja (#e67e22), branco (padrão) + - Comparar com período anterior? (mostra diferença + / -) + +6. GRÁFICOS + + Tipos disponíveis (todos gerados como SVG inline, sem bibliotecas externas): + + A) Linha temporal — evolução mês a mês ou dia a dia + Preciso saber: eixo X (datas/meses), eixo Y (qual valor) + + B) Donut — proporção entre 2 ou 3 categorias + Preciso saber: quais categorias e como calcular cada fatia + + C) Barras horizontais (ranking) — lista de itens ordenados por valor + Preciso saber: o que é cada barra (item) e qual valor + + D) Barras verticais — comparação por período ou categoria + Preciso saber: eixo X (categorias), eixo Y (valor), cores + + E) Barras empilhadas — composição de totais + Preciso saber: as fatias e suas cores + + Para cada gráfico, informe: + - Qual tipo (A/B/C/D/E) + - Título do gráfico + - Disposição no desktop: linha única, ou ao lado de outro gráfico? + (ex: "Donut SLA ao lado de Barras por Tipo de Serviço") + - SQL que gera os dados (ou descreva o que calcular) +``` + +--- + +## FASE 5 — Coleta da Movimentação (tabela de detalhes) + +``` +7. MOVIMENTAÇÃO (visão tabular com linha base + drill-down) + + A) LINHA BASE (colunas visíveis na tabela principal) + Para cada coluna: + - Nome exibido (cabeçalho) + - Campo no banco / expressão + - Tipo: texto, número, data, badge colorido + - Ordenável pelo usuário? (clique no cabeçalho) + - Incluso no filtro de pesquisa global? (campo de busca) + - Largura aproximada: pequena / média / grande + + B) DRILL-DOWN (linha de detalhe expandida ao clicar) + Ao clicar em uma linha, o que aparece abaixo dela? + - Campos adicionais não visíveis na linha principal + - Sub-tabela com itens relacionados? + - SQL adicional executado no servidor OU dados já carregados no HTML? + (Para performance, prefira carregar tudo de uma vez se o volume permitir) + + C) FILTROS CLIENT-SIDE da movimentação + - Campo de pesquisa de texto livre? (em quais colunas busca?) + - Selects de filtro rápido? (ex: filtrar por Status, por Técnico) + + D) EXPORTAÇÃO CSV + - Quais colunas exportar? + - Nome do arquivo de download + + E) BADGES / CORES de linha + Alguma linha tem cor de fundo especial por status? + (ex: linha verde para "novo", vermelha para "cancelado") +``` + +--- + +## FASE 6 — Validação antes de gerar + +Após coletar todas as informações, **apresente um resumo estruturado** ao usuário: + +``` +## Resumo do painel "NOME" + +**Identificação:** key = painel-key | Categoria: X + +**Filtros (sidebar):** +- Ano: DBComboBox → auto-reload +- Mês: ComboBox fixo → auto-reload +- [outros] + +**Dashboard — KPIs:** +1. Total de X → R$ 0,00 (azul) +2. [outros] + +**Dashboard — Gráficos:** +1. Linha temporal: evolução mensal de X (largura total) +2. Donut: proporção A vs B (lado esquerdo) | Barras: ranking por cliente (lado direito) + +**Movimentação:** +Colunas: [lista] +Drill-down: [descrição] +Filtro global: busca em [colunas] +Badges: [descrição] + +**Pendências / dúvidas antes de gerar:** +- [listar qualquer ponto incerto] + +Posso prosseguir com a geração? +``` + +**Só gere código após o usuário confirmar o resumo.** + +--- + +## FASE 7 — Geração do painel + +### 7.1 — Estrutura obrigatória do XML + +Todo painel indicador neste repositório **deve** ter exatamente esta estrutura: + +``` +rootLayout (VerticalLayout, 100% x 100%) + ├── topBar (HorizontalLayout) + │ ├── btnToggleFiltros → colapsa/expande filterBar + │ ├── btnDash → muda currentView para 'dashboard' + │ ├── btnMov → muda currentView para 'movimentacao' + │ ├── Label (spacer, expandRatio=1) + │ ├── btnDevPreview → preview mobile (vi_developer, visible=false) + │ ├── btnExitPreview → sair preview (visible=false) + │ └── btnTheme → alterna tema dark/light (chama doToggleTheme) + │ + └── mainArea (HorizontalLayout, expandRatio=1) + ├── filterBar (VerticalLayout, 220px) + │ ├── btnThemeMob (visible=false — PRIMEIRO filho; aparece no mobile) + │ ├── [campos de filtro do painel] + │ ├── btnPesquisar (só se houver filtro sem auto-reload; ao clicar aplica + │ │ os filtros E recolhe a filterBar — ver seção 7.2.1) + │ └── Label (spacer, expandRatio=1) + │ + └── contentPanel (Panel, expandRatio=1) + └── panelRoot (VerticalLayout) + └── scriptDashboard (ScriptWidget) +``` + +### 7.2 — initScript (run()) + +O `run()` no `` da raiz do form **deve sempre** ter esta sequência: + +```javascript +function run() { + var dashLib = libService.loadScript('dashboard_ia'); + var login = String(engine.getLoggedUser().getLogin()); + engine.setGlobalVariable('dashLib', dashLib); + engine.setGlobalVariable('userLogin', login); + + var page = Packages.com.vaadin.ui.UI.getCurrent().getPage(); + dashLib.addSharedCss(page); + addCss(page); // CSS específico do painel + + var _topBar = engine.getLayout('topBar'); + if (_topBar) { _topBar.getRootComposition().addStyleName('dash-topbar'); } + + // ── Detecção de banco: Oracle x PostgreSQL (ver seção 7.2.2) ── + // Precisa vir cedo, antes de qualquer setCaption com ícone, porque no Oracle + // os ícones astrais (emoji) não renderizam e precisam de fallback BMP/ASCII. + var _dbIco = libService.loadScript('db'); + var _bancoIco = new _dbIco(_dbIco.VITRUVIO_DATASOURCE); + var _isOracle = false; + try { _isOracle = !!_bancoIco.isOracle(); } catch(e) {} + engine.setGlobalVariable('isOracle', _isOracle); + engine.setGlobalVariable('ico', ico); // helper de ícone (seção 7.2.2) + // aplicarIconesOracleNaTopBar() é chamado MAIS ABAIXO, depois de dashLib.applyTheme, + // senão o applyTheme reescreve o caption do btnTheme por cima. + + // ── Detecção mobile ── + if (engine.isGlobalVariableSet('_mobileRender')) { + engine.setGlobalVariable('isMobile', true); + var _rootLayout = engine.getLayout('rootLayout'); + if (_rootLayout) { + var _rootLayoutComp = _rootLayout.getRootComposition(); + // 700px é só o placeholder inicial — o server não sabe a altura da tela do cliente. + // O script abaixo troca por um valor proporcional a window.screen.height assim que a + // página carrega (ver detalhes na skill vitruvio-criar-dashboard-mobile, seção 4.1). + _rootLayoutComp.setHeight("700px"); + _rootLayoutComp.addStyleName('mobile-root-frame'); + page.getJavaScript().execute( + "(function(){" + + "var el=document.querySelector('.mobile-root-frame');" + + "if(!el){return;}" + + "var sh=window.screen.height;" + + "var byMargin=sh-220;" + + "var byRatio=Math.round(sh*0.80);" + + "var h=Math.max(320,Math.min(byMargin,byRatio));" + + "el.style.height=h+'px';" + + "})();" + ); + } + if (_topBar) { _topBar.getRootComposition().addStyleName('mobile-topbar'); } + var _filterBar = engine.getLayout('filterBar'); + if (_filterBar) { _filterBar.getRootComposition().setVisible(false); } + var _btnThemeMob = engine.getWidgetController('btnThemeMob'); + if (_btnThemeMob) { _btnThemeMob.getButton().setVisible(true); } + // Oculta btnTheme do topBar — no mobile só btnThemeMob (dentro da filterBar) é visível. + // Sem isso o usuário vê dois botões de tema ao mesmo tempo. + var _btnThemeTb = engine.getWidgetController('btnTheme'); + if (_btnThemeTb) { _btnThemeTb.getButton().setVisible(false); } + page.getStyles().add( + ".mobile-view .mov-scroll { height:520px !important; overflow-y:auto !important;" + + " -webkit-overflow-scrolling:touch !important; overscroll-behavior:contain; touch-action:pan-y; } " + ); + } + + // ── Verificar vi_developer ── + try { + var _dbLib = libService.loadScript('db'); + var _bancoChk = new _dbLib(_dbLib.VITRUVIO_DATASOURCE); + var _devRow = _bancoChk.queryRow( + "SELECT COUNT(*) AS CNT FROM nauth.usuario u " + + "INNER JOIN nauth.usuario_grupo ug ON u.usuario_id = ug.usuario_fk " + + "INNER JOIN nauth.grupo g ON g.grupo_id = ug.grupo_fk " + + "WHERE u.login = :login AND g.sigla = 'vi_developer'", + { login: login } + ); + var _isDev = _devRow && Number(_devRow.CNT) > 0; + engine.setGlobalVariable('isDeveloper', _isDev); + var _btnDevCtrl = engine.getWidgetController('btnDevPreview'); + if (_btnDevCtrl) { _btnDevCtrl.getButton().setVisible(_isDev); } + } catch(e) {} + + // ── filterBar começa fechado ── + var _filterBarInit = engine.getLayout('filterBar'); + if (_filterBarInit) { _filterBarInit.getRootComposition().setVisible(false); } + var _btnToggle = engine.getWidgetController('btnToggleFiltros'); + if (_btnToggle) { _btnToggle.getButton().setCaption(ico('filtros_abrir')); } + + // ── Tema salvo nas preferências ── + var prefs = dashLib.loadPreferences(login); + var savedTheme = prefs.dark_mode ? 'dark' : 'light'; + engine.setGlobalVariable('theme', savedTheme); + if (_filterBarInit) { + _filterBarInit.getRootComposition().addStyleName( + savedTheme === 'light' ? 'dash-filterbar-light' : 'dash-filterbar-dark' + ); + } + dashLib.applyTheme(savedTheme, page, engine.getWidgetController('btnTheme').getButton()); + + // ── Ícones Oracle: reescreve os captions estáticos do topBar (seção 7.2.2) ── + // Depois do applyTheme para o caption do btnTheme não ser sobrescrito. + if (_isOracle) { + aplicarIconesOracleNaTopBar(); + var _bTh = engine.getWidgetController('btnTheme'); + if (_bTh) { _bTh.getButton().setCaption(ico(savedTheme === 'dark' ? 'tema_escuro' : 'tema_claro')); } + } + + // ── Registra doToggleTheme como global (chamado por btnTheme e btnThemeMob) ── + engine.setGlobalVariable('doToggleTheme', doToggleTheme); + engine.setGlobalVariable('enterMobilePreview', enterMobilePreview); + engine.setGlobalVariable('exitMobilePreview', exitMobilePreview); + + // ── Registra função JS client-side de aplicar tema no HTML gerado ── + page.getJavaScript().execute( + "window.ApplyTheme=function(){" + + "var w=document.getElementById('-main');" + + "if(!w)return;" + + "if(document.body.classList.contains('theme-light-app'))w.classList.add('theme-light');" + + "else w.classList.remove('theme-light');" + + "};" + ); + + // ── Registra sort e filter client-side ── + // (adaptar as colunas conforme a movimentação do painel) + page.getJavaScript().execute( + "window.movSort=function(c,t){ /* ... padrão dos outros painéis ... */ };" + + "window.movFilter=function(){ /* ... adaptar colunas ... */ };" + + "window.movExportCSV=function(){ /* ... adaptar colunas ... */ };" + ); + + // ── Valores padrão dos filtros (se aplicável) ── + // ex: engine.getField('dtIni').setValue(...); engine.getField('nmfSla').setValue(3); +} +``` + +### 7.2.1 — Botão Pesquisar (padrão obrigatório quando há filtros sem auto-reload) + +Filtros que disparam re-render sozinhos ao mudar (ComboBox de ano/mês) **não** precisam de botão. +Mas sempre que houver filtros que só devem ser aplicados sob demanda (DateField De/Até, campos de +texto, multi-seleção), a filterBar ganha um `btnPesquisar`. + +**Comportamento obrigatório do `btnPesquisar`:** ao clicar, além de reaplicar os filtros +(`renderView()`), **a filterBar volta a ficar oculta, exatamente como estava ao abrir o painel.** +A sidebar de filtros é um overlay de trabalho — depois que o usuário escolheu o que queria, o valor +está na tela do dashboard, não na sidebar. Deixá-la aberta só rouba largura do conteúdo e obriga um +segundo clique no toggle. Fechar sozinha devolve o dashboard inteiro e deixa claro que a pesquisa +foi aplicada. + +```xml + + + + + +``` + +O caption de "fechada" tem que ser o **mesmo** que o `run()` e o `btnToggleFiltros` usam para o +estado fechado — se você mudar o glifo num lugar, mude nos três, senão o botão de toggle passa a +mentir sobre o estado. + +### 7.2.2 — Ícones e Oracle (padrão obrigatório) + +O ambiente Oracle corrompe caracteres **fora do plano BMP** — emojis e símbolos astrais +(`📱` U+1F4F1, `🤖` U+1F916, `💬` U+1F4AC, `⬇` U+2B07…) chegam à tela como `¿`, `?` ou quadrados, +porque o charset/NLS do driver não os transporta. Símbolos **BMP** (`◄` U+25C4, `►` U+25BA, +`☽` U+263D, `☀` U+2600, `✕` U+2715, `✓` U+2713) sobrevivem normalmente. + +Por isso **todo painel deve descobrir o banco uma única vez** — `banco.isOracle()` num `new db(...)` — +e, quando for Oracle, **trocar os ícones astrais por um equivalente BMP ou ASCII**. Nunca gere um +emoji direto no caption ou no HTML sem passar por esse tratamento; o painel roda igual nos dois +bancos e o mesmo XML pode ser importado num cliente Oracle e noutro PostgreSQL. + +**1 — Detecção no `run()`** — já incluída no bloco da seção 7.2 +(`engine.setGlobalVariable('isOracle', ...)`). + +**2 — Helper `ico(chave)`** — declare no `` da raiz (junto de `run`, `doToggleTheme`) e +registre como global para o ScriptWidget usar o mesmo mapa: + +```javascript +function ico(chave) { + var oracle = engine.getGlobalVariable('isOracle') == true; + // pg = glifo usado em PostgreSQL (pode ser emoji); ora = fallback seguro no Oracle + var MAP = { + 'filtros_abrir': { pg: '► Filtros', ora: '► Filtros' }, // ► BMP, ok nos dois + 'filtros_fechar': { pg: '◄ Filtros', ora: '◄ Filtros' }, // ◄ BMP, ok nos dois + 'tema_escuro': { pg: '☽', ora: '☽' }, // ☽ BMP, ok nos dois + 'tema_claro': { pg: '☀', ora: '☀' }, // ☀ BMP, ok nos dois + 'fechar': { pg: '✕', ora: 'X' }, // ✕ + 'preview_mobile': { pg: '📱', ora: '[M]' }, // 📱 astral → fallback + 'csv': { pg: '⬇ CSV', ora: 'CSV' }, // ⬇ astral → fallback + 'robo': { pg: '🤖', ora: '[IA]' }, // 🤖 astral → fallback + 'chat': { pg: '💬', ora: 'Chat' } // 💬 astral → fallback + }; + var e = MAP[chave]; + if (!e) { return ''; } + return oracle ? e.ora : e.pg; +} +``` + +Amplie o `MAP` com toda chave de ícone que o painel usar — a regra é: **símbolo BMP** pode repetir +`pg`/`ora`; **emoji/astral** precisa de um `ora` em ASCII ou BMP. + +**3 — Reescrever os captions estáticos do XML** — os `ButtonWidget` do `topBar` têm o caption +declarado no XML (não passam pelo `ico()`). Quando Oracle, corrija-os no `run()`: + +```javascript +function aplicarIconesOracleNaTopBar() { + var mapa = { + btnToggleFiltros: 'filtros_abrir', // filterBar começa fechada + btnDevPreview: 'preview_mobile', + btnExitPreview: 'fechar' + // btnTheme fica de fora: o caption dele depende do tema atual (☽/☀) e é + // ajustado logo após esta chamada, no próprio run() (ver seção 7.2). + }; + for (var id in mapa) { + if (!mapa.hasOwnProperty(id)) { continue; } + var c = engine.getWidgetController(id); + if (c) { c.getButton().setCaption(ico(mapa[id])); } + } +} +``` + +**4 — No HTML gerado pelo ScriptWidget** — sempre que injetar um ícone no HTML (toolbar da +movimentação, botão CSV, FAB do chat), busque o helper: `var ico = engine.getGlobalVariable('ico');` +e use `ico('csv')` em vez de escrever `⬇` na string. + +### 7.3 — doToggleTheme (padrão obrigatório) + +```javascript +function doToggleTheme() { + var dashLib = engine.getGlobalVariable('dashLib'); + var login = engine.getGlobalVariable('userLogin'); + var current = String(engine.getGlobalVariable('theme') || 'dark'); + var next = current === 'dark' ? 'light' : 'dark'; + engine.setGlobalVariable('theme', next); + try { + var ui = Packages.com.vaadin.ui.UI.getCurrent(); + if (dashLib && ui) { dashLib.applyTheme(next, ui.getPage(), null); } + if (dashLib && login) { dashLib.savePreferences(login, { dark_mode: next === 'dark' }); } + } catch(e) {} + var icoFn = engine.getGlobalVariable('ico'); + var capTema = next === 'dark' + ? (icoFn ? icoFn('tema_escuro') : '☽') + : (icoFn ? icoFn('tema_claro') : '☀'); + var bT = engine.getWidgetController('btnTheme'); + if (bT) { bT.getButton().setCaption(capTema); } + var bM = engine.getWidgetController('btnThemeMob'); + if (bM) { bM.getButton().setCaption(capTema); } + var _fb = engine.getLayout('filterBar'); + if (_fb) { + var fbComp = _fb.getRootComposition(); + if (next === 'light') { fbComp.removeStyleName('dash-filterbar-dark'); fbComp.addStyleName('dash-filterbar-light'); } + else { fbComp.removeStyleName('dash-filterbar-light'); fbComp.addStyleName('dash-filterbar-dark'); } + } + try { + Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute( + "var el=document.getElementById('-main');" + + "if(el){if('" + next + "'==='light')el.classList.add('theme-light');else el.classList.remove('theme-light');}" + ); + } catch(e2) {} + var fn = engine.getGlobalVariable('renderView'); + if (fn) fn(); +} +``` + +### 7.4 — ScriptWidget: estrutura interna + +O ScriptWidget `scriptDashboard` **deve sempre** ter: + +```javascript +// Carregados uma vez, acessíveis para todas as funções do script +var components = libService.loadScript('vaadinComponents'); +var db = libService.loadScript('db'); +var banco = new db(db.VITRUVIO_DATASOURCE); +var base = null; + +// Helpers de tema +function getTheme() { return String(engine.getGlobalVariable('theme') || 'dark'); } +function isLight() { return getTheme() === 'light'; } +function isMobileCtx() { return engine.isGlobalVariableSet('isMobile') || engine.isGlobalVariableSet('_mobileRender'); } + +// Helper de ícone — mesmo mapa registrado no run() (seção 7.2.2). Use SEMPRE que +// injetar um ícone no HTML gerado (toolbar da movimentação, botão CSV, FAB do chat), +// para o painel funcionar igual em Oracle e PostgreSQL. +function ico(chave) { + var fn = engine.getGlobalVariable('ico'); + return fn ? fn(chave) : ''; +} +function isOracle() { return engine.getGlobalVariable('isOracle') == true; } + +// Função de renderização obrigatória +function renderHtml(html) { /* padrão dos outros painéis */ } + +// Funções de gráfico (apenas as que o painel usa) +function svgLineChart(...) { ... } +function svgDonut(...) { ... } +function svgHBar(...) { ... } + +// View: dashboard +function renderDash() { ... } + +// View: movimentação +function renderMov() { ... } + +// Dispatcher principal — chamado por botões e filtros +function renderView() { + var view = String(engine.getGlobalVariable('currentView') || 'dashboard'); + // Atualiza estilo do botão ativo + var bD = engine.getWidgetController('btnDash'); + var bM = engine.getWidgetController('btnMov'); + if (bD) { var bd = bD.getButton(); bd.removeStyleName('dash-nav-active'); if (view === 'dashboard') bd.addStyleName('dash-nav-active'); } + if (bM) { var bm = bM.getButton(); bm.removeStyleName('dash-nav-active'); if (view === 'movimentacao') bm.addStyleName('dash-nav-active'); } + // Exibe o filtro de ordenação só na movimentação (se aplicável) + var cmbOrdem = engine.getField('cmbOrdem'); + if (cmbOrdem) { cmbOrdem.setVisible(view === 'movimentacao'); } + if (view === 'movimentacao') { renderMov(); } + else { renderDash(); } +} + +// Chama renderView no init do ScriptWidget +function init(mapa) { + base = mapa.get('base'); + engine.setGlobalVariable('renderView', renderView); + engine.setGlobalVariable('currentView', 'dashboard'); + renderView(); +} +``` + +### 7.5 — Gráficos SVG disponíveis + +Copie **apenas** os gráficos que o painel usar, adaptando cores e labels: + +- **svgLineChart**: linha temporal com área sombreada — do `dashboard-contratos` +- **svgDonut**: donut de proporção 2 fatias — do `dashboard-tecnicos` +- **svgHBar**: barras horizontais de ranking — do `dashboard-tecnicos` +- **svgBarChart** (novo): barras verticais por categoria +- **svgStackedBar** (novo): barras verticais empilhadas + +Para gráficos novos, use as mesmas convenções de cores e variáveis `isLight`, `colorGrid`, `colorLbl` etc. + +### 7.6 — Movimentação: padrões obrigatórios + +**Cabeçalho da tabela — colunas sortáveis:** +```html + + Cliente + +``` + +**Células com data-v para sort e filter:** +```html +VALOR_EXIBIDO +``` + +**Linha base + linha detalhe (par de TRs):** +```html + + ... + + + ... conteúdo do drill-down ... + +``` + +**Toolbar da movimentação:** +```javascript +html += '
'; +html += ''; +// Selects de filtro rápido (se houver): +html += ''; +html += '' + linhas.length + ' reg.'; +html += ''; +html += '
'; +``` + +> `ico('csv')` devolve `⬇ CSV` em PostgreSQL e `CSV` em Oracle (o `⬇` é astral e não renderiza no +> Oracle — ver 7.2.2). Vale para qualquer ícone que você injete no HTML da movimentação. + +### 7.7 — CSS do painel + +Sempre inclua no `addCss(page)`: + +```javascript +var addCss = function(page) { + var style = + // Base dark + ".dash-wrapper { background:#0f1923; padding:16px; font-family:Arial,Helvetica,sans-serif; box-sizing:border-box; width:100%; height:100%; overflow-y:auto; } " + + ".dash-wrapper.mov-view { overflow:hidden; display:flex; flex-direction:column; } " + + ".mov-scroll { flex:1; overflow-y:auto; min-height:0; } " + + ".mov-scroll::-webkit-scrollbar { width:6px; } " + + ".mov-scroll::-webkit-scrollbar-track { background:#0f1923; } " + + ".mov-scroll::-webkit-scrollbar-thumb { background:#243447; border-radius:3px; } " + + // KPIs + ".dash-kpi-row { display:flex; gap:16px; margin-bottom:16px; } " + + ".dash-kpi { background:#1a2733; border-radius:6px; padding:20px 24px; flex:1; min-width:180px; } " + + ".dash-kpi-label { color:#8899aa; font-size:13px; margin-bottom:6px; } " + + ".dash-kpi-value { font-size:26px; font-weight:bold; color:#fff; } " + + ".dash-kpi-sm { background:#1a2733; border-radius:6px; padding:10px 14px; flex:1; min-width:0; } " + + ".dash-kpi-sm .dash-kpi-label { color:#8899aa; font-size:12px; margin-bottom:4px; } " + + ".dash-kpi-sm .dash-kpi-value { font-size:17px; font-weight:bold; } " + + // Charts + ".dash-chart-row { display:flex; gap:16px; margin-bottom:16px; } " + + ".dash-chart-box { background:#1a2733; border-radius:6px; padding:16px; } " + + ".dash-chart-title { color:#8899aa; font-size:13px; margin-bottom:10px; } " + + // Tabela movimentação + "table.mov-table { border-collapse:collapse; width:100%; font-size:12px; font-family:Arial; } " + + "table.mov-table th { background:#243447; color:#8899aa; padding:7px 10px; text-align:left; border-bottom:1px solid #2a3a4a; white-space:nowrap; position:sticky; top:0; z-index:5; } " + + "table.mov-table th.sortable { cursor:pointer; user-select:none; } " + + "table.mov-table th.sortable:hover { background:#2a3a4a; } " + + "table.mov-table td { padding:6px 10px; color:#ccc; border-bottom:1px solid #162030; } " + + "table.mov-table tr.row-main { cursor:pointer; } " + + "table.mov-table tr.row-main:hover td { background:#1e2d3d; } " + + "table.mov-table tr.row-detail td { cursor:default; background:#0a1525; white-space:normal; } " + + "table.mov-table tr.row-detail:hover td { filter:none; } " + + // Toolbar movimentação + ".mov-toolbar { display:flex; align-items:center; gap:8px; margin-bottom:8px; flex-wrap:wrap; } " + + ".mov-filter-input { background:#1a2733; border:1px solid #243447; color:#ccc; padding:5px 10px; border-radius:4px; font-size:11px; flex:1; min-width:120px; outline:none; font-family:Arial; } " + + ".mov-filter-input:focus { border-color:#4a9edd; } " + + ".mov-export-btn { background:#1a2733; border:1px solid #243447; color:#8899aa; padding:5px 12px; border-radius:4px; font-size:11px; cursor:pointer; white-space:nowrap; font-family:Arial; } " + + ".mov-export-btn:hover { border-color:#4a9edd; color:#4a9edd; } " + + // Sidebar filterBar + ".dash-filterbar-dark { background:#0d1921 !important; border-right:1px solid #1a2a38; } " + + ".dash-filterbar-light { background:#f1f5f9 !important; border-right:1px solid #e2e8f0; } " + + ".dash-filterbar-dark .v-caption { color:#8899aa !important; font-size:12px !important; } " + + ".dash-filterbar-light .v-caption { color:#64748b !important; font-size:12px !important; } " + + // Tema light — overrides + ".theme-light.dash-wrapper { background:#f0f2f5; } " + + ".theme-light .dash-kpi { background:#ffffff; box-shadow:0 1px 3px rgba(0,0,0,.08); } " + + ".theme-light .dash-kpi-label { color:#64748b; } " + + ".theme-light .dash-kpi-value { color:#1e293b; } " + + ".theme-light .dash-chart-box { background:#ffffff; box-shadow:0 1px 3px rgba(0,0,0,.08); } " + + ".theme-light .dash-chart-title { color:#64748b; } " + + ".theme-light table.mov-table th { background:#f8fafc; color:#64748b; border-bottom:1px solid #e2e8f0; } " + + ".theme-light table.mov-table td { color:#334155; border-bottom:1px solid #e2e8f0; } " + + ".theme-light table.mov-table tr.row-main:hover td { background:#f1f5f9; } " + + ".theme-light table.mov-table tr.row-detail td { background:#f8fafc; } " + + ".theme-light .mov-filter-input { background:#ffffff; border-color:#e2e8f0; color:#334155; } " + + ".theme-light .mov-export-btn { background:#ffffff; border-color:#e2e8f0; color:#64748b; } " + + ".theme-light .mov-export-btn:hover { border-color:#2563eb; color:#2563eb; } " + + // Preview mobile frame + ".preview-mobile-frame { margin:0 auto !important; border:2px solid #4a9edd !important; border-radius:12px !important; overflow:hidden !important; box-shadow:0 0 40px rgba(74,158,221,0.25) !important; } " + + ".preview-exit-btn { position:fixed !important; top:10px !important; right:12px !important; z-index:99999 !important; background:#e74c3c !important; border:none !important; border-radius:6px !important; padding:7px 16px !important; font-weight:bold !important; box-shadow:0 2px 12px rgba(0,0,0,0.4) !important; cursor:pointer !important; } " + + ".preview-exit-btn .v-button-caption { color:#fff !important; font-size:12px !important; } " + + // Mobile overrides + ".mobile-view.dash-wrapper { height:auto !important; overflow:visible !important; padding-bottom:24px; } " + + ".mobile-view .dash-kpi-row { flex-direction:column; gap:8px; } " + + ".mobile-view .dash-chart-row { flex-direction:column; } " + + ".mobile-view .dash-kpi { min-width:unset; width:100%; box-sizing:border-box; } " + + ".mobile-view .dash-kpi-sm { min-width:unset; width:100%; box-sizing:border-box; } " + + // Mobile — fontes maiores (tela menor exige texto mais legível) + ".mobile-view .dash-kpi-label { font-size:15px !important; } " + + ".mobile-view .dash-kpi-sm .dash-kpi-label { font-size:13px !important; } " + + ".mobile-view .dash-kpi-value { font-size:22px !important; } " + + ".mobile-view .dash-kpi-sm .dash-kpi-value { font-size:16px !important; } " + + ".mobile-view .dash-chart-title { font-size:14px !important; } " + + ".mobile-view .mov-filter-input { font-size:13px !important; } " + + ".mobile-view .mov-export-btn { font-size:13px !important; } " + + ".mobile-view table.mov-table td { font-size:13px !important; } " + + ".mobile-view table.mov-table th { font-size:13px !important; } " + + // [adicionar aqui CSS mobile específico do painel] + ""; + page.getStyles().add(style); +}; +``` + +### 7.8 — enterMobilePreview / exitMobilePreview + +**Padrão real (confirmado em `dashboard-contrato-desktop.xml`):** o preview de desenvolvedor **não** usa uma altura fixa tipo `700px`/`3000px` como o `_mobileRender` real (seção 4.1 da skill **vitruvio-criar-dashboard-mobile**) — usa altura natural. `base.setHeight(null)` faz o `ScriptWidget` crescer para o tamanho do conteúdo, o `Panel` (`contentPanel`) enxerga o overflow real e rola sozinho. Isso é mais simples que qualquer técnica de "colapso via JS" e funciona porque o preview roda inteiramente dentro do navegador desktop — não há altura de tela de celular real para calcular. + +Em `enterMobilePreview`: +```javascript +function enterMobilePreview() { + engine.setGlobalVariable('isMobile', true); + // base com height null → cresce ao tamanho do conteúdo → Panel enxerga overflow e rola + try { base.setHeight(null); } catch(e) {} + var _rl = engine.getLayout('rootLayout'); + if (_rl) { + var _rlComp = _rl.getRootComposition(); + _rlComp.setWidth("375px"); + _rlComp.addStyleName('preview-mobile-frame'); + } + // ... (filterBar, botões de tema, btnDevPreview, btnExitPreview) + renderView(); +} +``` + +Em `exitMobilePreview`: +```javascript +function exitMobilePreview() { + engine.unsetGlobalVariable('isMobile'); + // Restaura base para height 100% (modo desktop normal) + try { base.setHeight("100%"); } catch(e) {} + var _rl = engine.getLayout('rootLayout'); + if (_rl) { + var _rlComp = _rl.getRootComposition(); + _rlComp.setWidth("100%"); + _rlComp.removeStyleName('preview-mobile-frame'); + } + // ... (filterBar, botões de tema, btnDevPreview) + renderView(); +} +``` + +**Controle de botões de tema — obrigatório em ambas as funções:** + +Em `enterMobilePreview`, antes de `renderView()`: +```javascript +var _btnThMob = engine.getWidgetController('btnThemeMob'); +if (_btnThMob) _btnThMob.getButton().setVisible(true); +var _btnThTb = engine.getWidgetController('btnTheme'); +if (_btnThTb) _btnThTb.getButton().setVisible(false); +``` + +Em `exitMobilePreview`, antes de `renderView()`: +```javascript +var _btnThMob = engine.getWidgetController('btnThemeMob'); +if (_btnThMob) _btnThMob.getButton().setVisible(false); +var _btnThTbExit = engine.getWidgetController('btnTheme'); +if (_btnThTbExit) _btnThTbExit.getButton().setVisible(true); +``` + +**`renderHtml` distingue preview de mobile real / desktop** — só o preview usa altura natural; mobile real (`_mobileRender`) e desktop continuam `setSizeFull()`: +```javascript +function renderHtml(html) { + var comp = components.html(html); + // isPreviewMode: preview de dev (isMobile=true SEM _mobileRender). Mobile WebView real + // (_mobileRender=true) e desktop normal continuam usando setSizeFull() — só o preview + // precisa de altura natural para o Panel rolar corretamente dentro do navegador do dev. + var isPreviewMode = engine.isGlobalVariableSet('isMobile') && !engine.isGlobalVariableSet('_mobileRender'); + if (isPreviewMode) { + try { base.setHeight(null); } catch(e) {} + comp.setWidth("100%"); + } else { + comp.setSizeFull(); + } + base.removeAllComponents(); + base.addComponent(comp); + if (!isPreviewMode) { base.setExpandRatio(comp, 1.0); } + // JS após o render: aplica tema e, apenas dentro do frame de preview + // (.mobile-root-frame — NUNCA redimensionado, sua altura já foi fixada uma única vez pelo + // bloco _mobileRender do run() via window.screen.height), libera overflow:visible nos pais + // intermediários para o scroll externo funcionar sem "arrastar" a topBar junto. + Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute( + "setTimeout(function(){" + + "if(typeof meuPainelApplyTheme==='function')meuPainelApplyTheme();" + + "var el=document.getElementById('meu-painel-main');" + + "if(el){" + + "var p=el,n=0;" + + "while(p&&n<30){p=p.parentElement;n++;" + + "if(!p){break;}" + + "if(p.classList&&p.classList.contains('mobile-root-frame')){break;}" + + "var cs=window.getComputedStyle(p);" + + "if(cs&&(cs.overflowY==='auto'||cs.overflowY==='scroll'||cs.overflowY==='hidden')){p.style.overflowY='visible';}" + + "if(cs&&(cs.overflow==='auto'||cs.overflow==='scroll'||cs.overflow==='hidden')){p.style.overflow='visible';}" + + "}" + + "}" + + "},400);" + ); +} +``` + +> Nota: as funções que geram HTML (`desenharDashboard`/`renderDash` etc.) já checam +> `isMobileCtx()`/`mobileClass` com `isGlobalVariableSet('isMobile') || isGlobalVariableSet('_mobileRender')` +> — o `dashboard-contrato-desktop.xml` real adiciona uma terceira condição, `_mobPreviewMode`, que +> nenhuma das duas funções acima define (provavelmente um hook para um mecanismo externo). Inclua-a +> por segurança/compatibilidade, mas não é necessário defini-la para o preview de dev funcionar: +> `engine.isGlobalVariableSet('isMobile') || engine.isGlobalVariableSet('_mobileRender') || engine.isGlobalVariableSet('_mobPreviewMode')`. + +### 7.10 — Padrões obrigatórios para Mobile + +#### Botão de tema (claro/escuro) — posição no mobile + +No mobile, o `btnThemeMob` **deve sempre aparecer junto à barra de filtros**, no topo da filterBar — antes dos campos de filtro. Nunca posicionar o botão de tema isolado no rodapé da sidebar no mobile. + +Estrutura correta da filterBar no mobile: +``` +filterBar (VerticalLayout) + ├── btnThemeMob ← PRIMEIRO, antes dos filtros + ├── [campos de filtro] + ├── [botão Pesquisar, se houver] + └── Label spacer (expandRatio=1) +``` + +No form XML, posicione `btnThemeMob` como primeiro filho da filterBar. No `run()`, ao detectar mobile, torne-o visível: +```javascript +var _btnThemeMob = engine.getWidgetController('btnThemeMob'); +if (_btnThemeMob) { _btnThemeMob.getButton().setVisible(true); } +``` + +#### Cards (KPIs e blocos) — layout vertical obrigatório no mobile + +No mobile, todos os cards devem ser empilhados verticalmente, um embaixo do outro, ocupando 100% da largura: + +- **Nunca** use `flex-direction: row` para cards no mobile +- Cada card ocupa `width: 100%; box-sizing: border-box` +- Se houver **3 cards de mesmo contexto** (ex: 3 KPIs do mesmo grupo temático), renderize-os juntos em sequência vertical — eles preenchem o espaço de forma coesa +- Use `isMobileCtx()` para condicionar o HTML gerado: + +```javascript +if (isMobileCtx()) { + // Cards empilhados, 100% largura + html += '
'; + html += '
...
'; + html += '
...
'; + html += '
...
'; + html += '
'; +} else { + // Desktop: linha horizontal + html += '
'; + html += '
...
'; + html += '
'; +} +``` + +#### Fontes maiores no mobile + +Gere os tamanhos de fonte condicionalmente com `isMobileCtx()` sempre que o tamanho estiver em `style=""` inline — o CSS da classe `.mobile-view` já cobre os casos de classe: + +```javascript +var fsLabel = isMobileCtx() ? '15px' : '13px'; +var fsValue = isMobileCtx() ? '22px' : '26px'; +html += '
' + label + '
'; +html += '
' + value + '
'; +``` + +| Elemento | Desktop | Mobile | +|---|---|---| +| Label de KPI | 13px | 15px | +| Valor de KPI | 26px | 22px | +| KPI sm label | 12px | 13px | +| KPI sm valor | 17px | 16px | +| Título de gráfico | 13px | 14px | +| Texto de tabela | 12px | 13px | +| Toolbar (input/btn) | 11px | 13px | + +#### Listas e gráficos — alturas fixas e scroll obrigatórios no mobile + +**Regra crítica:** no mobile, toda lista de cards e todo gráfico de barras com N variável de itens **deve ter altura fixa e `overflow-y:auto`**. Sem isso, o conteúdo cresce infinitamente e o frame não rola corretamente. + +**O CSS DEVE estar no `addCss()`** — não apenas no bloco `_mobileRender`. Se ficar só em `_mobileRender`, o preview mobile do desenvolvedor não funciona (porque `isMobile=true` mas `_mobileRender` não está setado). + +```javascript +// ✅ CERTO — em addCss(), funciona em preview E mobile real +var addCss = function(page) { + var style = + // ...outros estilos... + ".mobile-view.dash-wrapper { height:auto !important; overflow:visible !important; padding-bottom:24px; } " + + ".mobile-view .mov-scroll { max-height:520px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; overscroll-behavior:contain; touch-action:pan-y; } " + + ".mobile-view .tec-mob-scroll { max-height:420px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; overscroll-behavior:contain; touch-action:pan-y; } " + + ".mobile-view .hbar-scroll { max-height:280px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; } " + + // ... +``` + +**Classes de scroll padrão:** + +| Classe | Altura | Uso | +|---|---|---| +| `.mov-scroll` | max 520px | Container da lista de movimentação (cresce com o conteúdo, scroll acima de 520px) | +| `.tec-mob-scroll` | max 420px | Container de lista de técnicos/ranking (cresce com o conteúdo, scroll acima de 420px) | +| `.hbar-scroll` | max 280px | Container de gráfico de barras horizontais (svgHBar) | + +**Padrão de uso nas funções de render mobile:** + +```javascript +// ❌ ERRADO — sem scroll container, conteúdo cresce infinito +if (mobileClass) { + html += '
'; + for (var i = 0; i < itens.length; i++) { + html += '
...
'; + } + html += '
'; +} + +// ✅ CERTO — sempre com scroll container de altura fixa +if (mobileClass) { + // Barra de pesquisa FORA do scroll (fica fixo no topo) + html += ''; + // Lista DENTRO do scroll + html += '
'; + for (var i = 0; i < itens.length; i++) { + html += '
...
'; + } + if (itens.length === 0) html += '
Sem dados
'; + html += '
'; +} +``` + +Para lista de técnicos/ranking com botões de ordenação: + +```javascript +// O header com botões de sort fica FORA do scroll (não some ao rolar) +html += '
'; +html += '
'; +html += 'Técnicos'; +html += '
'; +html += '
'; +// A lista fica DENTRO do scroll +html += '
'; +html += '
'; +for (var i = 0; i < tecArr.length; i++) { + html += '
...
'; +} +html += '
'; +html += '
'; +``` + +Para gráficos de barras horizontais (svgHBar): + +```javascript +// ❌ ERRADO — gráfico sem limitação de altura no mobile +html += '
'; +html += svgHBar(dados, '#4a9edd'); +html += '
'; + +// ✅ CERTO — gráfico dentro de container com max-height + scroll +html += '
'; +html += '
Título
'; +html += '
' + svgHBar(dados, '#4a9edd') + '
'; +html += '
'; +``` + +#### Lib e tema — sempre carregados ao entrar na tela + +**Obrigatório em todo painel:** + +1. Sempre carregar `dashLib = libService.loadScript('dashboard_ia')` no `run()` +2. Sempre aplicar o tema salvo nas preferências antes de qualquer render: `dashLib.loadPreferences(login)` determina o tema inicial — nunca assume `dark` por padrão sem verificar +3. O tema deve ser aplicado em dois lugares: + - Vaadin (server-side): `dashLib.applyTheme(savedTheme, page, btnTheme)` + - HTML gerado (client-side): via `window.ApplyTheme()` registrado na página + +```javascript +// CORRETO — sempre verificar preferência salva +var prefs = dashLib.loadPreferences(login); +var savedTheme = prefs.dark_mode ? 'dark' : 'light'; // nunca assuma 'dark' sem verificar +engine.setGlobalVariable('theme', savedTheme); +dashLib.applyTheme(savedTheme, page, engine.getWidgetController('btnTheme').getButton()); +``` + +--- + +### 7.11 — (Opcional) Assistente de IA "Pietro" (chatBar) + +**Só inclua este bloco se o usuário pedir explicitamente um assistente de IA/chat no dashboard.** +Não é parte obrigatória do padrão de indicador — é um add-on visto em `dashboard-contrato-desktop.xml` +e `dashboard-tecnicos-desktop.xml` (ambos os painéis de referência o incluem, mas o padrão +"indicador dashboard" em si — KPIs, gráficos, movimentação — funciona plenamente sem ele). + +**Dependência externa:** o chat roda sobre um script `IA_chat_agente_ia` que **não** é criado por +esta skill — precisa já existir no repositório (é uma lib de chat genérica, reutilizável por +qualquer painel). Se o usuário pedir o Pietro mas esse script não existir no repo, avise antes de +prosseguir. + +**Peças do padrão** (nomes fixos — mantenha-os, o script `IA_chat_agente_ia` espera exatamente isso): + +1. **Botão no topBar** — `btnToggleChat`, abre/fecha a sidebar `chatBar`: +```xml + + + + + +``` + +2. **Sidebar `chatBar`** (irmã de `mainArea`, dentro de `rootLayout`, 320px, oculta por padrão) — + header com título + botão fechar, área de mensagens rolável, barra de input: +```xml + + + + + + + + + + + + + + + + + +