# 10 · Experiência do usuário

Fonte: https://muriloht.com/idempiere/10-experiencia-do-usuario (Guia iDempiere, Murilo H. Torquato). Versão alvo: iDempiere 14.

Objetivos:
- Cadastrar um cliente sem sair da visita, com a entrada rápida
- Destacar informação importante com estilos condicionais
- Colocar um indicador no painel inicial do usuário
- Criar um formulário ZK próprio dentro do plugin
- Saber quando vale escrever tela em Java e quando o Dicionário basta

## Antes de começar

- Módulo 09 concluído: a visita é um documento, e "em aberto" é `Processed='N'`.
- Este módulo tem três partes de Dicionário (passos 1 a 3) e uma de Java (passo 4). Se o tempo for curto, faça as três primeiras: são as que mais melhoram o dia do usuário.

## O que você vai construir

Um sistema que funciona não é o mesmo que um sistema bom de usar. Os técnicos da GardenWorld reclamam de quatro coisas:

- "Para agendar um cliente novo, tenho de sair da visita e ir ao cadastro."
- "Não vejo, batendo o olho, quais visitas são urgentes."
- "Quando entro no sistema, não sei quanto trabalho tenho pendente."
- "Queria uma tela só com a minha agenda, sem filtros."

Cada uma tem uma resposta diferente no iDempiere, e só a última exige Java.

## Passo a passo

### 1. Entrada rápida: cliente novo sem sair da visita

**Conceito:** a **Quick Entry** (entrada rápida) abre um formulário pequeno para criar ou alterar o registro apontado por um campo, sem sair da janela. Os campos do formulário são os marcados como **Quick Entry** na janela de destino.

O cadastro de parceiros já vem com entrada rápida. Teste: como GardenAdmin, numa visita nova, clique com o botão direito no campo **Cliente** e escolha **New**. Aparece um formulário com Search Key, Name, Customer e mais alguns campos.

Falta o essencial no Brasil: o CNPJ ou CPF. Como **System**, na janela **Window, Tab and Field**, abra a janela **Business Partner**, aba **Business Partner**, e na aba **Field** marque **Quick Entry** no campo **Tax ID**.

Faça logout e login, e teste de novo: o formulário agora pede o Tax ID. Crie um cliente pela entrada rápida. Ele já fica selecionado na visita.

> A entrada rápida grava um registro de verdade, com as mesmas regras da janela completa: callouts, `beforeSave` e event handlers rodam normalmente. O cuidado é outro: **toda coluna obrigatória sem valor padrão precisa estar no formulário**, ou o registro não salva. Confira as obrigatórias antes de tirar um campo da entrada rápida.

> Para um callout agir só dentro da entrada rápida, leia a variável de contexto `_QUICK_ENTRY_MODE_` da janela.

### 2. Estilo condicional: urgente em vermelho

**Conceito:** um **CSS Style** é um conjunto de linhas de CSS, cada uma com uma **Display Logic**. A linha só vale quando a lógica é verdadeira. O estilo é aplicado a campos (rótulo ou valor) e a colunas de Info Window.

Como **System**, na janela **CSS Style**, crie:

| Campo | Valor |
|---|---|
| Name | `EDU Urgente` |
| Entity Type | `EDU` |

Na aba **Style Line**:

| Line No | Inline Style | Display Logic |
|---|---|---|
| 10 | `color: #b3261e; font-weight: bold;` | `@EDU_IsUrgente@=Y` |

Agora aplique. Na janela **Window, Tab and Field**, janela **Visita Técnica**, aba **Visita**, aba **Field**, abra o campo **Urgente** e preencha **Label Style** com `EDU Urgente`. Faça o mesmo no campo **Motivo da Urgência**.

Teste: abra uma visita urgente e uma normal. Marque e desmarque **Urgente** e veja o rótulo mudar na hora: a lógica é reavaliada a cada mudança, como no módulo 02.

> Use estilo para **sinalizar**, nunca para esconder informação: o daltônico que não distingue o vermelho ainda precisa ver o texto. E com moderação: se tudo é destaque, nada é.

Na Info Window do módulo 08, a coluna **Concluída** ganha o mesmo recurso pelo campo **Field Style** da coluna. Crie um estilo `EDU Em aberto`, com cor diferente, para `@EDU_IsConcluida@=N`, e aplique.

### 3. Um indicador no painel inicial

**Conceito:** o painel inicial do iDempiere é feito de **Dashboard Content**: pequenos blocos com gráfico, relatório, HTML ou uma **Status Line**. Você já tem uma Status Line do módulo 08; aqui vai uma variação com o usuário logado.

Crie a mensagem (janela **Message**, Entity Type `EDU`):

| Search Key | Message Type | Message Text |
|---|---|---|
| `EDU_MinhasVisitasEmAberto` | Information | `Você tem {0} visita(s) técnica(s) em aberto.` |

Crie a Status Line (janela **Status Line**, Entity Type `EDU`):

| Campo | Valor |
|---|---|
| Name | `EDU Minhas visitas em aberto` |
| Message | `EDU_MinhasVisitasEmAberto` |
| SQL Expression/Statement | `SELECT COUNT(*) FROM EDU_Visita WHERE SalesRep_ID=@#AD_User_ID@ AND Processed='N' AND IsActive='Y' AND AD_Client_ID=@#AD_Client_ID@` |

Sem aba **Used In**: ela não pertence a janela nenhuma. O `@#AD_User_ID@` é o usuário logado, da mesma forma nas lógicas do módulo 02.

Agora, como **GardenAdmin**, na janela **Dashboard Content**, crie:

| Campo | Valor |
|---|---|
| Name | `Minhas visitas` |
| Status Line | `EDU Minhas visitas em aberto` |
| Column No / Line No | `1` / `1` |
| Show in Dashboard | marcado |

Volte ao painel inicial (ou faça logout e login). O bloco aparece, com a contagem do usuário logado. Entre como GardenUser e veja outro número.

> Por ser da GardenWorld, este Dashboard Content é configuração do cliente, não do Dicionário. Para cada empresa ter o seu, é o jeito certo. Para distribuir com o plugin, vai no Pack Out como **Data** (passo 5).

### 4. Um formulário ZK: Minha Agenda

**Conceito:** quando nenhuma janela, Info Window ou relatório resolve, você escreve uma tela em Java. O iDempiere usa o **ZK** na interface web, e uma tela própria é um **Form**: uma classe que herda de `ADForm` e monta os componentes no `initForm()`.

A regra para decidir: **escreva um Form quando a tela não é um cadastro nem uma busca**. Um painel de agenda, uma conferência de estoque com leitor de código de barras, um assistente passo a passo. Para listar e filtrar registros, a Info Window do módulo 08 é mais barata e o usuário já sabe usar.

Primeiro, o plugin passa a depender da interface web. No `MANIFEST.MF`, acrescente ao `Require-Bundle`:

```
Require-Bundle: org.adempiere.base;bundle-version="14.0.0",
 org.adempiere.plugin.utils;bundle-version="14.0.0",
 org.adempiere.ui.zk;bundle-version="14.0.0",
 zk;bundle-version="10.0.1",
 zul;bundle-version="10.0.1",
 zcommon;bundle-version="10.0.1"
```

> Com isso, o plugin só funciona onde a interface web existe. Numa instalação maior, o costume é separar: um plugin com modelo, regras e processos, e outro só com a interface. Para o guia, um basta.

Crie `com.gardenworld.visitas.form.AgendaTecnico`:

```java
package com.gardenworld.visitas.form;

import java.util.List;

import org.adempiere.webui.apps.AEnv;
import org.adempiere.webui.panel.ADForm;
import org.compiere.model.Query;
import org.compiere.util.DisplayType;
import org.compiere.util.Env;
import org.compiere.util.Msg;
import org.idempiere.ui.zk.annotation.Form;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Button;
import org.zkoss.zul.Column;
import org.zkoss.zul.Columns;
import org.zkoss.zul.Grid;
import org.zkoss.zul.Label;
import org.zkoss.zul.Row;
import org.zkoss.zul.Rows;

import com.gardenworld.visitas.model.MVisita;

@Form
public class AgendaTecnico extends ADForm {

    private static final long serialVersionUID = 1L;

    @Override
    protected void initForm() {
        Grid grid = new Grid();
        grid.setHflex("1");
        grid.setVflex("1");
        appendChild(grid);

        Columns colunas = new Columns();
        colunas.appendChild(new Column(Msg.getElement(Env.getCtx(), "EDU_DataVisita")));
        colunas.appendChild(new Column(Msg.getElement(Env.getCtx(), "DocumentNo")));
        colunas.appendChild(new Column(Msg.getElement(Env.getCtx(), "C_BPartner_ID")));
        colunas.appendChild(new Column(Msg.getElement(Env.getCtx(), "EDU_TipoVisita")));
        colunas.appendChild(new Column(""));
        grid.appendChild(colunas);

        Rows linhas = new Rows();
        grid.appendChild(linhas);

        List<MVisita> visitas = new Query(Env.getCtx(), MVisita.Table_Name,
                "SalesRep_ID=? AND Processed='N'", null)
            .setParameters(Env.getAD_User_ID(Env.getCtx()))
            .setClient_ID()
            .setOnlyActiveRecords(true)
            .setOrderBy(MVisita.COLUMNNAME_EDU_DataVisita)
            .list();

        for (MVisita visita : visitas) {
            Row linha = new Row();
            linha.appendChild(new Label(DisplayType.getDateFormat().format(visita.getEDU_DataVisita())));
            linha.appendChild(new Label(visita.getDocumentNo()));
            linha.appendChild(new Label(visita.get_DisplayValue(MVisita.COLUMNNAME_C_BPartner_ID, true)));
            linha.appendChild(new Label(visita.get_DisplayValue(MVisita.COLUMNNAME_EDU_TipoVisita, true)));

            Button abrir = new Button(Msg.getMsg(Env.getCtx(), "Zoom"));
            int visitaId = visita.getEDU_Visita_ID();
            abrir.addEventListener(Events.ON_CLICK, e -> AEnv.zoom(MVisita.Table_ID, visitaId));
            linha.appendChild(abrir);

            linhas.appendChild(linha);
        }
    }
}
```

O que vale observar:

- **Os títulos vêm do Dicionário** (`Msg.getElement`), não do código. Se alguém traduzir o elemento, a agenda acompanha.
- **`get_DisplayValue`** devolve o que o usuário veria na janela: o nome do cliente em vez do ID, o nome do tipo em vez do código.
- **`AEnv.zoom`** abre o registro na janela configurada para a tabela, como o duplo clique da Info Window.
- **A consulta usa o usuário do contexto** e `setClient_ID()`. Um Form não tem a segurança automática de uma janela: filtrar por empresa e por usuário é responsabilidade sua.

A quarta factory do plugin, `com.gardenworld.visitas.VisitasFormFactory`:

```java
package com.gardenworld.visitas;

import org.adempiere.webui.factory.AnnotationBasedFormFactory;
import org.adempiere.webui.factory.IFormFactory;
import org.osgi.service.component.annotations.Component;

@Component(immediate = true, service = IFormFactory.class,
           property = {"service.ranking:Integer=1"})
public class VisitasFormFactory extends AnnotationBasedFormFactory {

    @Override
    protected String[] getPackages() {
        return new String[] {"com.gardenworld.visitas.form"};
    }
}
```

Cadastre o Form. Como **System**, na janela **Form**:

| Campo | Valor |
|---|---|
| Name | `Minha Agenda` |
| Classname | `com.gardenworld.visitas.form.AgendaTecnico` |
| Data Access Level | `Client+Organization` |
| Entity Type | `EDU` |

Crie a entrada de menu com **Action** `Form` e **Special Form** `Minha Agenda`, e rode **Role Access Update** como GardenAdmin.

Reinicie o servidor. Como GardenAdmin, abra **Minha Agenda**: aparecem as visitas em aberto em que ele é o técnico. Clique no botão de uma linha para abrir a visita.

### 5. Commit

No Pack Out: a janela **Business Partner** não é sua, então não entra inteira. A mudança de Quick Entry no campo Tax ID vai como **Data** da tabela `AD_Field`, com `SELECT * FROM AD_Field WHERE AD_Field_UU='<UUID do campo>'` (o UUID está no próprio registro do campo). Inclua também o estilo (**Data**, tabelas `AD_Style` e `AD_StyleLine`), a janela `Visita Técnica` (os estilos nos campos), a mensagem nova, a Status Line (**Data**), o Form (tipo **Form**) e o menu. Gere o `2Pack_1.0.6.zip`.

```bash
git add .
git commit -m "Visitas 1.0.6: entrada rápida, estilos, indicador no painel e agenda do técnico"
```

> Mudar um objeto do core, como o campo Tax ID, é uma **customização do core**. Ela vai no seu pacote, mas registre isso no README do plugin: numa atualização do iDempiere, é o primeiro lugar a conferir.

## Checkpoint

- [ ] Crio um cliente novo pelo botão direito no campo Cliente, sem sair da visita
- [ ] A entrada rápida pede o CNPJ/CPF (Tax ID)
- [ ] Visita urgente mostra o campo Urgente em vermelho
- [ ] O painel inicial mostra quantas visitas em aberto o usuário tem
- [ ] O formulário Minha Agenda lista as visitas em aberto do técnico logado
- [ ] O botão da agenda abre a visita na janela Visita Técnica

## Erros comuns

| Sintoma | Causa provável | Correção |
|---|---|---|
| Não aparece **New** no botão direito do campo | O campo não é do tipo Search para `C_BPartner`, ou o perfil não pode criar parceiros | Confira a referência da coluna e o acesso do perfil à janela Business Partner |
| A entrada rápida não salva | Coluna obrigatória fora do formulário | Marque **Quick Entry** também nela, ou dê um valor padrão |
| O estilo não aparece | **Label Style** no lugar errado, ou Display Logic falsa | Confira o campo e a lógica com `@EDU_IsUrgente@=Y` |
| O bloco não aparece no painel | **Show in Dashboard** desmarcado, ou o perfil não tem acesso ao conteúdo | Passo 3; confira a aba **Dashboard Content Access** |
| O bloco mostra `{0}` | Mensagem sem marcador ou SQL que não devolve valor | Teste o SQL no `psql`, trocando as variáveis por valores |
| Plugin `INSTALLED` depois do passo 4 | Faltou algum bundle do ZK no `Require-Bundle` | `diag <id>` no console |
| O menu abre uma tela em branco ou erro | **Classname** diferente da classe, ou factory não registrada | Compare letra por letra; confira `OSGI-INF/` |
| A agenda aparece vazia | O usuário não é técnico de nenhuma visita em aberto | Abra uma visita em aberto e troque o técnico para o usuário logado |

## Desafio

Acrescente à agenda um botão **Iniciar rota**, que abre o endereço da visita no mapa. Dica: o endereço completo está em `C_BPartner_Location` → `C_Location`, e a ZK abre uma URL em nova aba com `Executions.getCurrent().sendRedirect(url, "_blank")`.

## Referência

### Onde cada tipo de melhoria mora

| Necessidade | Recurso | Código? |
|---|---|---|
| Criar registro relacionado sem sair da tela | Quick Entry | Não |
| Destacar valores | CSS Style | Não |
| Esconder, travar, exigir campos | Lógicas (módulo 02) | Não |
| Buscar e filtrar | Info Window (módulo 08) | Não |
| Número ou aviso de contexto | Status Line (módulo 08) | Não |
| Bloco no painel inicial | Dashboard Content | Não |
| Tela que não é cadastro nem busca | Form ZK | Sim |

### Onde o CSS Style se aplica

| Lugar | Campo |
|---|---|
| Rótulo de um campo | **Label Style** na aba Field |
| Valor de um campo | **Field Style** na aba Field |
| Coluna de Info Window | **Field Style** na coluna |

A **Display Logic** de cada linha usa a mesma sintaxe do módulo 02. Uma linha sem lógica vale sempre.

## Próximo módulo

[11 · Integrações](https://muriloht.com/idempiere/11-integracoes): a visita conversa com outros sistemas, pela API REST e por notificações de saída.
