O usuário digita na barra de busca e a lista filtra em tempo real, sem clicar em botão. A busca é fuzzy: açúcar acha Açúcar Refinado, acucar acha igual, AÇÚCAR também — tudo via uma coluna dedicada no banco com texto pré-normalizado (lowercase, sem acento).
5 peças trabalhando juntas: coluna nova no banco, campo no Bean, normalização no DAO, inner class QuickSearch que recebe o input do usuário, e a barra visual com ui().bgDark() no window().
package br.xt.app.departamento.produto;
import br.jasap.dao.DBInfo;
import java.io.Serializable;
public class DepartamentoProdutoBean implements Serializable {
public static String TABLE = "departamento.produto";
private Integer id_produto;
private String nome_produto;
private Double vl_produto;
private Integer qtd_produto;
private String obs_produto;
private String qs_produto;
@DBInfo(serial=true, pk=true)
public Integer getId_produto() { return id_produto; }
public void setId_produto(Integer id_produto) { this.id_produto = id_produto; }
public String getNome_produto() { return nome_produto; }
public void setNome_produto(String nome_produto) { this.nome_produto = nome_produto; }
public Double getVl_produto() { return vl_produto; }
public void setVl_produto(Double vl_produto) { this.vl_produto = vl_produto; }
public Integer getQtd_produto() { return qtd_produto; }
public void setQtd_produto(Integer qtd_produto) { this.qtd_produto = qtd_produto; }
public String getObs_produto() { return obs_produto; }
public void setObs_produto(String obs_produto) { this.obs_produto = obs_produto; }
public String getQs_produto() { return qs_produto; }
public void setQs_produto(String qs_produto) { this.qs_produto = qs_produto; }
public static String ID_PRODUTO = "id_produto";
public static String NOME_PRODUTO = "nome_produto";
public static String VL_PRODUTO = "vl_produto";
public static String QTD_PRODUTO = "qtd_produto";
public static String OBS_PRODUTO = "obs_produto";
public static String QS_PRODUTO = "qs_produto";
}
Mudanças nesta sequência: 1 campo, 1 par getter/setter e 1 constante. Nenhum import novo — o framework trata qs_produto como qualquer outra coluna do tipo String.
package br.xt.app.departamento.produto;
import br.jasap.core.AppManager;
import br.jasap.dao.Query;
import br.jasap.dao.SQL;
import br.jasap.util.JasapFunctions;
import br.jasap.util.JasapList;
import br.xt.AppsRootDAO;
public class DepartamentoProdutoDAO extends AppsRootDAO {
public DepartamentoProdutoDAO() {
}
public DepartamentoProdutoDAO(AppManager manager) {
setManager(manager);
setDataBase(manager.getDataBase());
}
public void daoList(JasapList list) throws Exception {
StringBuilder sql = new StringBuilder();
sql.append("select a.* from " + DepartamentoProdutoBean.TABLE + " a");
sql.append(" where 1=1 <where> <orderby>");
Query query = getDataBase().getQuery(sql.toString());
daoWhere(list.getFiltro());
select().executeList(query, list);
while (query.next()) {
DepartamentoProdutoBean bean = new DepartamentoProdutoBean();
query.populateBean(bean);
list.getList().add(bean);
}
query.release();
}
public void daoInsert(DepartamentoProdutoBean bean) throws Exception {
qs_produto(bean);
insert().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void daoSingle(DepartamentoProdutoBean bean) throws Exception {
select().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void daoUpdate(DepartamentoProdutoBean bean) throws Exception {
qs_produto(bean);
update().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void daoDelete(DepartamentoProdutoBean bean) throws Exception {
delete().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void qs_produto(DepartamentoProdutoBean bean) throws Exception {
bean.setQs_produto(JasapFunctions.searchString(
bean.getNome_produto() + " " +
bean.getObs_produto()
));
}
public void daoWhere(Object objWhere) throws Exception {
String where = "";
if (objWhere != null) {
DepartamentoProdutoWBean filtro = (DepartamentoProdutoWBean) objWhere;
if (filtro.getQs_produto() != null && !JasapFunctions.equals(filtro.getQs_produto(), "")) {
String[] cols = { SQL.column(DepartamentoProdutoBean.QS_PRODUTO) };
where = where + qsWhere(filtro.getQs_produto(), cols);
}
filtro.setWhere(where);
}
}
}
Mudanças nesta sequência: 2 imports novos (SQL, JasapFunctions), 1 marcador <where> no SQL do daoList, 1 chamada de daoWhere, chamadas de qs_produto(bean) no daoInsert e daoUpdate, e os 2 métodos novos no fim (qs_produto e daoWhere).
package br.xt.app.departamento.produto;
import br.jasap.core.Effect;
import br.jasap.effect.Response;
import br.jasap.gui.JasapPage;
import br.jasap.gui.ListColumn;
import br.jasap.gui.ListLine;
import br.jasap.gui.ListView;
import br.jasap.gui.Bar;
import br.jasap.gui.Button;
import br.jasap.gui.Table;
import br.jasap.gui.Toast;
import br.jasap.gui.form.Text;
import br.jasap.util.JasapFunctions;
import br.jasap.util.Js;
import br.jasap.util.ModalConfig;
import br.jasap.util.exceptions.SQLConstraintException;
import br.xt.acore.view.IconButton;
import br.xt.acore.view.XtPage;
public class DepartamentoProdutoList extends DepartamentoProdutoAction {
// ... execute() e render() (sem alteração)
public Table window() throws Exception {
Table w = new Table(getManager()).setSize("100%", "100%");
w.rowC("99%", JasapPage.DIV_WSPACE, lView());
w.rowC("1%", null, ui().line());
w.rowC("1%", null, qs_produto());
w.rowC("1%", JasapPage.DIV_BOTTOM, br());
w.rowC("1%", null, ui().line());
return w;
}
protected Text qs_produto = null;
public String qs_produto() throws Exception {
qs_produto = ui().text(DepartamentoProdutoBean.QS_PRODUTO)
.setLabel("Consulta")
.setStyle("width:400;height:30px;font-size:14px")
.setMaxlength(300)
.setValue(getFiltro().getQs_produto())
.setOnkeyup(Js.pressEnter(
link(QuickSearch.class)
.putScript(DepartamentoProdutoBean.QS_PRODUTO, Js.SELF_VALUE)
.ajax()));
Table aux = new Table(getManager(), "100%", "45")
.setStyle(ui().bgDark())
.rowC()
.setAlign(Table.ALIGN_CENTER)
.setVerticalAlign(Table.ALIGN_MIDDLE)
.setContent(qs_produto.toHtml() + "<span class=\"glyphicon glyphicon-search xt-ico-white\"></span>")
.table();
return aux.toHtml();
}
// ... br(), lView(), getFiltro() (sem alteração)
public static class Sort extends DepartamentoProdutoList {
@Override
public Effect execute() throws Exception {
update(lView().getDIV_HEADER(), lView().getHeader());
update(lView().getDIV_BODY(), lView().getBody());
update(lView().getDIV_NAVIGATE(), lView().getNavForm());
return new Response();
}
}
public static class QuickSearch extends DepartamentoProdutoList {
@Override
public Effect execute() throws Exception {
getFiltro().setQs_produto(getInput().getString(DepartamentoProdutoBean.QS_PRODUTO));
update(lView().getDIV_BODY(), lView().getBody());
update(lView().getDIV_NAVIGATE(), lView().getNavForm());
eval(Js.setFocusTo(DepartamentoProdutoBean.QS_PRODUTO));
return new Response();
}
}
// ... DeleteFromList (sem alteração)
public static final String LIST = ROOT.concat("__LIST/");
public static final String FILTRO = LIST.concat("__FILTRO");
public static final String CONFIRM_LIST = LIST.concat("__CONFIRM_LIST");
}
Mudanças nesta sequência: 1 import (Text), 1 linha nova no window() (a barra de busca), o método qs_produto() com a barra visual, e a inner class QuickSearch.
package br.xt.app.departamento;
import br.xt.app.departamento.produto.DepartamentoProdutoForm;
import br.xt.app.departamento.produto.DepartamentoProdutoList;
import br.xt.app.painel.PnlManager;
public class DepartamentoManager extends PnlManager {
public static final String F_ACESSO_MODULO = "XT.PAINEL_CONTROLE.ACESSO_MODULO.DEPARTAMENTO";
@Override
public void config() throws Exception {
regFun("PAINEL DE CONTROLE", "Acesso ao Módulo", "DEPARTAMENTO", F_ACESSO_MODULO);
regAction(DepartamentoHome.class);
regAction(DepartamentoHome.Title.class);
regAction(DepartamentoHome.MenuItem.class);
regAction(DepartamentoHome.MenuInicial.class);
regAction(DepartamentoProdutoList.class);
regAction(DepartamentoProdutoList.Sort.class);
regAction(DepartamentoProdutoList.QuickSearch.class);
regAction(DepartamentoProdutoList.DeleteFromList.class);
regAction(DepartamentoProdutoForm.class);
regAction(DepartamentoProdutoForm.ShowInsert.class);
regAction(DepartamentoProdutoForm.ShowUpdate.class);
regAction(DepartamentoProdutoForm.Insert.class);
regAction(DepartamentoProdutoForm.Update.class);
regAction(DepartamentoProdutoForm.Cancelar.class);
}
}
Mudança nesta sequência: 1 linha regAction nova. Nenhum import novo — DepartamentoProdutoList já era importado.
qs_produto no banco novo
ALTER TABLE departamento.produto ADD COLUMN qs_produto text;
CREATE EXTENSION IF NOT EXISTS unaccent;
UPDATE departamento.produto
SET qs_produto = lower(unaccent(coalesce(nome_produto, '') || ' ' || coalesce(obs_produto, '')));
Três comandos no PostgreSQL:
ALTER TABLE — cria a coluna qs_produto do tipo text. Obrigatório: sem essa coluna, o primeiro INSERT ou UPDATE de produto vai dar erro column "qs_produto" does not exist. O framework Jasap inclui automaticamente todos os campos do Bean no SQL.CREATE EXTENSION unaccent — habilita a extensão do PostgreSQL que remove acentos. Necessária pro UPDATE abaixo. Roda só uma vez por banco.UPDATE — popula qs_produto nos produtos que já existem antes da feature entrar. Opcional: produtos novos serão populados automaticamente pelo método qs_produto(bean) no DAO. Sem rodar este UPDATE, os produtos antigos não vão aparecer na busca até serem editados.O UPDATE faz o equivalente Java do JasapFunctions.searchString(): lower + unaccent + concatenação. Resultado: "Açúcar Refinado 1kg" vira "acucar refinado 1kg".
Buscar com normalização exige 5 partes coordenadas. Se qualquer uma faltar, ou a busca não funciona, ou o app quebra:
| Peça | Onde | Papel |
|---|---|---|
Coluna qs_produto | banco (PostgreSQL) | Guarda o texto pesquisável já normalizado (lower + sem acento) |
Campo qs_produto | Bean | Mapeia a coluna do banco — getter/setter + constante |
| Normalização | DAO (qs_produto, daoWhere) | Monta o texto normalizado no INSERT/UPDATE; filtra com qsWhere no SELECT |
Inner class QuickSearch | List | Recebe o texto digitado, salva no filtro, atualiza body + navegação |
Barra ui().bgDark() | List (window(), qs_produto()) | O input visual com glyphicon de lupa |
O fluxo completo: usuário digita → onkeyup dispara QuickSearch via AJAX → texto vai pro filtro na sessão → lView() chama daoList → daoWhere monta o WHERE com qsWhere → SQL retorna só os produtos que casam → body da lista atualiza.
qs_produto no banco
Por que uma coluna dedicada? Por que não buscar direto em nome_produto e obs_produto?
Duas razões:
nome_produto LIKE '%x%' OR obs_produto LIKE '%x%') escala mal. Com uma coluna pré-normalizada, o WHERE vira um único LIKE simples.açúcar ou acucar ou AÇÚCAR — todos têm que achar o mesmo registro. Comparar texto bruto com acentos e caixa não funciona. Pré-normalizar uma vez na escrita evita normalizar a cada leitura.O conteúdo da coluna é a concatenação dos campos pesquisáveis, tudo em lowercase e sem acento:
nome_produto | obs_produto | qs_produto |
|---|---|---|
| Açúcar Refinado 1kg | cristal fino | acucar refinado 1kg cristal fino |
| Café Torrado 500g | torra média | cafe torrado 500g torra media |
qs_produto no Bean
private String qs_produto;
public String getQs_produto() { return qs_produto; }
public void setQs_produto(String qs_produto) { this.qs_produto = qs_produto; }
public static String QS_PRODUTO = "qs_produto";
Padrão idêntico aos outros campos do Bean: declaração privada, getter, setter, e a constante pública com o nome da coluna no banco. Sem nada especial.
Por que NÃO usar @DBInfo(transient=true)? Justamente porque queremos que o framework persista esse campo. O qs_produto tem que ir pro INSERT e pro UPDATE automaticamente. Deixar como campo normal é o que faz isso acontecer.
A constante QS_PRODUTO é usada em três lugares: no DAO (SQL.column(QS_PRODUTO)), na List (input HTML) e na inner class QuickSearch (leitura do input). Constante centraliza o nome — se mudar a coluna no banco, muda em um lugar só.
qs_produto + daoWhere)
O DAO ganha duas responsabilidades: preencher o qs_produto na escrita e filtrar por ele na leitura.
qs_produto(bean)public void qs_produto(DepartamentoProdutoBean bean) throws Exception {
bean.setQs_produto(JasapFunctions.searchString(
bean.getNome_produto() + " " +
bean.getObs_produto()
));
}
JasapFunctions.searchString() faz o trabalho pesado: recebe a string concatenada, devolve em lowercase e sem acento. É o equivalente Java do lower(unaccent(...)) do PostgreSQL.
Esse método é chamado no daoInsert e daoUpdate, antes do execute. Resultado: toda escrita atualiza o qs_produto automaticamente, sem o programador ter que lembrar.
daoWherepublic void daoWhere(Object objWhere) throws Exception {
String where = "";
if (objWhere != null) {
DepartamentoProdutoWBean filtro = (DepartamentoProdutoWBean) objWhere;
if (filtro.getQs_produto() != null && !JasapFunctions.equals(filtro.getQs_produto(), "")) {
String[] cols = { SQL.column(DepartamentoProdutoBean.QS_PRODUTO) };
where = where + qsWhere(filtro.getQs_produto(), cols);
}
filtro.setWhere(where);
}
}
Decompondo:
filtro.getQs_produto() — texto que o usuário digitou (já guardado no WBean pela inner class QuickSearch).qsWhere(texto, cols) — método herdado do JasapDAO. Quebra o texto em palavras, normaliza cada uma e gera o SQL (qs_produto LIKE '%palavra1%' AND qs_produto LIKE '%palavra2%' ...). Suporta busca multi-palavra.filtro.setWhere(where) — grava o WHERE final no WBean. O placeholder <where> no SQL do daoList é substituído por esse texto na hora da execução.daoListsql.append(" where 1=1 <where> <orderby>");
// ...
daoWhere(list.getFiltro());
select().executeList(query, list);
Duas adições no daoList: o placeholder <where> no SQL (junto com 1=1 que vira o "ponto âncora" pra concatenação dos AND), e a chamada daoWhere(list.getFiltro()) antes do executeList pra montar o WHERE.
QuickSearch
public static class QuickSearch extends DepartamentoProdutoList {
@Override
public Effect execute() throws Exception {
getFiltro().setQs_produto(getInput().getString(DepartamentoProdutoBean.QS_PRODUTO));
update(lView().getDIV_BODY(), lView().getBody());
update(lView().getDIV_NAVIGATE(), lView().getNavForm());
eval(Js.setFocusTo(DepartamentoProdutoBean.QS_PRODUTO));
return new Response();
}
}
A action chamada a cada tecla digitada na barra de busca. 4 linhas de execução:
getFiltro().setQs_produto(getInput().getString(...)) — lê o texto digitado (vem do onkeyup da barra) e grava no WBean. O WBean fica na sessão, então o filtro persiste entre cliques na lista, ordenação, abertura de form etc.update(getDIV_BODY(), getBody()) — re-renderiza só as linhas da tabela. Como lView() chama daoList, e daoList chama daoWhere com o filtro recém-atualizado, o SQL volta com a lista filtrada.update(getDIV_NAVIGATE(), getNavForm()) — atualiza a área de paginação. Sem isso, contagem total fica errada quando paginação for adicionada depois.eval(Js.setFocusTo(QS_PRODUTO)) — devolve o foco pro campo de busca. Sem isso, o usuário perderia o foco a cada tecla e a barra ficaria inutilizável.ui().bgDark()
protected Text qs_produto = null;
public String qs_produto() throws Exception {
qs_produto = ui().text(DepartamentoProdutoBean.QS_PRODUTO)
.setLabel("Consulta")
.setStyle("width:400;height:30px;font-size:14px")
.setMaxlength(300)
.setValue(getFiltro().getQs_produto())
.setOnkeyup(Js.pressEnter(
link(QuickSearch.class)
.putScript(DepartamentoProdutoBean.QS_PRODUTO, Js.SELF_VALUE)
.ajax()));
Table aux = new Table(getManager(), "100%", "45")
.setStyle(ui().bgDark())
.rowC()
.setAlign(Table.ALIGN_CENTER)
.setVerticalAlign(Table.ALIGN_MIDDLE)
.setContent(qs_produto.toHtml() + "<span class=\"glyphicon glyphicon-search xt-ico-white\"></span>")
.table();
return aux.toHtml();
}
O método monta o input + lupa dentro de uma barra horizontal escura. Decompondo:
ui().text(QS_PRODUTO) — cria um input HTML com o nome do parâmetro igual à constante QS_PRODUTO. Esse nome é o que getInput().getString(QS_PRODUTO) lê do outro lado..setValue(getFiltro().getQs_produto()) — preenche com o texto que estava na sessão. Sem isso, ao fechar e reabrir um form, a barra apareceria vazia mesmo com filtro ativo..setOnkeyup(Js.pressEnter(...)) — dispara a action QuickSearch ao pressionar Enter. Js.SELF_VALUE envia o conteúdo atual do próprio input.ui().bgDark() — CSS que dá o fundo escuro (cinza chumbo) na barra, padrão visual de barras de filtro no XT.glyphicon glyphicon-search — ícone de lupa do Bootstrap, branco (xt-ico-white), à direita do input.A barra é incluída no window() via w.rowC("1%", null, qs_produto()) — uma linha horizontal entre a lista e o rodapé.
regAction — registrar no Manager
regAction(DepartamentoProdutoList.QuickSearch.class);
Uma linha no DepartamentoManager.config(). Sem isso, o framework não conhece a inner class QuickSearch e o onkeyup da barra retorna "setor não encontrado".
A linha fica logo depois do regAction(Sort.class), antes do DeleteFromList. Organização lógica: List → Sort → QuickSearch → DeleteFromList.
vl_produto e qtd_produto ficam de fora
Repare que o método qs_produto(bean) só concatena nome e observação:
bean.setQs_produto(JasapFunctions.searchString(
bean.getNome_produto() + " " +
bean.getObs_produto()
));
Valor e quantidade ficam de fora de propósito. Buscar número como texto não funciona bem em pt-BR:
| Você digita | Coluna tem | Encontra? |
|---|---|---|
5.2 | 5.2 | sim |
5,2 | 5.2 | não |
1000 | 1000 (mas em vários produtos) | casa demais — falso positivo |
Em Java, Double.toString() sempre usa ponto como separador decimal. Mas o usuário brasileiro digita vírgula. Se incluíssemos valor no qs_produto, a busca por preço quebraria pra todo mundo.
O Jasap resolve isso com outra feature: filtros de range (vl_ini / vl_fim) — campos separados pra "de R$ X até R$ Y". Esse é assunto pro próximo episódio "Filtros avançados". A barra de busca rápida é pra texto livre — nome, observação, descrição.
O QuickSearch isolado faz uma coisa só: filtrar a lista por texto digitado. Algumas features parecem fazer parte mas são separadas:
| Feature | Faz parte? | Porquê |
|---|---|---|
Trocar setOnCloseURL → url(Sort.class) | Não | Otimização: faz a barra preservar o filtro ao fechar form. Mas é uma linha trocada na List, episódio próprio se quiser destacar. |
Filtro de range numérico (vl_ini/vl_fim) | Não | Filtros avançados — feature separada (próximo episódio) |
Reset de página (resetPage) | Não | Só faz sentido com paginação ativa — episódio de paginação |
| Tabs de status | Não | Filtro por status — feature separada |
O QuickSearch que implementamos aqui é a base: 5 peças coordenadas, busca fuzzy funcionando. As features acima estendem o filtro de outras formas, mas não dependem do QuickSearch e o QuickSearch não depende delas.
| Arquivo / Recurso | Tipo | Edição |
|---|---|---|
| SQL | novo | Coluna qs_produto text + extensão unaccent + UPDATE de backfill |
| DepartamentoProdutoBean | editar | Campo + getter/setter + constante QS_PRODUTO |
| DepartamentoProdutoDAO | editar | 2 imports, <where> no SQL, chamadas qs_produto(bean) no insert/update, métodos qs_produto e daoWhere |
| DepartamentoProdutoList | editar | 1 import (Text), linha no window(), método qs_produto(), inner class QuickSearch |
| DepartamentoManager | editar | 1 regAction(QuickSearch.class) |
4 arquivos editados, 1 alteração no banco, nenhum arquivo Java novo. Resultado: barra de busca rápida funcionando com normalização (case-insensitive, sem acento), suporte a múltiplas palavras, e filtro persistente na sessão.