Modal de detalhe e grids

detailModal com abas summary, form, relatedGrid, readonlyGrid e customComponent; edição inline por linha; rowActions com interpolação, condições e refresh entre abas.

Centrais frequentemente têm uma entidade principal com filhos operacionais — Projeto → Itens → Pagamentos, Pedido → Produtos → Expedições, Contrato → Parcelas → Documentos. A modal de detalhe resolve esse padrão sem página React.

Grid principal
└── ação "Editar"
    └── modal com abas
        ├── Resumo              (summary)
        ├── Dados Principais    (form)
        ├── Itens               (relatedGrid editável inline)
        └── Pagamentos          (readonlyGrid)

Exemplo completo

<CoreCollection
  model="OrcamentoProjeto"
  label="Orçamentos/Projetos"
  endpoint="/orcamentos-projetos"
  list={{
    filters: [{ field: "tipoRegistro", label: "Tipo", type: "select", options: [
      { label: "Todos", value: "" }, { label: "Só orçamentos", value: "Orçamento" }, { label: "Só projetos", value: "Projeto" },
    ] }],
    columns: ["tipoRegistro", "codigo", "nome", "cliente", "status", "totalParaComImpostos"],
    rowActions: [{ type: "openDetailModal", label: "Editar", icon: "edit", initialTab: "resumo" }],
  }}
  relations={{
    itens: { model: "OrcamentoItem", foreignKey: "projetoId", parentKey: "_id" },
    pagamentos: { model: "Pagamento", foreignKey: "projetoId", parentKey: "_id" },
  }}
  detailModal={{
    enabled: true,
    titleField: "nome",
    size: "xl",
    defaultTab: "resumo",
    tabs: [
      { id: "resumo", label: "Resumo", type: "summary", cards: [
        { label: "Itens", source: "relatedCount", relation: "itens" },
        { label: "Pagamentos", source: "relatedCount", relation: "pagamentos" },
        { label: "Total", field: "totalParaComImpostos", format: "currency" },
      ] },
      { id: "dados", label: "Dados Principais", type: "form", groups: [
        { label: "Identificação", fields: ["tipoRegistro", "codigo", "nome", "status", "cliente"], columns: 2 },
        { label: "Evento", fields: ["dataEvento", "localEvento", "contato"] },
      ] },
      { id: "itens", label: "Itens", type: "relatedGrid", relation: "itens", editable: true, editMode: "inline",
        columns: [
          { field: "item", editable: true },
          { field: "status", editable: true },
          { field: "totalParaComImpostos", editable: true, format: "currency" },
          { field: "pagamentoId", label: "Pagamento", readonly: true, display: "badgeExists" },
        ],
        rowActions: [{
          id: "gerarPagamento", label: "Gerar pagamento", type: "apiAction", method: "POST",
          endpoint: "/orcamentos-itens/:id/gerar-pagamento",
          confirm: { title: "Gerar pagamento?", description: "Será criado um ticket financeiro vinculado a este item." },
          disabledWhen: { field: "pagamentoId", exists: true },
          refresh: ["self", "pagamentos", "resumo", "parent"],
        }],
      },
      { id: "pagamentos", label: "Pagamentos", type: "readonlyGrid", relation: "pagamentos",
        columns: ["codigo", "descricao", "statusEsteira", "valorFechamento"] },
    ],
  }}
/>

Abas

type Uso Configuração
summary Cards de indicadores cards[] com source: field, relatedCount, relatedSum, relatedAvg, customMetric; format
form Dados do registro principal (salva o pai) groups[] com label, description, fields, columns (1–4), ou fields
relatedGrid Filhos editáveis relation, editable, editMode (inline, modal, drawer), columns, rowActions
readonlyGrid Relacionados só leitura relation, columns, rowActions
customComponent Componente registrado component: "custom:Chave", props

detailModal.size: md, lg, xl, full.

Relações

relations: { itens: { model: "OrcamentoItem", foreignKey: "projetoId", parentKey: "_id", label: "Itens" } }

A aba busca os filhos por foreignKey = parent[parentKey]. Declare uma vez e reutilize em abas, filtros e ações.

Edição inline

Com editable: true e editMode: "inline":

  • célula editável conforme a metadata do campo (ou editor explícito: string, number, currency, date, enum, select, ref, boolean, textarea);
  • linha marcada como alterada, com Salvar e Cancelar por linha;
  • PUT /:modelPath/:id pelo CRUD genérico;
  • loading e erro por linha;
  • refresh pós-salvamento.

Em esteiras, relatedGrid também aceita create (inclusão vinculada ao pai) e delete (com confirmação).

Ações por linha

type Efeito
openDetailModal Abre a modal (initialTab)
navigate Navega para href
apiAction Chama endpoint com method
customAction Renderer próprio (renderer)

Interpolação de endpoint

:id (linha), :parentId (registro pai), :campo (campo da linha) e :parent.campo (campo do pai).

Condições

disabledWhen: { field: "pagamentoId", exists: true }
hiddenWhen:   { field: "status", equals: "Cancelado" }

Operadores: exists, equals, notEquals, in, gt, gte, lt, lte.

Refresh

self (grid atual), parent (registro principal), all (todas as queries da modal) ou o id de uma aba.

RBAC visual

Abas, colunas e ações aceitam roles/permissions. A UI oculta ou desabilita conforme a permissão — mas a autoridade final é o backend: a rota de apiAction precisa declarar sua permission.

Componentes envolvidos

CoreDetailModal, CoreTabbedDetail, CoreSummaryCards, CoreRelatedGrid, CoreInlineEditableCell e CoreRowAction implementam o contrato de forma genérica.