UI das telas — layout X-Adm e fragments do app¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15
As telas do integrador (Micronaut Views + JTE — templates compilados, decisão 0025) usam o layout padrão X-Adm, o kit mantido no repo central e comum a todo app da casa que renderiza HTML no servidor. Vale inclusive para as telas internas de admin/ops/dev: a consistência de marca e de UX do operador não abre exceção para tela interna. A norma está na constituição, Engenharia e a receita por stack em Java/Micronaut §UI.
O que é do central e o que é do app¶
| Arquivo | Dono | Regra |
|---|---|---|
src/main/jte/kit/layout.jte, src/main/jte/kit/pager.jte |
repo central | Não editar aqui. Cópia verbatim do template rastreado (o layout e o paginador das listagens); a /xadm-docs re-deriva quando o kit muda no central. |
public/css/custom-theme.css |
repo central | Idem. Carrega a paleta única da casa (royal #064490, accent #2a5b97, navy #042c5e, prata #939495) — a mesma do portal de docs. |
public/images/logo.png, public/images/favicon.ico |
repo central | Idem. |
src/main/jte/kit/navMenu.jte, src/main/jte/tag/*.jte |
este app | O que é só do integrador: o menu (navMenu.jte) e os tags reutilizáveis (exportExcelButton.jte, homeCard.jte). |
src/main/jte/**/list.jte, **/form.jte (as telas) |
este app | Cada tela escreve só o seu conteúdo e chama o kit/layout.jte passando-o. |
Precisa mudar a cor, o header ou a marca? A mudança é no repo central e chega aqui pela
/xadm-docs — editar o layout.jte ou o custom-theme.css neste repo faz o app divergir
da casa em silêncio e transforma todo re-pull futuro num merge à mão.
Como uma tela se monta¶
A composição JTE inverte a do Thymeleaf: em vez de a página puxar fragments do layout, a
página chama o layout e passa o próprio conteúdo (e, opcionalmente, o menu) como blocos
@`...`@:
@template.kit.layout(title = "Contratos - Integrador", appNome = appNome, ...,
navMenu = @`@template.kit.navMenu(showInternalMenus = showInternalMenus, ...)`,
content = @`
<div class="container mt-5">
... o conteúdo da tela ...
</div>
`)
O kit/layout.jte expõe os @param title, appNome, clienteXadm, appVersao, appModo,
authEnabled, currentUserEmail, currentUserName (vêm do model via GlobalViewModel) e dois
blocos Content: content (obrigatório — o corpo da tela) e navMenu (opcional). Sem navMenu
o header sai simples (só marca + sessão); com ele, o menu aparece. O bloco direito
(versão/modo/chip de sessão) é o kit/headerRight.jte, chamado uma vez pelo layout.
Todas as telas do integrador passam o navMenu do kit/navMenu.jte. Item de menu novo se
acrescenta lá, não no layout.
Menu por sessão¶
O menu é o mesmo em toda tela; o que muda é o que ele mostra. O GlobalViewModel
(um ViewModelProcessor) injeta em todo model:
appNome("Integrador") eclienteXadm(envCLIENTE) — o que o cabeçalho exibe;showInternalMenus— os dropdowns internos (Cadastros/Estoque/Pedidos/MDF-e/Sistema) só aparecem em dev/test ou, em produção com auth ativo, para usuário logado. Anônimo em produção vê só o Swagger e o botão Entrar;debugOperationsAllowed/crudWriteAllowed— gates de escrita e da entrada Debug, espelhando oUiProductionGuard(segurança);authEnabled,currentUserEmail,currentUserName— o chip de sessão (login Google).
A regra de visibilidade é exercitada por teste de rendering (pacote views), que confere
o HTML servido: logado vê os dropdowns, anônimo não.
Home: cards por seção com contagem¶
A home (index.jte) agrupa os cards nas mesmas seções do menu (Cadastros / Estoque /
Pedidos / MDF-e / Sistema) e cada card mostra, num badge, quantos registros ativos
(deleted = false) a tabela tem — dá pra ver de relance onde há dado e onde não há. O card
é a tag homeCard(href, titulo, desc, n) do kit (tag/homeCard.jte); o número vem de
HomeCountService, um contador JDBC genérico (SELECT count(*) … WHERE deleted = false,
ou bruto para request/push_enviada, que não têm soft-delete). Os nomes de tabela são
literais hardcoded no serviço — nunca vêm de request, então não há injeção. Tabela que
falhe a contagem (ex. migração não aplicada num ambiente) devolve null e o card mostra
— em vez de derrubar a página. Tabela nova na home ⇒ acrescente a linha em
HomeCountService.TABLES e o card no index.jte.
Listagens paginadas¶
Toda tela de listagem pagina no banco — nenhuma carrega a tabela inteira. O controller recebe o
Pageable que o Micronaut Data monta de ?page= (base 0) e ?size=, e o repositório devolve o
Page (ou Slice). O tamanho vem de micronaut.data.pageable no application.yml
(default-page-size: 50, max-page-size: 200); ?size= acima do teto é cortado pelo binder. O
?sort= da URL é descartado (pageable.withoutSort()): a ordem é a do método do repositório —
id desc nas tabelas do espelho (UUID: ordem estável, não cronológica), enviado_em desc no
/push-enviadas e id desc (identity, a ordem de chegada) no /request.
O controller põe no model a lista da página, na mesma chave de antes (ex. contratos), e o Page
em pagina; a tela chama o paginador do kit, @template.kit.pager(pagina = pagina, baseUrl = "/contratos",
filtros = showDeleted ? "&showDeleted=true" : ""): anterior/próxima e "Página X de Y · N registro(s)".
O filtros é a query já codificada que a navegação preserva — aqui, o showDeleted; tela sem filtro
omite o parâmetro. O /request usa Slice — o log de ingest não tem teto, e o count(*)
varreria a tabela a cada página —, então o paginador mostra só "Página X" e anterior/mais.
O export XLSX (/export/xlsx/{tabela}, TableExportService) não pagina: exporta a tabela toda.
A paginação é exercitada por render com Postgres real (ListagemPaginadaRenderTest), inclusive
toda listagem de tela com um ?sort= de propriedade inexistente, que tem de responder 200.
Espelho MDF-e: telas somente-leitura¶
As 7 tabelas do espelho MDF-e (mdf, mdfcompl, mdfitens, nfeevento, nfmdf,
nfcomplmdf, fretes — baixa por macro Sascar, .ia/009) têm cada uma um
*ViewController que lista os registros ativos (findByDeletedFalseOrderByIdDesc, paginado —
ver Listagens paginadas) numa list.jte
sem CRUD — são escritas só pelo ingest, então a UI não edita nem apaga. Todas entram no
menu MDF-e e na seção MDF-e da home, e estão na allowlist do TableExportService (botão
Exportar Excel funciona). Ao contrário das telas de cadastro (Fones etc.), não há form.jte
nem rota de save/delete.