O PO-UI é o framework de componentes do ecossistema TOTVS. Ele resolve metade do problema — a parte visual. A outra metade, que ninguém fala, é como extrair os dados do Protheus de forma performática e transformá-los em informação útil.
O que é PO-UI e por que usar
O PO-UI é uma biblioteca de componentes Angular criada pela TOTVS, seguindo o padrão TOTVS Design System. Ele já resolve:
- Componentes prontos: tabelas, gráficos, filtros, modais, drawers.
- Padrão visual: consistência com o ecossistema TOTVS.
- Responsividade: mobile-first nativo.
- Acessibilidade: WCAG 2.1 nos componentes principais.
O PO-UI não substitui o ADVPL — ele consome APIs REST que o ADVPL expõe. O backend continua sendo Protheus; o frontend é Angular com PO-UI.
Arquitetura típica de um dashboard
┌─────────────────────────────────────────────────┐
│ Frontend Angular + PO-UI │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐│
│ │ po-chart │ │ po-table │ │ po-drawer││
│ │ (gráficos) │ │ (dados) │ │ (filtro) ││
│ └──────┬──────┘ └──────┬──────┘ └────┬─────┘│
│ │ HTTP │ HTTP │ │
└─────────┼─────────────────┼──────────────┼──────┘
│ │ │
┌─────────┼─────────────────┼──────────────┼──────┐
│ Backend Protheus (ADVPL) │
│ ┌──────┴──────┐ ┌───────┴─────┐ ┌────┴────┐│
│ │ WS REST │ │ WS REST │ │ WS REST ││
│ │ /dashboard │ │ /vendas │ │ /filtro ││
│ └──────┬──────┘ └───────┬─────┘ └────┬────┘│
│ │ SQL │ SQL │SQL │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────┐│
│ │ SQL Server (Protheus) ││
│ │ SC5, SC6, SF2, SD2, SA1, SB1... ││
│ └──────────────────────────────────────────────┘│
└──────────────────────────────────────────────────┘Componentes essenciais para dashboards
po-chart — Gráficos
O po-chart usa p-series (array de PoChartSerie) e p-type (PoChartType): barras, linhas, área, pizza e donut. Fixe a versão do PO-UI do seu projeto e confira a API daquela versão — nomes de propriedade mudam entre releases:
// Template: <po-chart [p-type]="chartType" [p-series]="chartSeries" [p-x-axis-quotes]="meses"> export class DashboardComponent implements OnInit { chartType: PoChartType = PoChartType.Bar; chartSeries: PoChartSerie[] = []; meses: string[] = []; constructor(private dashboardService: DashboardService) {} ngOnInit() { this.dashboardService.getVendasMes().subscribe(data => { this.meses = data.map(item => item.mes); this.chartSeries = [ { label: 'Faturamento (R$)', data: data.map(item => item.faturamento) }, { label: 'Pedidos', data: data.map(item => item.pedidos) } ]; }); } }
APIs antigas de chart (chartItems/chartColumns com POChartAxisType) não existem na versão atual. E ícones: POIcon está depreciado na v21 — use os ícones Animalia (an an-...).
po-table — Tabela de dados
// Template PO-UI — o po-table tem p-striped; NÃO existe p-striped-columns <po-page-default p-title="Dashboard de Pedidos"> <po-widget p-title="Pedidos Recentes"> <po-table [p-columns]="columns" [p-items]="pedidos" [p-loading]="loading" p-striped="true" > </po-table> </po-widget> </po-page-default> // Colunas — currency usa código ISO; labels usam color-01..color-12 + type columns: PoTableColumn[] = [ { property: 'numero', label: 'Pedido', type: 'string' }, { property: 'cliente', label: 'Cliente', type: 'string' }, { property: 'valor', label: 'Valor', type: 'currency', format: 'BRL' }, { property: 'status', label: 'Status', type: 'label', labels: [ { value: '1', label: 'Faturado', type: 'success', color: 'color-10' }, { value: '2', label: 'Pendente', type: 'warning', color: 'color-08' } ] } ];
po-drawer — Filtros laterais
Em vez de poluir a tela com filtros, use o po-drawer para um painel lateral de filtragem:
<po-drawer [p-hide-close]="false" [p-expanded]="drawerExpanded" p-title="Filtros" p-size="sm" > <po-combo p-label="Filial" [p-options]="filiais" [(ngModel)]="filtro.filial" ></po-combo> <po-datepicker p-label="Período" [(ngModel)]="filtro.periodo" ></po-datepicker> <po-button p-label="Aplicar" (p-click)="aplicarFiltros()" ></po-button> </po-drawer>
po-lookup — Traduzindo códigos
Para buscar códigos com modal de consulta, o po-lookup é um componente de input separado — não é uma propriedade de coluna do po-table. Em tabelas, o padrão mais robusto é o backend já devolver o nome resolvido:
// 1. Filtro com po-lookup — componente próprio, com p-service-api <po-lookup p-label="Cliente" [p-columns]="lookupColumns" [p-service-api]="'/api/v1/clientes'" [(ngModel)]="filtro.cliente" ></po-lookup> // 2. Na tabela: coluna string — o backend já resolveu o nome { property: 'cliente', label: 'Cliente', type: 'string' } // backend retorna: { "cliente": "001-01 - ACME LTDA" }
po-info — KPIs e valores resumidos
Para exibir valores de destaque (KPIs), o po-info é mais indicado que uma tabela. Atenção: o po-info só tem p-label e p-value (+ p-orientation) — não existe p-icon nem p-value-color. Se precisar de ícone, coloque o po-info dentro de um po-widget com ícone Animalia:
<po-widget p-title="Financeiro"> <po-info class="po-md-4" p-label="Faturamento do Mês" [p-value]="faturamentoMes" ></po-info> <po-info class="po-md-4" p-label="Pedidos Ativos" [p-value]="pedidosAtivos" ></po-info> <po-info class="po-md-4" p-label="Ticket Médio" [p-value]="ticketMedio" ></po-info> </po-widget>
Backend ADVPL: WS REST para dashboards
O frontend precisa de dados. O backend usa o framework WSRESTFUL/WSMETHOD com ::SetResponse() e retorna .T. — erros via SetRestFault():
#Include "protheus.ch" + "restful.ch" + "topconn.ch" WsRestFul Dashboard Description "KPIs do dashboard v1" Format Application_Json WsMethod GET Description "KPIs agregados dos últimos 30 dias" WsSyntax "/api/v1/dashboard" End WsRestFul WsMethod GET WsService Dashboard Local oResponse := JsonObject():New() Local cQuery := "" // KPIs em uma query — colunas explícitas, filial, no-lock cQuery := "SELECT COUNT(DISTINCT SC5.C5_NUM) AS PEDIDOS, " cQuery += " SUM(SC6.C6_VALOR) AS FATURAMENTO, " cQuery += " COUNT(DISTINCT SC5.C5_CLIENTE+SC5.C5_LOJACLI) AS CLIENTES " cQuery += "FROM " + RetSqlName("SC5") + " SC5 " cQuery += "JOIN " + RetSqlName("SC6") + " SC6 " cQuery += " ON SC6.C6_FILIAL = SC5.C5_FILIAL " cQuery += "AND SC6.C6_NUM = SC5.C5_NUM " cQuery += "WHERE SC5.C5_FILIAL = '" + xFilial("SC5") + "' " cQuery += "AND SC5.C5_EMISSAO >= '" + Dtos(Date()-30) + "' " cQuery += "AND SC5.D_E_L_E_T_ = ' ' AND SC6.D_E_L_E_T_ = ' ' " cQuery += " %nolock%" cQuery := ChangeQuery(cQuery) TCQUERY cQuery NEW ALIAS "TMPDASH" // Campos numéricos podem chegar como texto — force o tipo TCSetField("TMPDASH", "FATURAMENTO", "N", 18, 2) oResponse["pedidos"] := TMPDASH->PEDIDOS oResponse["faturamento"] := TMPDASH->FATURAMENTO oResponse["clientes"] := TMPDASH->CLIENTES TMPDASH->(dbCloseArea()) ::SetContentType("application/json") ::SetResponse(oResponse:ToJson()) Return .T.
Valide no SX3 físico do seu ambiente antes de presumir campos "padrão" — C6_VALOR pode não existir fisicamente em ambientes com leiaute customizado. E filtros vindos do front (ex.: código de cliente) devem ser validados por tipo/tamanho antes de entrar na query — nunca concatene entrada livre sem validação (ver artigo de segurança de APIs).
Performance: regras para dashboards rápidos
Não faça uma query gigante e divida no frontend. Cada widget deve ter sua própria query otimizada.
Dados de dashboard não mudam a cada segundo. Implemente o cache em camada específica: FWCache/GetGlbValue com expiração no AppServer, ou HTTP cache no proxy reverso — não um "cache" genérico sem mecanismo definido.
Uma query de dashboard sem índices adequados trava em produção. Analise o execution plan antes de publicar.
Tabelas com +10k registros precisam de paginação server-side. No po-table isso é feito com p-show-more/(p-show-more), p-height + virtual scroll, ou p-service-api para carregar sob demanda — não existe um p-page solto.
Precisa de um dashboard PO-UI para sua empresa?
Falar com a ELP