@bluemarble/bi 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\n * quem grava é o CLI (`bm-bi new`).\n */\n\nexport class ScaffoldError extends Error {}\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\nconst LAYOUT_ID = /^[\\w-]+$/\n\nexport function checkModelName(name: string) {\n if (!IDENTIFIER.test(name))\n throw new ScaffoldError(\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\n )\n}\n\nexport function checkLayoutId(id: string) {\n if (!LAYOUT_ID.test(id))\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\n}\n\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\nexport function modelFileCode(name: string) {\n checkModelName(name)\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\n 'export const',\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\n )\n}\n\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\nexport function layoutFileCode(id: string) {\n checkLayoutId(id)\n const layout: ChartsLayout = { id, filters: [], charts: [] }\n return `${JSON.stringify(layout, null, 2)}\\n`\n}\n\n/** `models/index.ts` novo, com só este modelo. */\nexport function modelIndexCode(name: string) {\n checkModelName(name)\n return [\n `import { ${name} } from './${name}'`,\n '',\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\n 'export const dashboardModels = {',\n ` ${name}`,\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\n */\nexport function registerModel(source: string, name: string): string | null {\n checkModelName(name)\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\n if (importPath.test(source)) return null\n\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\n if (!object)\n throw new ScaffoldError(\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\n )\n const open = object.index + object[0].length - 1\n let depth = 0\n let close = -1\n for (let i = open; i < source.length; i++) {\n if (source[i] === '{') depth++\n else if (source[i] === '}' && --depth === 0) {\n close = i\n break\n }\n }\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\n\n const body = source.slice(open + 1, close)\n const items = body.trimEnd()\n let newBody: string\n if (!items.trim()) newBody = `\\n ${name}\\n`\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\n\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\n const line = `import { ${name} } from './${name}'\\n`\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\n const last = imports.at(-1)\n const at = last ? last.index + last[0].length : 0\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\n return result\n}\n\n// --- página ---\n\n/** `analiseCusto` → `analise-custo` (rota da página). */\nexport const kebabCase = (text: string) =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase()\n\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\nexport const titleCase = (text: string) =>\n kebabCase(text)\n .split('-')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\n\nexport type PageOptions = {\n model: string\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\n layoutImport: string\n title: string\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\n endpoint: string\n /** App Router do Next: a página vira client component (`'use client'`). */\n clientDirective?: boolean\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\n chartsImport?: string\n}\n\n/**\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\n * dashboard com esses marcadores no lugar do que muda de um para outro.\n */\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\n __MODEL__: options.model,\n __LAYOUT__: options.layoutImport,\n __TITLE__: options.title,\n __COMPONENT__: pageComponentName(options.model),\n __ENDPOINT__: options.endpoint\n})\n\nexport function fillPageTemplate(template: string, options: PageOptions) {\n let content = template\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\nexport function pageFileCode(options: PageOptions) {\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\n checkModelName(model)\n return [\n ...(clientDirective ? [\"'use client'\", ''] : []),\n 'import {',\n ' DashboardCanvas,',\n ' DashboardCharts,',\n ' type DashboardFetcher,',\n ' DashboardFilters,',\n ' DashboardProvider',\n \"} from '@bluemarble/bi/client'\",\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\n \"import { Box, Typography } from '@mui/material'\",\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\n `import layout from '${layoutImport}'`,\n '',\n '/**',\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\n ' */',\n '',\n `const TITULO = ${literal(title)}`,\n 'const chartsLayout = layout as ChartsLayout',\n '',\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\n 'const fetcher: DashboardFetcher = async (query) => {',\n ` const response = await fetch('${endpoint}', {`,\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify(query)',\n ' })',\n ' if (!response.ok) throw new Error(await response.text())',\n ' return (await response.json()) as DashboardResult',\n '}',\n '',\n `export default function ${pageComponentName(model)}() {`,\n ' return (',\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\n ' <Box sx={{ p: 2 }}>',\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\n ' <DashboardCanvas layout={chartsLayout}>',\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\n ' <Typography component=\"h1\" variant=\"h5\">',\n ' {TITULO}',\n ' </Typography>',\n ' <DashboardFilters layout={chartsLayout} />',\n ' <DashboardCharts layout={chartsLayout} />',\n ' </Box>',\n ' </DashboardCanvas>',\n ' </Box>',\n ' </DashboardProvider>',\n ' )',\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\n * `custo`). Sem a opção (ou sem config), `tsx`.\n */\nexport function pageExtension(nextConfig: string | undefined) {\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\n const extensions = [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1])\n return extensions.find((extension) => extension?.endsWith('tsx')) ?? 'tsx'\n}\n","#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative, sep } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport {\n checkLayoutId,\n checkModelName,\n fillPageTemplate,\n kebabCase,\n layoutFileCode,\n modelFileCode,\n modelIndexCode,\n type PageOptions,\n pageExtension,\n pageFileCode,\n registerModel,\n ScaffoldError,\n titleCase\n} from './scaffold'\n\nconst HELP = `Uso: bm-bi new <nome> [opções]\n\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\n(layouts/<id>.json) e a página. Depois é montar tudo pelas abas Modelo e Layout do devtools.\n\nA página vai para o router do Next que existir: src/pages (ou pages) →\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\n\nOpções:\n --id <id> id do layout (padrão: o nome do modelo)\n --title <texto> título da página (padrão: o id em palavras)\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\n --skip-page não cria a página\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\n --endpoint <url> endpoint do runQuery na página padrão (padrão: /api/dashboard/query)\n -h, --help esta ajuda\n`\n\nconst exists = (path: string) => existsSync(path)\n\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\nfunction importPath(from: string, to: string) {\n const path = relative(dirname(from), to).split(sep).join('/')\n return path.startsWith('.') ? path : `./${path}`\n}\n\n/** Página pelo router do Next que existir no app (Pages primeiro, como o `custo`). */\nfunction defaultPagePath(cwd: string, slug: string) {\n for (const pages of ['src/pages', 'pages']) {\n if (!exists(join(cwd, pages))) continue\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\n .map((file) => join(cwd, file))\n .find(exists)\n const extension = pageExtension(config && readFileSync(config, 'utf8'))\n return join(cwd, pages, 'dashboard', slug, `index.${extension}`)\n }\n for (const app of ['src/app', 'app'])\n if (exists(join(cwd, app))) return join(cwd, app, 'dashboard', slug, 'page.tsx')\n return undefined\n}\n\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\nfunction chartsIndex(dir: string) {\n return ['index.ts', 'index.tsx']\n .map((file) => join(dir, 'charts', file))\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\n}\n\nfunction main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n id: { type: 'string' },\n dir: { type: 'string', default: 'src/dashboard' },\n models: { type: 'string' },\n layouts: { type: 'string' },\n title: { type: 'string' },\n page: { type: 'string' },\n 'skip-page': { type: 'boolean' },\n template: { type: 'string' },\n endpoint: { type: 'string', default: '/api/dashboard/query' },\n help: { type: 'boolean', short: 'h' }\n }\n })\n const [command, name, ...rest] = positionals\n if (values.help || !command) {\n process.stdout.write(HELP)\n return\n }\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\n\n const id = values.id ?? name\n checkModelName(name)\n checkLayoutId(id)\n\n const cwd = process.cwd()\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\n const modelPath = join(modelsDir, `${name}.ts`)\n const indexPath = join(modelsDir, 'index.ts')\n const layoutPath = join(layoutsDir, `${id}.json`)\n const dashboardDir = join(cwd, values.dir)\n const pagePath = values['skip-page']\n ? undefined\n : values.page\n ? join(cwd, values.page)\n : defaultPagePath(cwd, kebabCase(id))\n const templatePath = values.template\n ? join(cwd, values.template)\n : [join(dashboardDir, 'page.template')].find(exists)\n if (templatePath && !exists(templatePath))\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, templatePath)}`)\n\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\n for (const path of [modelPath, layoutPath, pagePath])\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\n const index = existsSync(indexPath)\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\n : modelIndexCode(name)\n let page: string | undefined\n if (pagePath) {\n const charts = chartsIndex(dashboardDir)\n const options: PageOptions = {\n model: name,\n layoutImport: importPath(pagePath, layoutPath),\n title: values.title ?? titleCase(id),\n endpoint: values.endpoint,\n // App Router: a página usa hooks, então é client component\n clientDirective:\n basename(pagePath).startsWith('page.') &&\n relative(cwd, pagePath).split(sep).includes('app'),\n chartsImport: charts && importPath(pagePath, dirname(charts))\n }\n page = templatePath\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\n : pageFileCode(options)\n }\n\n const written: string[] = []\n const write = (path: string, content: string) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n written.push(relative(cwd, path))\n }\n write(modelPath, modelFileCode(name))\n if (index !== null) write(indexPath, index)\n write(layoutPath, layoutFileCode(id))\n if (pagePath && page !== undefined) write(pagePath, page)\n\n process.stdout.write(\n [\n ...written.map((path) => `✓ ${path}`),\n ...(pagePath\n ? templatePath\n ? [` (página do template ${relative(cwd, templatePath)})`]\n : []\n : values['skip-page']\n ? []\n : [\n '',\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\n ' <DashboardFilters layout={layout} />',\n ' <DashboardCharts layout={layout} />',\n ' </DashboardProvider>'\n ]),\n ''\n ].join('\\n')\n )\n}\n\ntry {\n main(process.argv.slice(2))\n} catch (error) {\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\n process.stderr.write(`${error.message}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\n * quem grava é o CLI (`bm-bi new`).\n */\n\nexport class ScaffoldError extends Error {}\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\nconst LAYOUT_ID = /^[\\w-]+$/\n\nexport function checkModelName(name: string) {\n if (!IDENTIFIER.test(name))\n throw new ScaffoldError(\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\n )\n}\n\nexport function checkLayoutId(id: string) {\n if (!LAYOUT_ID.test(id))\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\n}\n\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\nexport function modelFileCode(name: string) {\n checkModelName(name)\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\n 'export const',\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\n )\n}\n\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\nexport function layoutFileCode(id: string) {\n checkLayoutId(id)\n const layout: ChartsLayout = { id, filters: [], charts: [] }\n return `${JSON.stringify(layout, null, 2)}\\n`\n}\n\n/** `models/index.ts` novo, com só este modelo. */\nexport function modelIndexCode(name: string) {\n checkModelName(name)\n return [\n `import { ${name} } from './${name}'`,\n '',\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\n 'export const dashboardModels = {',\n ` ${name}`,\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\n */\nexport function registerModel(source: string, name: string): string | null {\n checkModelName(name)\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\n if (importPath.test(source)) return null\n\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\n if (!object)\n throw new ScaffoldError(\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\n )\n const open = object.index + object[0].length - 1\n let depth = 0\n let close = -1\n for (let i = open; i < source.length; i++) {\n if (source[i] === '{') depth++\n else if (source[i] === '}' && --depth === 0) {\n close = i\n break\n }\n }\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\n\n const body = source.slice(open + 1, close)\n const items = body.trimEnd()\n let newBody: string\n if (!items.trim()) newBody = `\\n ${name}\\n`\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\n\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\n const line = `import { ${name} } from './${name}'\\n`\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\n const last = imports.at(-1)\n const at = last ? last.index + last[0].length : 0\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\n return result\n}\n\n// --- página ---\n\n/** `analiseCusto` → `analise-custo` (rota da página). */\nexport const kebabCase = (text: string) =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase()\n\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\nexport const titleCase = (text: string) =>\n kebabCase(text)\n .split('-')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\n\nexport type PageOptions = {\n model: string\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\n layoutImport: string\n title: string\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\n endpoint: string\n /** App Router do Next: a página vira client component (`'use client'`). */\n clientDirective?: boolean\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\n chartsImport?: string\n}\n\n/**\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\n * dashboard com esses marcadores no lugar do que muda de um para outro.\n */\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\n __MODEL__: options.model,\n __LAYOUT__: options.layoutImport,\n __TITLE__: options.title,\n __COMPONENT__: pageComponentName(options.model),\n __ENDPOINT__: options.endpoint\n})\n\nexport function fillPageTemplate(template: string, options: PageOptions) {\n let content = template\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\nexport function pageFileCode(options: PageOptions) {\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\n checkModelName(model)\n return [\n ...(clientDirective ? [\"'use client'\", ''] : []),\n 'import {',\n ' DashboardCanvas,',\n ' DashboardCharts,',\n ' type DashboardFetcher,',\n ' DashboardFilters,',\n ' DashboardProvider',\n \"} from '@bluemarble/bi/client'\",\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\n \"import { Box, Typography } from '@mui/material'\",\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\n `import layout from '${layoutImport}'`,\n '',\n '/**',\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\n ' */',\n '',\n `const TITULO = ${literal(title)}`,\n 'const chartsLayout = layout as ChartsLayout',\n '',\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\n 'const fetcher: DashboardFetcher = async (query) => {',\n ` const response = await fetch('${endpoint}', {`,\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify(query)',\n ' })',\n ' if (!response.ok) throw new Error(await response.text())',\n ' return (await response.json()) as DashboardResult',\n '}',\n '',\n `export default function ${pageComponentName(model)}() {`,\n ' return (',\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\n ' <Box sx={{ p: 2 }}>',\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\n ' <DashboardCanvas layout={chartsLayout}>',\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\n ' <Typography component=\"h1\" variant=\"h5\">',\n ' {TITULO}',\n ' </Typography>',\n ' <DashboardFilters layout={chartsLayout} />',\n ' <DashboardCharts layout={chartsLayout} />',\n ' </Box>',\n ' </DashboardCanvas>',\n ' </Box>',\n ' </DashboardProvider>',\n ' )',\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\n * `custo`). Sem a opção (ou sem config), `tsx`.\n */\nexport function pageExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('tsx')) ?? 'tsx'\n}\n\n/** Itens do `pageExtensions` do `next.config` (lido como texto). */\nfunction pageExtensions(nextConfig: string | undefined) {\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\n return [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1] as string)\n}\n\n// --- endpoint ---\n\nexport type ApiOptions = {\n /** Import do `models/index.ts` (o `dashboardModels`), relativo ao endpoint. */\n modelsImport: string\n /** Pastas do devtools, relativas à raiz do app (`process.cwd()` no servidor). */\n modelsDir: string\n layoutsDir: string\n snapshotsDir: string\n /** Import do registro de tipos de gráfico do servidor (`export const chartTypes`). */\n chartTypesImport?: string\n /** App Router do Next: `route.ts` com `POST(request)` em vez do handler do Pages Router. */\n appRouter?: boolean\n}\n\n/** Valores trocados num template de endpoint do app (`api.template`). */\nexport const apiTemplateValues = (options: ApiOptions): Record<string, string> => ({\n __MODELS__: options.modelsImport,\n __MODELS_DIR__: options.modelsDir,\n __LAYOUTS_DIR__: options.layoutsDir,\n __SNAPSHOTS_DIR__: options.snapshotsDir\n})\n\nexport function fillApiTemplate(template: string, options: ApiOptions) {\n let content = template\n for (const [marker, value] of Object.entries(apiTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** `src/dashboard/models` → `'src', 'dashboard', 'models'` (argumentos do `path.join`). */\nconst pathArgs = (dir: string) =>\n dir\n .split(/[\\\\/]/)\n .filter(Boolean)\n .map((part) => literal(part))\n .join(', ')\n\n/**\n * Endpoint padrão do `runQuery` com Prisma: um só para todos os dashboards (só aceita os modelos do\n * `models/index.ts`), com o devtools ligado só em desenvolvimento.\n */\nexport function apiFileCode(options: ApiOptions) {\n const { modelsImport, chartTypesImport, appRouter } = options\n const dir = (path: string) => `path.join(process.cwd(), ${pathArgs(path)})`\n const run = (body: string, indent: string) =>\n [\n 'const result = await runQuery(',\n ` dashboardModels,`,\n ` ${body} as DashboardQuery,`,\n ' (sql, params) => prisma.$queryRawUnsafe<unknown[]>(sql, ...params),',\n ' { cacheTtlMs: CACHE_TTL_MS, devtools: DEVTOOLS }',\n ')'\n ].map((line) => `${indent}${line}`)\n return [\n \"import path from 'node:path'\",\n \"import { DashboardQueryError, runQuery } from '@bluemarble/bi/server'\",\n \"import type { DashboardQuery } from '@bluemarble/bi/types'\",\n \"import { PrismaClient } from '@prisma/client'\",\n ...(appRouter ? [] : [\"import type { NextApiRequest, NextApiResponse } from 'next'\"]),\n ...(chartTypesImport ? [`import { chartTypes } from '${chartTypesImport}'`] : []),\n `import { dashboardModels } from '${modelsImport}'`,\n '',\n '// Client próprio: rotas que chamam `prisma.$disconnect()` no client global derrubariam',\n '// consultas longas em andamento (\"Engine is not yet connected\").',\n 'const globalForPrisma = globalThis as unknown as { dashboardPrisma?: PrismaClient }',\n 'const prisma = globalForPrisma.dashboardPrisma ?? new PrismaClient()',\n \"if (process.env.NODE_ENV === 'development') globalForPrisma.dashboardPrisma = prisma\",\n '',\n 'const CACHE_TTL_MS = 5 * 60 * 1000',\n '',\n '// Devtools (debug, rascunho de modelo, salvar modelo/layout) aceita SQL vindo do browser: só em dev',\n \"const DEVTOOLS = process.env.NODE_ENV === 'development' && {\",\n ` modelsDir: ${dir(options.modelsDir)},`,\n ` snapshotsDir: ${dir(options.snapshotsDir)},`,\n ` layoutsDir: ${dir(options.layoutsDir)}${chartTypesImport ? ',' : ''}`,\n ...(chartTypesImport ? [' chartTypes'] : []),\n '}',\n '',\n '/**',\n ' * Endpoint genérico do `@bluemarble/bi`: recebe `{ model, dimensions, measures, filters }` e',\n ' * devolve as linhas agregadas. Só aceita nomes definidos nos modelos registrados.',\n ' */',\n ...(appRouter\n ? [\n 'export async function POST(request: Request) {',\n ' try {',\n ...run('(await request.json())', ' '),\n ' return Response.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return Response.json({ error: error.message }, { status: 400 })',\n ' throw error',\n ' }',\n '}'\n ]\n : [\n 'export default async function handler(req: NextApiRequest, res: NextApiResponse) {',\n \" if (req.method !== 'POST') return res.status(405).setHeader('Allow', 'POST').end()\",\n ' try {',\n ...run('req.body', ' '),\n ' return res.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return res.status(400).json({ error: error.message })',\n ' throw error',\n ' }',\n '}'\n ]),\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das rotas de API do Pages Router pelo `pageExtensions` do `next.config` (ex.: `api.ts`\n * no `custo`). Sem a opção (ou sem config), `ts`.\n */\nexport function apiExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('ts')) ?? 'ts'\n}\n","#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative, sep } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport {\n type ApiOptions,\n apiExtension,\n apiFileCode,\n checkLayoutId,\n checkModelName,\n fillApiTemplate,\n fillPageTemplate,\n kebabCase,\n layoutFileCode,\n modelFileCode,\n modelIndexCode,\n type PageOptions,\n pageExtension,\n pageFileCode,\n registerModel,\n ScaffoldError,\n titleCase\n} from './scaffold'\n\nconst HELP = `Uso: bm-bi new <nome> [opções]\n\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\n(layouts/<id>.json), a página e, se ainda não existir, o endpoint do runQuery (um só para todos os\ndashboards). Depois é montar tudo pelas abas Modelo e Layout do devtools.\n\nA página vai para o router do Next que existir: src/pages (ou pages) →\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\n\nO endpoint vai para a rota do --endpoint no mesmo router: src/pages → api/dashboard/query.<ext> (ext\nde API do pageExtensions); src/app → api/dashboard/query/route.ts. O padrão usa Prisma\n($queryRawUnsafe); com <dir>/api.template, sai dele, trocando __MODELS__ (import do models/index),\n__MODELS_DIR__, __LAYOUTS_DIR__ e __SNAPSHOTS_DIR__ (pastas relativas à raiz do app).\n\nOpções:\n --id <id> id do layout (padrão: o nome do modelo)\n --title <texto> título da página (padrão: o id em palavras)\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\n --skip-page não cria a página\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\n --endpoint <url> endpoint do runQuery (padrão: /api/dashboard/query)\n --api <arquivo> arquivo do endpoint (padrão: pela rota do --endpoint, ver acima)\n --skip-api não cria o endpoint\n --api-template <a.> template do endpoint (padrão: <dir>/api.template, se existir)\n --snapshots <pasta> pasta dos snapshots no endpoint (padrão: tests/dashboard/snapshots)\n -h, --help esta ajuda\n`\n\nconst exists = (path: string) => existsSync(path)\n\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\nfunction importPath(from: string, to: string) {\n const path = relative(dirname(from), to).split(sep).join('/')\n return path.startsWith('.') ? path : `./${path}`\n}\n\n/** Router do Next que existir no app (Pages primeiro, como o `custo`). */\nfunction nextRouter(cwd: string) {\n for (const pages of ['src/pages', 'pages']) {\n if (!exists(join(cwd, pages))) continue\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\n .map((file) => join(cwd, file))\n .find(exists)\n return { app: false, dir: join(cwd, pages), config: config && readFileSync(config, 'utf8') }\n }\n for (const app of ['src/app', 'app'])\n if (exists(join(cwd, app))) return { app: true, dir: join(cwd, app), config: undefined }\n return undefined\n}\n\ntype Router = ReturnType<typeof nextRouter>\n\nfunction defaultPagePath(router: Router, slug: string) {\n if (!router) return undefined\n return router.app\n ? join(router.dir, 'dashboard', slug, 'page.tsx')\n : join(router.dir, 'dashboard', slug, `index.${pageExtension(router.config)}`)\n}\n\n/** Endpoint na rota do `--endpoint` (ex.: `/api/dashboard/query`). */\nfunction defaultApiPath(router: Router, endpoint: string) {\n if (!router) return undefined\n if (!/^(\\/[\\w-]+)+$/.test(endpoint))\n throw new ScaffoldError(\n `Endpoint inválido para criar a rota: ${endpoint} (use --api ou --skip-api)`\n )\n const route = endpoint.slice(1).split('/')\n return router.app\n ? join(router.dir, ...route, 'route.ts')\n : join(router.dir, ...route.slice(0, -1), `${route.at(-1)}.${apiExtension(router.config)}`)\n}\n\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\nfunction chartsIndex(dir: string) {\n return ['index.ts', 'index.tsx']\n .map((file) => join(dir, 'charts', file))\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\n}\n\n/** `export const chartTypes` em <dir>/charts/types.ts: o devtools do endpoint valida o layout. */\nfunction chartTypesFile(dir: string) {\n return ['types.ts', 'types.tsx']\n .map((file) => join(dir, 'charts', file))\n .find(\n (path) => exists(path) && /export\\s+const\\s+chartTypes\\b/.test(readFileSync(path, 'utf8'))\n )\n}\n\n/** Caminho relativo à raiz do app, sempre com `/`. */\nconst fromRoot = (cwd: string, path: string) => relative(cwd, path).split(sep).join('/')\n\nfunction main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n id: { type: 'string' },\n dir: { type: 'string', default: 'src/dashboard' },\n models: { type: 'string' },\n layouts: { type: 'string' },\n title: { type: 'string' },\n page: { type: 'string' },\n 'skip-page': { type: 'boolean' },\n template: { type: 'string' },\n endpoint: { type: 'string', default: '/api/dashboard/query' },\n api: { type: 'string' },\n 'skip-api': { type: 'boolean' },\n 'api-template': { type: 'string' },\n snapshots: { type: 'string', default: 'tests/dashboard/snapshots' },\n help: { type: 'boolean', short: 'h' }\n }\n })\n const [command, name, ...rest] = positionals\n if (values.help || !command) {\n process.stdout.write(HELP)\n return\n }\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\n\n const id = values.id ?? name\n checkModelName(name)\n checkLayoutId(id)\n\n const cwd = process.cwd()\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\n const modelPath = join(modelsDir, `${name}.ts`)\n const indexPath = join(modelsDir, 'index.ts')\n const layoutPath = join(layoutsDir, `${id}.json`)\n const dashboardDir = join(cwd, values.dir)\n const router = nextRouter(cwd)\n const pagePath = values['skip-page']\n ? undefined\n : values.page\n ? join(cwd, values.page)\n : defaultPagePath(router, kebabCase(id))\n const apiPath = values['skip-api']\n ? undefined\n : values.api\n ? join(cwd, values.api)\n : defaultApiPath(router, values.endpoint)\n // O endpoint é um só para todos os dashboards: se já existe, fica como está\n const apiExists = !!apiPath && exists(apiPath)\n const templatePath = values.template\n ? join(cwd, values.template)\n : [join(dashboardDir, 'page.template')].find(exists)\n const apiTemplatePath = values['api-template']\n ? join(cwd, values['api-template'])\n : [join(dashboardDir, 'api.template')].find(exists)\n for (const path of [templatePath, apiTemplatePath])\n if (path && !exists(path))\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, path)}`)\n\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\n for (const path of [modelPath, layoutPath, pagePath])\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\n const index = existsSync(indexPath)\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\n : modelIndexCode(name)\n let page: string | undefined\n if (pagePath) {\n const charts = chartsIndex(dashboardDir)\n const options: PageOptions = {\n model: name,\n layoutImport: importPath(pagePath, layoutPath),\n title: values.title ?? titleCase(id),\n endpoint: values.endpoint,\n // App Router: a página usa hooks, então é client component\n clientDirective:\n basename(pagePath).startsWith('page.') &&\n relative(cwd, pagePath).split(sep).includes('app'),\n chartsImport: charts && importPath(pagePath, dirname(charts))\n }\n page = templatePath\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\n : pageFileCode(options)\n }\n let api: string | undefined\n if (apiPath && !apiExists) {\n const chartTypes = chartTypesFile(dashboardDir)\n const options: ApiOptions = {\n modelsImport: importPath(apiPath, modelsDir),\n modelsDir: fromRoot(cwd, modelsDir),\n layoutsDir: fromRoot(cwd, layoutsDir),\n snapshotsDir: fromRoot(cwd, join(cwd, values.snapshots)),\n chartTypesImport: chartTypes && importPath(apiPath, chartTypes.replace(/\\.tsx?$/, '')),\n appRouter: basename(apiPath).startsWith('route.')\n }\n api = apiTemplatePath\n ? fillApiTemplate(readFileSync(apiTemplatePath, 'utf8'), options)\n : apiFileCode(options)\n }\n\n const written: string[] = []\n const write = (path: string, content: string) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n written.push(relative(cwd, path))\n }\n write(modelPath, modelFileCode(name))\n if (index !== null) write(indexPath, index)\n write(layoutPath, layoutFileCode(id))\n if (pagePath && page !== undefined) write(pagePath, page)\n if (apiPath && api !== undefined) write(apiPath, api)\n\n process.stdout.write(\n [\n ...written.map((path) => `✓ ${path}`),\n ...(pagePath\n ? templatePath\n ? [` (página do template ${relative(cwd, templatePath)})`]\n : []\n : values['skip-page']\n ? []\n : [\n '',\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\n ' <DashboardFilters layout={layout} />',\n ' <DashboardCharts layout={layout} />',\n ' </DashboardProvider>'\n ]),\n ...(apiPath\n ? apiExists\n ? [`· ${relative(cwd, apiPath)} (endpoint já existe, mantido)`]\n : apiTemplatePath\n ? [` (endpoint do template ${relative(cwd, apiTemplatePath)})`]\n : []\n : values['skip-api']\n ? []\n : ['', 'Endpoint não criado: não achei src/pages nem src/app. Use --api <arquivo>.']),\n ''\n ].join('\\n')\n )\n}\n\ntry {\n main(process.argv.slice(2))\n} catch (error) {\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\n process.stderr.write(`${error.message}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { A as DashboardQuery, C as ColumnAlign, I as FilterValue, M as DashboardResult, N as DashboardRow, T as FormatRule, X as ModelMeta, Y as ModelDef, a as CardStyle, c as ChartNumberFormat, d as ChartTypes, f as ChartsLayout, j as DashboardQueryDebug, k as DashboardFilters$1, n as BarValuePosition, s as ChartDef, u as ChartTypeSpec, w as ColumnVerticalAlign } from "../layout-DsG-zRIO.js";
1
+ import { $ as ModelMeta, A as ChartTypeSpec, D as ChartDef, I as DonutLegendPosition, L as DonutValueContent, M as ChartsLayout, O as ChartNumberFormat, Q as ModelDef, S as BarValuePosition, T as CardStyle, c as DashboardFilters$1, d as DashboardResult, f as DashboardRow, h as FilterValue, i as ValueRule, j as ChartTypes, l as DashboardQuery, n as ColumnVerticalAlign, r as FormatRule, t as ColumnAlign, u as DashboardQueryDebug } from "../formatRules-DTKbov47.js";
2
2
  import { ComponentType, MouseEvent, ReactNode } from "react";
3
3
  //#region src/client/BarChart.d.ts
4
4
  type BarChartProps = {
@@ -44,6 +44,13 @@ type BarChartProps = {
44
44
  valueSize?: number;
45
45
  /** Empilhado: total da pilha na ponta da barra. */
46
46
  showTotal?: boolean;
47
+ /** Só mostra o valor (e o total) que atende à regra, ex.: `{ operator: '!=', value: 0 }`. */
48
+ valueRule?: ValueRule;
49
+ /**
50
+ * Com `clickToFilter`: o filtro da dimensão (e da série) esconde o resto em vez de só apagá-lo
51
+ * (a consulta passa a respeitar o próprio filtro). Padrão: `false` (apaga).
52
+ */
53
+ hideUnselected?: boolean;
47
54
  /**
48
55
  * Cor por série: `{ medida: '#rrggbb' }` ou, com `series`, `{ valor: '#rrggbb' }` (também
49
56
  * "(em branco)" e "Outros"). O que faltar segue a paleta.
@@ -64,7 +71,7 @@ type BarChartProps = {
64
71
  * Com `series`, uma medida quebrada pelos valores de outra dimensão:
65
72
  * `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
66
73
  */
67
- declare function BarChart({ dimension, measures: measureList, series, formatSeries, maxSeries, orientation, stacked, sort, limit, formatDimension, hideEmpty, showEmpty, showValues, valuePosition, valueColor, valueSize, showTotal, colors, clickToFilter, height, labels, formats: formatsByMeasure, queryLabel }: BarChartProps): import("react").JSX.Element;
74
+ declare function BarChart({ dimension, measures: measureList, series, formatSeries, maxSeries, orientation, stacked, sort, limit, formatDimension, hideEmpty, showEmpty, showValues, valuePosition, valueColor, valueSize, showTotal, valueRule, hideUnselected, colors, clickToFilter, height, labels, formats: formatsByMeasure, queryLabel }: BarChartProps): import("react").JSX.Element;
68
75
  //#endregion
69
76
  //#region src/client/ComboChart.d.ts
70
77
  type ComboChartProps = {
@@ -199,9 +206,30 @@ type DashboardChartsProps = {
199
206
  formatters?: Record<string, (value: FilterValue) => string>;
200
207
  /** Moldura de cada gráfico (título + conteúdo). */
201
208
  panel?: ComponentType<ChartPanelProps>;
202
- /** Espaço entre os cards (unidades do `theme.spacing`); o `cards.gap` do layout (px) tem prioridade. */
209
+ /** Espaço entre os cards (unidades do `theme.spacing`); `cards.rowGap`/`columnGap` do layout (px) têm prioridade. */
203
210
  gap?: number;
204
211
  };
212
+ /**
213
+ * `sx` da moldura de um card com o estilo do layout (`card` do `panel`). Só traz o que o layout
214
+ * definiu, para um `panel` do app espalhar por cima do visual dele:
215
+ * `<Paper sx={{ p: 2, borderRadius: 0, ...chartCardSx(card) }}>`.
216
+ */
217
+ declare function chartCardSx(card: CardStyle): {
218
+ boxSizing: 'border-box';
219
+ p?: string | undefined;
220
+ bgcolor?: string | undefined;
221
+ borderWidth?: string | undefined;
222
+ borderStyle?: string | undefined;
223
+ borderColor?: string | undefined;
224
+ borderRadius?: string | undefined;
225
+ boxShadow?: number | undefined;
226
+ };
227
+ /** `sx` do título de um card com o estilo do layout (idem `chartCardSx`). */
228
+ declare function chartTitleSx(card: CardStyle): Partial<{
229
+ color: string | undefined;
230
+ fontSize: string | undefined;
231
+ textAlign: "center" | "left" | "right" | undefined;
232
+ }>;
205
233
  /**
206
234
  * Renderiza os gráficos de um layout declarativo num grid de 12 colunas (no celular, um por linha;
207
235
  * dentro de um `<DashboardCanvas>` de tamanho fixo, sempre as 12). Em desenvolvimento, a aba
@@ -474,14 +502,23 @@ type DonutChartProps = {
474
502
  formats?: Record<string, ChartNumberFormat>;
475
503
  /** Casas decimais do percentual. Padrão: 1. */
476
504
  percentDecimals?: number;
505
+ /** Lado da legenda. Padrão: `right` (nome, % e valor; em cima/embaixo, só o nome, em linha). */
506
+ legendPosition?: DonutLegendPosition;
507
+ /** Valor de cada fatia do lado de fora da rosca. */
508
+ showValues?: boolean;
509
+ /** O que o valor de fora mostra. Padrão: `both` (`44.981 (51%)`). */
510
+ valueContent?: DonutValueContent;
511
+ /** Total no furo da rosca. Padrão: `true`. */
512
+ showTotal?: boolean;
477
513
  /** Nome da consulta no devtools. */
478
514
  queryLabel?: string;
479
515
  };
480
516
  /**
481
- * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor).
517
+ * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
518
+ * ou em cima/embaixo; `showValues` põe o valor de cada fatia do lado de fora.
482
519
  * Ex.: `<DonutChart dimension="operacao_status" measure="custoTotal" />`.
483
520
  */
484
- declare function DonutChart({ dimension, measure, maxSlices, formatDimension, clickToFilter, height, labels, formats, percentDecimals, queryLabel }: DonutChartProps): import("react").JSX.Element;
521
+ declare function DonutChart({ dimension, measure, maxSlices, formatDimension, clickToFilter, height, labels, formats, percentDecimals, legendPosition, showValues, valueContent, showTotal, queryLabel }: DonutChartProps): import("react").JSX.Element;
485
522
  //#endregion
486
523
  //#region src/client/FilterSelect.d.ts
487
524
  type FilterSelectProps = {
@@ -575,5 +612,5 @@ declare function useModelQuery({ dimensions, measures, ignoreFilters, label, ski
575
612
  };
576
613
  };
577
614
  //#endregion
578
- export { BarChart, type ChartComponentProps, type ChartComponentSpec, type ChartPanelProps, ComboChart, DashboardCanvas, DashboardCharts, type DashboardFetcher, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartPlaceholder, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
615
+ export { BarChart, type ChartComponentProps, type ChartComponentSpec, type ChartPanelProps, ComboChart, DashboardCanvas, DashboardCharts, type DashboardFetcher, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartCardSx, chartPlaceholder, chartTitleSx, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
579
616
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
- import { T as rowRuleStyles, _ as tableColumns, f as isBuiltinChart, h as percentColumn, o as chartCardStyle, u as customChartProps } from "../layout-CBzYDoqE.js";
2
- import { S as layoutItemKey, _ as useClickFilter, a as formatCurrency, c as measureFormat, d as OTHER_COLOR, f as SERIES_COLORS, g as seriesColor, h as numberOf, i as fieldLabel, l as measureFormatter, m as chartPlaceholder, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as TooltipBox, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as Legend, v as DashboardProvider, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "../ModelDictionary-CyeYU6ZJ.js";
1
+ import { D as matchesValueRule, O as rowRuleStyles, d as customChartProps, g as percentColumn, o as cardsGap, p as isBuiltinChart, s as chartCardStyle, v as tableColumns } from "../layout-CrZ00tB0.js";
2
+ import { S as layoutItemKey, _ as useClickFilter, a as formatCurrency, c as measureFormat, d as OTHER_COLOR, f as SERIES_COLORS, g as seriesColor, h as numberOf, i as fieldLabel, l as measureFormatter, m as chartPlaceholder, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as TooltipBox, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as Legend, v as DashboardProvider, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "../ModelDictionary-DUGFHBo-.js";
3
3
  import { Box, Checkbox, CircularProgress, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInput, Paper, Select, Table, TableBody, TableCell, TableHead, TableRow, TableSortLabel, Typography } from "@mui/material";
4
4
  import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, ComposedChart, LabelList, Line, Pie, PieChart, ResponsiveContainer, Tooltip as Tooltip$1, XAxis, YAxis } from "recharts";
5
5
  import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
@@ -134,13 +134,14 @@ const ROW_HEIGHT = 28;
134
134
  * Com `series`, uma medida quebrada pelos valores de outra dimensão:
135
135
  * `<BarChart dimension="mes" series="tipo_descarga" measures={['volume']} stacked />`.
136
136
  */
137
- function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showEmpty = false, showValues = true, valuePosition = "outside", valueColor, valueSize = 12, showTotal = false, colors, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
137
+ function BarChart({ dimension, measures: measureList, series, formatSeries = String, maxSeries = 6, orientation = "vertical", stacked = false, sort = "dimension", limit, formatDimension = String, hideEmpty = true, showEmpty = false, showValues = true, valuePosition = "outside", valueColor, valueSize = 12, showTotal = false, valueRule, hideUnselected = false, colors, clickToFilter = false, height = 360, labels, formats: formatsByMeasure, queryLabel }) {
138
138
  const { meta, filters, setFilter } = useDashboard();
139
+ const dimming = clickToFilter && !hideUnselected;
139
140
  const measures = series ? measureList.slice(0, 1) : measureList;
140
141
  const { rows, isLoading, error, inspectProps } = useModelQuery({
141
142
  dimensions: series ? [dimension, series] : [dimension],
142
143
  measures,
143
- ignoreFilters: clickToFilter ? series ? [dimension, series] : [dimension] : void 0,
144
+ ignoreFilters: dimming ? series ? [dimension, series] : [dimension] : void 0,
144
145
  label: queryLabel ?? `BarChart · ${dimension}`
145
146
  });
146
147
  const domain = useModelQuery({
@@ -153,7 +154,7 @@ function BarChart({ dimension, measures: measureList, series, formatSeries = Str
153
154
  const table = meta?.dimensions[dimension]?.table;
154
155
  const categories = useModelQuery({
155
156
  dimensions: [dimension],
156
- ignoreFilters: Object.keys(filters).filter((name) => clickToFilter && name === dimension || !table || meta?.dimensions[name]?.table !== table),
157
+ ignoreFilters: Object.keys(filters).filter((name) => dimming && name === dimension || !table || meta?.dimensions[name]?.table !== table),
157
158
  label: `${queryLabel ?? `BarChart · ${dimension}`} · categorias`,
158
159
  skip: !showEmpty || !meta
159
160
  });
@@ -420,13 +421,13 @@ function BarChart({ dimension, measures: measureList, series, formatSeries = Str
420
421
  segmentLabels && /* @__PURE__ */ jsx(LabelList, {
421
422
  dataKey: bar.dataKey,
422
423
  position: position[valuePosition],
423
- formatter: (value) => stacked && !Number(value) ? "" : bar.format(value),
424
+ formatter: (value) => stacked && !Number(value) || !matchesValueRule(valueRule, Number(value)) ? "" : bar.format(value),
424
425
  style: labelStyle(valuePosition === "outside")
425
426
  }),
426
427
  totalLabel && index === bars.length - 1 && /* @__PURE__ */ jsx(LabelList, {
427
428
  dataKey: TOTAL_KEY,
428
429
  position: position.outside,
429
- formatter: (value) => (bars[0]?.format ?? String)(value),
430
+ formatter: (value) => matchesValueRule(valueRule, Number(value)) ? (bars[0]?.format ?? String)(value) : "",
430
431
  style: {
431
432
  ...labelStyle(true),
432
433
  ...valuePosition !== "outside" && { fill: "#333" },
@@ -1021,11 +1022,22 @@ function DataTable({ dimensions = EMPTY, measures, percentOf = EMPTY, formatDime
1021
1022
  //#endregion
1022
1023
  //#region src/client/DonutChart.tsx
1023
1024
  const OTHERS = "Outros";
1025
+ /** Fatia abaixo dessa fração fica sem valor de fora (os rótulos se atropelariam); tem o tooltip. */
1026
+ const MIN_VALUE_SHARE = .02;
1027
+ const RADIAN = Math.PI / 180;
1028
+ /** Direção da caixa (rosca + legenda) conforme o lado da legenda. */
1029
+ const LEGEND_DIRECTION = {
1030
+ right: "row",
1031
+ left: "row-reverse",
1032
+ top: "column-reverse",
1033
+ bottom: "column"
1034
+ };
1024
1035
  /**
1025
- * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor).
1036
+ * Rosca de uma medida por uma dimensão, com o total no centro e a legenda ao lado (nome, % e valor)
1037
+ * ou em cima/embaixo; `showValues` põe o valor de cada fatia do lado de fora.
1026
1038
  * Ex.: `<DonutChart dimension="operacao_status" measure="custoTotal" />`.
1027
1039
  */
1028
- function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = String, clickToFilter = false, height = 300, labels, formats, percentDecimals = 1, queryLabel }) {
1040
+ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = String, clickToFilter = false, height = 300, labels, formats, percentDecimals = 1, legendPosition = "right", showValues = false, valueContent = "both", showTotal = true, queryLabel }) {
1029
1041
  const { meta } = useDashboard();
1030
1042
  const { rows, isLoading, error, inspectProps } = useModelQuery({
1031
1043
  dimensions: [dimension],
@@ -1068,6 +1080,26 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1068
1080
  if (slice?.value !== null && slice?.value !== void 0) filter.toggle(slice.value, event);
1069
1081
  };
1070
1082
  const dimmed = (slice) => filter.selected.length > 0 && (slice.value === null || filter.isDimmed(slice.value));
1083
+ const valueText = (amount) => valueContent === "value" ? format(amount) : valueContent === "percent" ? percent(amount) : `${format(amount)} (${percent(amount)})`;
1084
+ const SliceValue = ({ cx, cy, midAngle, outerRadius, payload }) => {
1085
+ const slice = payload;
1086
+ if (slice.amount / total < MIN_VALUE_SHARE) return null;
1087
+ const radius = outerRadius + 10;
1088
+ const cos = Math.cos(-midAngle * RADIAN);
1089
+ const x = cx + radius * cos;
1090
+ const y = cy + radius * Math.sin(-midAngle * RADIAN);
1091
+ return /* @__PURE__ */ jsx("text", {
1092
+ x,
1093
+ y,
1094
+ textAnchor: Math.abs(cos) < .15 ? "middle" : cos > 0 ? "start" : "end",
1095
+ dominantBaseline: "central",
1096
+ fontSize: 12,
1097
+ fill: "#333",
1098
+ opacity: dimmed(slice) ? .45 : 1,
1099
+ children: valueText(slice.amount)
1100
+ });
1101
+ };
1102
+ const side = legendPosition === "left" || legendPosition === "right";
1071
1103
  const SliceTooltip = ({ active, payload }) => {
1072
1104
  if (!active || !payload?.length) return null;
1073
1105
  const slice = payload[0].payload;
@@ -1093,17 +1125,21 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1093
1125
  sx: {
1094
1126
  height,
1095
1127
  display: "flex",
1096
- alignItems: "center",
1097
- gap: 2,
1128
+ flexDirection: LEGEND_DIRECTION[legendPosition],
1129
+ alignItems: side ? "center" : "stretch",
1130
+ gap: side ? 2 : 1,
1098
1131
  opacity: isLoading ? .5 : 1,
1099
1132
  transition: "opacity .2s"
1100
1133
  },
1101
1134
  children: [/* @__PURE__ */ jsxs(Box, {
1102
1135
  sx: {
1103
1136
  position: "relative",
1104
- height: "100%",
1105
- flex: "0 0 50%",
1106
- minWidth: 140
1137
+ minWidth: 140,
1138
+ minHeight: 0,
1139
+ ...side ? {
1140
+ height: "100%",
1141
+ flex: showValues ? "1 1 0" : "0 0 50%"
1142
+ } : { flex: "1 1 0" }
1107
1143
  },
1108
1144
  children: [/* @__PURE__ */ jsx(ResponsiveContainer, {
1109
1145
  width: "100%",
@@ -1112,13 +1148,15 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1112
1148
  data: slices,
1113
1149
  dataKey: "amount",
1114
1150
  nameKey: "label",
1115
- innerRadius: "62%",
1116
- outerRadius: "92%",
1151
+ innerRadius: showValues ? "46%" : "62%",
1152
+ outerRadius: showValues ? "68%" : "92%",
1117
1153
  startAngle: 90,
1118
1154
  endAngle: -270,
1119
1155
  stroke: "#fff",
1120
1156
  strokeWidth: 2,
1121
1157
  isAnimationActive: false,
1158
+ label: showValues ? SliceValue : false,
1159
+ labelLine: false,
1122
1160
  onClick: (_, index, event) => select(slices[index], event),
1123
1161
  cursor: clickToFilter ? "pointer" : void 0,
1124
1162
  children: slices.map((slice) => /* @__PURE__ */ jsx(Cell, {
@@ -1126,7 +1164,7 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1126
1164
  fillOpacity: dimmed(slice) ? .35 : 1
1127
1165
  }, slice.key))
1128
1166
  })] })
1129
- }), /* @__PURE__ */ jsxs(Box, {
1167
+ }), showTotal && /* @__PURE__ */ jsxs(Box, {
1130
1168
  sx: {
1131
1169
  position: "absolute",
1132
1170
  inset: 0,
@@ -1152,14 +1190,15 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1152
1190
  children: format(total)
1153
1191
  })]
1154
1192
  })]
1155
- }), /* @__PURE__ */ jsx(Box, {
1193
+ }), side ? /* @__PURE__ */ jsx(Box, {
1156
1194
  component: "ul",
1157
1195
  sx: {
1158
1196
  listStyle: "none",
1159
1197
  m: 0,
1160
1198
  p: 0,
1161
1199
  minWidth: 0,
1162
- flex: 1,
1200
+ flex: showValues ? "0 1 auto" : 1,
1201
+ maxWidth: showValues ? "45%" : void 0,
1163
1202
  overflow: "auto",
1164
1203
  maxHeight: "100%"
1165
1204
  },
@@ -1210,6 +1249,43 @@ function DonutChart({ dimension, measure, maxSlices = 6, formatDimension = Strin
1210
1249
  })
1211
1250
  ]
1212
1251
  }, slice.key))
1252
+ }) : /* @__PURE__ */ jsx(Box, {
1253
+ component: "ul",
1254
+ sx: {
1255
+ listStyle: "none",
1256
+ m: 0,
1257
+ p: 0,
1258
+ display: "flex",
1259
+ flexWrap: "wrap",
1260
+ justifyContent: "center",
1261
+ columnGap: 1.5,
1262
+ rowGap: .25,
1263
+ flex: "0 0 auto",
1264
+ maxHeight: "40%",
1265
+ overflow: "auto"
1266
+ },
1267
+ children: slices.map((slice) => /* @__PURE__ */ jsxs(Box, {
1268
+ component: "li",
1269
+ title: `${format(slice.amount)} · ${percent(slice.amount)}`,
1270
+ onClick: (event) => select(slice, event),
1271
+ sx: {
1272
+ display: "flex",
1273
+ alignItems: "center",
1274
+ gap: .5,
1275
+ fontSize: 13,
1276
+ color: "#333",
1277
+ whiteSpace: "nowrap",
1278
+ opacity: dimmed(slice) ? .45 : 1,
1279
+ cursor: clickToFilter && slice.value !== null ? "pointer" : void 0
1280
+ },
1281
+ children: [/* @__PURE__ */ jsx(Box, { sx: {
1282
+ width: 10,
1283
+ height: 10,
1284
+ borderRadius: "50%",
1285
+ bgcolor: slice.color,
1286
+ flex: "0 0 auto"
1287
+ } }), slice.label]
1288
+ }, slice.key))
1213
1289
  })]
1214
1290
  })
1215
1291
  });
@@ -1295,26 +1371,52 @@ function KpiCard({ measure, label, secondary, secondaryLabel, labels, formats, q
1295
1371
  }
1296
1372
  //#endregion
1297
1373
  //#region src/client/DashboardCharts.tsx
1374
+ const px = (value) => value === void 0 ? void 0 : `${value}px`;
1375
+ /** Tira as chaves `undefined`, para o estilo do layout só sobrepor o que foi definido. */
1376
+ const defined = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
1377
+ /**
1378
+ * `sx` da moldura de um card com o estilo do layout (`card` do `panel`). Só traz o que o layout
1379
+ * definiu, para um `panel` do app espalhar por cima do visual dele:
1380
+ * `<Paper sx={{ p: 2, borderRadius: 0, ...chartCardSx(card) }}>`.
1381
+ */
1382
+ function chartCardSx(card) {
1383
+ return {
1384
+ boxSizing: "border-box",
1385
+ ...defined({
1386
+ p: px(card.padding),
1387
+ bgcolor: card.background,
1388
+ borderWidth: px(card.borderWidth),
1389
+ borderStyle: card.borderWidth === void 0 ? void 0 : "solid",
1390
+ borderColor: card.borderColor,
1391
+ borderRadius: px(card.borderRadius),
1392
+ boxShadow: card.shadow
1393
+ })
1394
+ };
1395
+ }
1396
+ /** `sx` do título de um card com o estilo do layout (idem `chartCardSx`). */
1397
+ function chartTitleSx(card) {
1398
+ return defined({
1399
+ color: card.titleColor,
1400
+ fontSize: px(card.titleSize),
1401
+ textAlign: card.titleAlign
1402
+ });
1403
+ }
1298
1404
  function DefaultPanel({ title, card, children }) {
1299
- const px = (value) => value === void 0 ? void 0 : `${value}px`;
1300
1405
  return /* @__PURE__ */ jsxs(Paper, {
1301
- elevation: card.shadow ?? 0,
1406
+ elevation: 0,
1302
1407
  sx: {
1303
- p: px(card.padding) ?? 1.5,
1408
+ p: 1.5,
1304
1409
  minWidth: 0,
1305
1410
  height: "100%",
1306
- bgcolor: card.background,
1307
- border: `${card.borderWidth ?? 1}px solid`,
1308
- borderColor: card.borderColor ?? "divider",
1309
- borderRadius: px(card.borderRadius)
1411
+ border: "1px solid",
1412
+ borderColor: "divider",
1413
+ ...chartCardSx(card)
1310
1414
  },
1311
1415
  children: [title && /* @__PURE__ */ jsx(Typography, {
1312
1416
  sx: {
1313
1417
  fontWeight: 700,
1314
1418
  mb: 1,
1315
- color: card.titleColor,
1316
- fontSize: px(card.titleSize),
1317
- textAlign: card.titleAlign
1419
+ ...chartTitleSx(card)
1318
1420
  },
1319
1421
  children: title
1320
1422
  }), children]
@@ -1335,10 +1437,12 @@ function DashboardCharts({ layout, formatters = {}, panel: Panel = DefaultPanel,
1335
1437
  const { charts } = current;
1336
1438
  const fixed = useFixedCanvas();
1337
1439
  const columns = `repeat(12, minmax(0, 1fr))`;
1440
+ const space = cardsGap(current.cards);
1338
1441
  return /* @__PURE__ */ jsx(Box, {
1339
1442
  sx: {
1340
1443
  display: "grid",
1341
- gap: current.cards?.gap === void 0 ? gap : `${current.cards.gap}px`,
1444
+ rowGap: px(space.row) ?? gap,
1445
+ columnGap: px(space.column) ?? gap,
1342
1446
  gridTemplateColumns: fixed ? columns : {
1343
1447
  xs: "1fr",
1344
1448
  lg: columns
@@ -1520,6 +1624,6 @@ function DashboardFilters({ layout, formatters = {}, gap = 2 }) {
1520
1624
  });
1521
1625
  }
1522
1626
  //#endregion
1523
- export { BarChart, ComboChart, DashboardCanvas, DashboardCharts, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartPlaceholder, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
1627
+ export { BarChart, ComboChart, DashboardCanvas, DashboardCharts, DashboardFilters, DashboardProvider, DataTable, DonutChart, FilterSelect, INSPECT_ATTRIBUTE, KpiCard, Legend, ModelDictionary, OTHER_COLOR, SERIES_COLORS, TooltipBox, chartCardSx, chartPlaceholder, chartTitleSx, fieldLabel, formatCurrency, formatMeasure, formatNumber, measureFormat, measureFormatter, numberOf, seriesColor, useClickFilter, useDashboard, useModelQuery };
1524
1628
 
1525
1629
  //# sourceMappingURL=index.js.map