Tabs de Status — Ativos / Inativos / Arquivados / Todos editar arquivo

A lista de produtos já suporta ordenação, busca rápida e paginação — mas ainda mostra tudo misturado. Produtos arquivados aparecem junto com ativos, inativos se perdem no meio. As Tabs de Status separam a visualização: cada aba filtra a lista por um status específico, e a aba "Todos" mantém a visão completa pra quando precisar.

0:00 / 0:00

Agora mergulhamos no código — 6 arquivos tocados, as mudanças linha por linha.

0:00 / 0:00

Esta etapa toca 6 arquivos: 1 novo (banco, a coluna st_produto) e 5 editados (Bean, DAO, Form, List, Manager). A List é onde mora a maior mudança — TabStrip, 4 inner classes de aba, e a infra resetPage() que fecha o débito técnico da Paginação anterior.

CÓDIGO COMPLETO — departamento.produto (SQL)
ALTER TABLE departamento.produto ADD COLUMN st_produto integer;
UPDATE departamento.produto SET st_produto = 1;

Duas instruções: ALTER cria a coluna nullable; UPDATE faz o backfill marcando todos os produtos existentes como Ativo (1) pra não sumirem da aba "Ativos" no primeiro render.

CÓDIGO COMPLETO — DepartamentoProdutoBean
package br.xt.app.departamento.produto;

import br.jasap.dao.DBInfo;
import br.jasap.util.DomainValue;
import br.jasap.util.JasapList;
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;
    private Integer st_produto;
    private Integer insert_chk;

    // ... getters/setters de id/nome/vl/qtd/obs/qs (sem alteração)

    public Integer getSt_produto() { return st_produto; }
    public void setSt_produto(Integer st_produto) { this.st_produto = st_produto; }

    // ... getter/setter de insert_chk (sem alteração)

    public static class DomStatus {
        public static final Integer ATIVO     = 1;
        public static final Integer INATIVO   = 2;
        public static final Integer ARQUIVADO = 3;

        public static JasapList domain() {
            JasapList list = new JasapList();
            list.getList().add(new DomainValue(ATIVO,     "Ativo"));
            list.getList().add(new DomainValue(INATIVO,   "Inativo"));
            list.getList().add(new DomainValue(ARQUIVADO, "Arquivado"));
            return list;
        }
    }

    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";
    public static String ST_PRODUTO   = "st_produto";
    public static String INSERT_CHK   = "insert_chk";

}

Mudanças: campo st_produto + getter/setter, inner class DomStatus (constantes + domain()), constante ST_PRODUTO, dois imports novos (DomainValue, JasapList).

CÓDIGO COMPLETO — DepartamentoProdutoDAO
public void daoWhere(Object objWhere) throws Exception {
    String where = "";
    if (objWhere != null) {
        DepartamentoProdutoWBean filtro = (DepartamentoProdutoWBean) objWhere;

        if (filtro.getSt_produto() != null)
            where = SQL.and(where, SQL.equals(SQL.column(DepartamentoProdutoBean.ST_PRODUTO), SQL.value(filtro.getSt_produto())));

        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ça: 2 linhas antes do filtro de qs_produto. Se filtro.getSt_produto() é null, não adiciona cláusula — é o que permite a aba "Todos" mostrar tudo sem filtro de status.

CÓDIGO COMPLETO — DepartamentoProdutoForm
import br.jasap.gui.form.Radio;

public class DepartamentoProdutoForm extends DepartamentoProdutoAction {

    // ... execute(), ShowInsert, Insert, ShowUpdate, Update, Cancelar, render(), window(), br() (sem alteração)

    public String form() throws Exception {
        Form frm = ui().form();
        frm.addHidden(DepartamentoProdutoBean.ID_PRODUTO, proBean().getId_produto());

        frm.line().add(nome_produto(), "150");
        frm.line().add(vl_produto(),   "150");
        frm.line().add(qtd_produto(),  "150");
        frm.line().add(st_produto(),   "150");
        frm.line().add(obs_produto(),  "150");
        frm.line().add(insert_chk(),   "150");

        return frm.getTable().toHtml();
    }

    // ... nome_produto(), vl_produto(), qtd_produto() (sem alteração)

    protected Radio st_produto = null;
    public Radio st_produto() throws Exception {
        if (st_produto == null) {
            Integer valorAtual = proBean().getSt_produto() != null ? proBean().getSt_produto() : DepartamentoProdutoBean.DomStatus.ATIVO;
            st_produto = new Radio(getManager(), DepartamentoProdutoBean.ST_PRODUTO)
                    .setLabel("Status")
                    .setValue(DepartamentoProdutoBean.DomStatus.domain())
                    .setRequired(true);
            st_produto.getValue().setSelected(valorAtual);
        }
        return st_produto;
    }

    // ... obs_produto(), insert_chk(), FORM (sem alteração)

}

Mudanças: import Radio, método st_produto(), e plugar na form() entre qtd_produto e obs_produto. Default ATIVO no insert (quando proBean().getSt_produto() ainda é null).

CÓDIGO COMPLETO — DepartamentoProdutoList
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.TabStrip;
import br.jasap.gui.Table;
import br.jasap.gui.Toast;
import br.jasap.gui.form.Text;
// ... demais imports (sem alteração)

public class DepartamentoProdutoList extends DepartamentoProdutoAction {

    @Override
    public Effect execute() throws Exception {
        if (getSession().getObject(TBL) == null) {
            getSession().addStr(TBL, TBL_ATIVOS);
            getFiltro().setSt_produto(DepartamentoProdutoBean.DomStatus.ATIVO);
        }
        render();
        return new Response();
    }

    // ... render() (sem alteração)

    public Table window() throws Exception {
        Table w = new Table(getManager()).setSize("100%", "100%");
        w.rowC("1%",  JasapPage.DIV_TABSTRIP, tbs());
        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;
    }

    private TabStrip tbs = null;
    public TabStrip tbs() throws Exception {
        if (tbs == null) {
            tbs = ui().tabStrip().setSelectedKey(getSession().getString(TBL, getInput()));
            tbs.createTab(TBL_ATIVOS,     "Ativos")    .setOnclick(link(TabAtivos.class).ajax());
            tbs.createTab(TBL_INATIVOS,   "Inativos")  .setOnclick(link(TabInativos.class).ajax());
            tbs.createTab(TBL_ARQUIVADOS, "Arquivados").setOnclick(link(TabArquivados.class).ajax());
            tbs.createTab(TBL_TODOS,      "Todos")     .setOnclick(link(TabTodos.class).ajax());
        }
        return tbs;
    }

    public void resetPage() {
        getSession().addInt(RESET_PAGE, 1);
    }

    // ... qs_produto(), br() (sem alteração)

    private ListView lv = null;
    public ListView lView() throws Exception {
        if (lv == null) {
            lv = ui().lView();

            // ... setSortAct, setPageAction, setPage, setOrderBy, setSort, ajax() (sem alteração)

            lv.setPageSize(50);

            if (getSession().isSet(RESET_PAGE)) {
                lv.setPage(1);
                getSession().remove(RESET_PAGE);
            }

            lv.setFiltro(getFiltro());
            getFactory().departamento().proModel().daoList(lv.getData());

            // ... colunas e while (sem alteração)
        }
        return lv;
    }

    // ... getFiltro() (sem alteração)

    public static class Sort extends DepartamentoProdutoList { /* sem alteração */ }

    public static class QuickSearch extends DepartamentoProdutoList {
        @Override
        public Effect execute() throws Exception {
            resetPage();
            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();
        }
    }

    public static class TabAtivos extends DepartamentoProdutoList {
        @Override
        public Effect execute() throws Exception {
            resetPage();
            getSession().addStr(TBL, TBL_ATIVOS);
            getFiltro().setSt_produto(DepartamentoProdutoBean.DomStatus.ATIVO);
            render();
            return new Response();
        }
    }

    public static class TabInativos extends DepartamentoProdutoList {
        @Override
        public Effect execute() throws Exception {
            resetPage();
            getSession().addStr(TBL, TBL_INATIVOS);
            getFiltro().setSt_produto(DepartamentoProdutoBean.DomStatus.INATIVO);
            render();
            return new Response();
        }
    }

    public static class TabArquivados extends DepartamentoProdutoList {
        @Override
        public Effect execute() throws Exception {
            resetPage();
            getSession().addStr(TBL, TBL_ARQUIVADOS);
            getFiltro().setSt_produto(DepartamentoProdutoBean.DomStatus.ARQUIVADO);
            render();
            return new Response();
        }
    }

    public static class TabTodos extends DepartamentoProdutoList {
        @Override
        public Effect execute() throws Exception {
            resetPage();
            getSession().addStr(TBL, TBL_TODOS);
            getFiltro().setSt_produto(null);
            render();
            return new Response();
        }
    }

    public static class DeleteFromList extends DepartamentoProdutoList { /* sem alteração */ }

    public static final String LIST           = ROOT.concat("__LIST/");
    public static final String FILTRO         = LIST.concat("__FILTRO");
    public static final String RESET_PAGE     = LIST.concat("__RESET_PAGE");
    public static final String TBL            = LIST.concat("__TBL");
    public static final String TBL_ATIVOS     = "__ATIVOS";
    public static final String TBL_INATIVOS   = "__INATIVOS";
    public static final String TBL_ARQUIVADOS = "__ARQUIVADOS";
    public static final String TBL_TODOS      = "__TODOS";
    public static final String CONFIRM_LIST   = LIST.concat("__CONFIRM_LIST");

}

Mudanças principais: import TabStrip, default ATIVO no execute(), row do tabstrip no window(), método tbs(), método resetPage(), bloco consumer no lView(), chamada de resetPage() no QuickSearch, 4 inner classes TabAtivos/TabInativos/TabArquivados/TabTodos, e 6 constantes novas (TBL, TBL_ATIVOS/INATIVOS/ARQUIVADOS/TODOS, RESET_PAGE).

CÓDIGO COMPLETO — DepartamentoManager
        regAction(DepartamentoProdutoList.class);
        regAction(DepartamentoProdutoList.Sort.class);
        regAction(DepartamentoProdutoList.QuickSearch.class);
        regAction(DepartamentoProdutoList.TabAtivos.class);
        regAction(DepartamentoProdutoList.TabInativos.class);
        regAction(DepartamentoProdutoList.TabArquivados.class);
        regAction(DepartamentoProdutoList.TabTodos.class);
        regAction(DepartamentoProdutoList.DeleteFromList.class);

Mudança: 4 regAction novos — um por Tab inner class. Cada action que vai receber requisição precisa estar registrada no Manager; sem isso, clicar na aba retorna erro "action não encontrada".

O mecanismo — 3 pilares

Tabs de Status parece uma feature só, mas são 3 mecanismos independentes que se encontram na UI:

PilarOnde moraPapel
Filtro por statusBanco + Bean + DAONova coluna st_produto, campo no Bean, cláusula WHERE st_produto = ? no DAO quando o filtro tá setado
TabStrip + 4 Tab actionsList + ManagerTabStrip renderiza as abas, cada Tab*.execute() grava o status na sessão e re-renderiza
resetPage()List (infra)Padrão session flag + consumer — trocar de aba volta pra página 1 (senão o usuário na página 3 dos Ativos vai pro fantasma da página 3 dos Inativos)

Pode existir 1 sem os outros 2: filtro sozinho funcionaria com URL params, TabStrip sozinho sem filtro seria só decoração, resetPage sozinho é infra sem uso. Juntos, viram a feature.

Mudança 1 — Banco: coluna st_produto + backfill
ALTER TABLE departamento.produto ADD COLUMN st_produto integer;
UPDATE departamento.produto SET st_produto = 1;

Duas instruções, uma única vez (já rodou — 75 produtos marcados como Ativo). A coluna é integer nullable — nullable porque registros antigos poderiam existir sem status definido. O UPDATE backfill é que evita isso: marca todos com st_produto = 1 (ATIVO) pra não sumirem da primeira aba renderizada.

Por que nullable e não NOT NULL?

Três motivos práticos: (1) ALTER TABLE ... ADD COLUMN ... NOT NULL sem DEFAULT falha se a tabela não tá vazia — precisa de 2 instruções separadas; (2) em produção, colunas nullable são mais tolerantes a estado legado; (3) a aba "Todos" filtra com st_produto IS NOT NULL ou sem filtro — nullable permite testes de regressão mais fáceis.

Por que 1 no backfill e não outro valor?

1 é o código numérico de DomStatus.ATIVO no Bean. O domínio é fechado (1=Ativo, 2=Inativo, 3=Arquivado). Usar 1 aqui e ATIVO no Java garante que os dois lados convergem — se o enum mudar, o backfill roda de novo alinhado.

Mudança 2 — Bean: st_produto + DomStatus
private Integer st_produto;

public Integer getSt_produto() { return st_produto; }
public void setSt_produto(Integer st_produto) { this.st_produto = st_produto; }

public static class DomStatus {
    public static final Integer ATIVO     = 1;
    public static final Integer INATIVO   = 2;
    public static final Integer ARQUIVADO = 3;

    public static JasapList domain() {
        JasapList list = new JasapList();
        list.getList().add(new DomainValue(ATIVO,     "Ativo"));
        list.getList().add(new DomainValue(INATIVO,   "Inativo"));
        list.getList().add(new DomainValue(ARQUIVADO, "Arquivado"));
        return list;
    }
}

public static String ST_PRODUTO = "st_produto";

O Bean segue a convenção: campo privado + getter/setter padrão + constante pública com o nome da coluna. Nada de @DBInfo no st_produto — não é PK nem serial, o framework trata como coluna comum.

DomStatus como inner class

Domínio fechado (3 valores possíveis) encapsulado no Bean dono do campo. Padrão replicado de LabProdutoBean.DomStatus. Três razões pra ficar aqui e não em br.xt.util.DomStatus global:

  • Coesão: quem altera os valores possíveis de st_produto está mexendo no Bean de Produto
  • Escopo limitado: outros Beans podem ter DomStatus diferentes (ex: LabPessoaBean.DomStatus tem 5 valores) — não faz sentido globalizar
  • Navegação: DepartamentoProdutoBean.DomStatus.ATIVO lê como "o status ativo de produto" — inequívoco

domain() factory

Método estático que monta a JasapList de domínio para componentes visuais (Radio, Combo). Cada invocação cria uma lista nova — não compartilha estado com outras invocações. É o idioma Jasap: o componente visual recebe a lista e gerencia qual DomainValue está selecionado.

Imports novos

  • br.jasap.util.DomainValue — par chave/rótulo que popula a JasapList
  • br.jasap.util.JasapList — lista genérica usada por componentes visuais do framework
Mudança 3 — Form: Radio st_produto()
protected Radio st_produto = null;
public Radio st_produto() throws Exception {
    if (st_produto == null) {
        Integer valorAtual = proBean().getSt_produto() != null ? proBean().getSt_produto() : DepartamentoProdutoBean.DomStatus.ATIVO;
        st_produto = new Radio(getManager(), DepartamentoProdutoBean.ST_PRODUTO)
                .setLabel("Status")
                .setValue(DepartamentoProdutoBean.DomStatus.domain())
                .setRequired(true);
        st_produto.getValue().setSelected(valorAtual);
    }
    return st_produto;
}

Sem esse método, o usuário não teria como mudar o status de um produto — ele ficaria preso no valor que tá no banco. As tabs viram read-only depois do primeiro insert.

Radio vs Combo

3 opções é o limite psicológico pra Radio: todas visíveis sem scroll, escolha imediata. Acima de 5, Combo economiza espaço. A convenção do XT é Radio pra domínios ≤ 5 valores — confere com LabProdutoForm.st_pro().

Default ATIVO no insert

Integer valorAtual = proBean().getSt_produto() != null ? proBean().getSt_produto() : DepartamentoProdutoBean.DomStatus.ATIVO;

Lógica ternária: se o Bean já tem status (modo edição), usa o valor existente; se é null (modo insert, primeiro render), usa ATIVO. Isso garante que o Radio sempre tem uma opção pré-selecionada — sem isso, o usuário precisaria clicar mesmo pra "manter" o valor.

setRequired(true)

Valida no submit — se por algum motivo o Radio chegar no servidor sem seleção (ex: usuário abriu o DevTools e removeu o atributo checked), o framework barra. Belt + suspenders com o default.

Posicionamento na form()

Depois de qtd_produto, antes de obs_produto. Ordem: campos básicos → quantitativos → status → texto livre. Status é "coluna de controle" — não é dado do produto, é classificação.

Mudança 4 — DAO: filtro st_produto no daoWhere
if (filtro.getSt_produto() != null)
    where = SQL.and(where, SQL.equals(SQL.column(DepartamentoProdutoBean.ST_PRODUTO), SQL.value(filtro.getSt_produto())));

Uma condicional, uma linha de where =. Posicionada antes do filtro de qs_produto — ordem arbitrária pro SQL, mas convencional: status primeiro, busca textual depois.

Os 4 helpers de SQL

  • SQL.and(where, clausula) — concatena com AND só se where não tá vazia (senão retorna a cláusula sem AND pendurado)
  • SQL.equals(coluna, valor) — monta "a.coluna = valor" com aspas corretas
  • SQL.column(nome) — prefixa com alias da tabela principal ("a.st_produto")
  • SQL.value(obj) — serializa o valor pro SQL com escape (string com aspas, número sem, null vira NULL)

Sem esses helpers, seria where += " AND a.st_produto = " + filtro.getSt_produto() — funcional, mas vulnerável a SQL injection e sem escape de aspas. O idioma Jasap é sempre via SQL.*.

null = sem filtro

O if (filtro.getSt_produto() != null) é o que permite a aba "Todos" funcionar: quando TabTodos.execute() roda getFiltro().setSt_produto(null), o daoWhere pula o bloco — SQL sai sem cláusula de status, retorna tudo.

Mudança 5 — List: TabStrip + 4 Tab actions
private TabStrip tbs = null;
public TabStrip tbs() throws Exception {
    if (tbs == null) {
        tbs = ui().tabStrip().setSelectedKey(getSession().getString(TBL, getInput()));
        tbs.createTab(TBL_ATIVOS,     "Ativos")    .setOnclick(link(TabAtivos.class).ajax());
        tbs.createTab(TBL_INATIVOS,   "Inativos")  .setOnclick(link(TabInativos.class).ajax());
        tbs.createTab(TBL_ARQUIVADOS, "Arquivados").setOnclick(link(TabArquivados.class).ajax());
        tbs.createTab(TBL_TODOS,      "Todos")     .setOnclick(link(TabTodos.class).ajax());
    }
    return tbs;
}

setSelectedKey + constante TBL

TBL é a chave de sessão que guarda qual aba tá selecionada no momento. setSelectedKey lê esse valor e o TabStrip marca a aba correspondente como ativa no HTML (classe CSS, estilo visual).

  • Sem setSelectedKey: todas as abas aparecem inativas — feature quebrada visualmente
  • Sem persistir em sessão: ao recarregar a tela, a aba selecionada é "esquecida" — volta pro default

createTab(key, label)

Primeiro argumento é a chave interna (TBL_ATIVOS etc) — precisa bater com o que setSelectedKey espera. Segundo é o rótulo visível. O setOnclick(link(TabX.class).ajax()) amarra o clique à action correspondente.

4 inner classes quase idênticas

public static class TabAtivos extends DepartamentoProdutoList {
    @Override
    public Effect execute() throws Exception {
        resetPage();
        getSession().addStr(TBL, TBL_ATIVOS);
        getFiltro().setSt_produto(DepartamentoProdutoBean.DomStatus.ATIVO);
        render();
        return new Response();
    }
}

Cada Tab faz a mesma coreografia, diferindo só no valor gravado:

  1. resetPage() — volta pra página 1 (Mudança 6 explica o mecanismo)
  2. getSession().addStr(TBL, TBL_*) — grava qual aba tá ativa (pro setSelectedKey do próximo render ler)
  3. getFiltro().setSt_produto(...) — seta o filtro que o daoWhere vai aplicar
  4. render() — re-renderiza a lista completa

Por que 4 classes separadas e não 1 parametrizada?

Idioma Jasap: cada regAction(Class) no Manager precisa ser uma classe nominal. Não existe regAction(Class, params). O framework usa o nome da classe como chave de roteamento no XML — 1 classe por endpoint.

Alternativa rejeitada: uma única TabChange que lê o alvo de um parâmetro TAB_KEY. Funcionaria, mas quebra o registro nominal — seria uma action com comportamentos diferentes dependendo do input, mais difícil de navegar no Manager e mais fácil de bugar.

Custo real: cada classe tem ~8 linhas. 4 × 8 = 32 linhas repetitivas. Para o benefício de clareza e idioma, é um custo aceitável.

TabTodos passa null

getFiltro().setSt_produto(null) — é o que desliga a cláusula de status no daoWhere (Mudança 4). A aba "Todos" não é um status especial; é a ausência de filtro de status.

Default ATIVO no execute() da List base

@Override
public Effect execute() throws Exception {
    if (getSession().getObject(TBL) == null) {
        getSession().addStr(TBL, TBL_ATIVOS);
        getFiltro().setSt_produto(DepartamentoProdutoBean.DomStatus.ATIVO);
    }
    render();
    return new Response();
}

Primeira visita à lista (sessão ainda sem TBL): seta ATIVO como default. Nas visitas subsequentes, a sessão já tem valor — o if não entra e respeita a escolha prévia do usuário.

Mudança 6 — List: infra resetPage() (fecha o débito da Paginação)

Quando a Paginação foi adicionada, ela resolveu o reset pra busca rápida via .putInteger(PAGE, 0) no onkeyup — uma solução pontual. Agora, com 4 Tab actions que também precisam resetar página, esse padrão não escala: cada uma teria que passar PAGE=0 no link do TabStrip. A solução genérica é um helper.

Os 3 elementos

// Método helper — qualquer action pode chamar
public void resetPage() {
    getSession().addInt(RESET_PAGE, 1);
}

// Constante pra chave de sessão
public static final String RESET_PAGE = LIST.concat("__RESET_PAGE");

// Bloco consumer dentro de lView(), logo depois de setPageSize
if (getSession().isSet(RESET_PAGE)) {
    lv.setPage(1);
    getSession().remove(RESET_PAGE);
}

O padrão é flag de sessão + consumer:

  1. resetPage() grava a flag — não muda nada imediatamente
  2. No próximo lView(), o consumer detecta a flag, força lv.setPage(1), remove a flag
  3. Efeito: qualquer action que chamar resetPage() antes do próximo render vai aterrissar na página 1

Chamado em 5 lugares

  • QuickSearch.execute() — substituiu o .putInteger(PAGE, 0) que morava no onkeyup
  • TabAtivos.execute(), TabInativos.execute(), TabArquivados.execute(), TabTodos.execute()

Todas as 5 actions seguem o mesmo idioma: resetPage() como primeira linha do execute(), depois o resto.

Por que essa infra não entrou na Paginação?

Na Paginação só o QuickSearch precisava resetar — 1 caller. Adicionar 3 peças de infra (método + constante + bloco) pra atender 1 caller é over-engineering. O .putInteger(PAGE, 0) no onkeyup resolvia com 1 linha.

Agora, em Tabs, são 5 callers. A matemática muda: ou o .putInteger(PAGE, 0) se espalha por 4 links do TabStrip + 1 no onkeyup, ou centraliza em 1 helper. O helper ganha.

Por que o Laboratório tem a mesma chamada mas não funciona?

Na referência do Lab (LabProdutoList), resetPage() existe e é chamado no QuickSearch, mas o bloco consumer não foi adicionado no lView(). Resultado: a flag RESET_PAGE é gravada na sessão e ninguém lê — código morto. Funciona "por acidente" porque o onkeyup também faz .putInteger(PAGE, 0).

Essa etapa (Tabs de Status) foi a primeira a implementar o padrão corretamente — helper + consumer + callers em sintonia. É o padrão de referência daqui pra frente.

Reaparecerá em Filtros avançados

Quando vier a feature de filtros (range de valor, período de data, etc), cada "Aplicar Filtros" / "Limpar Filtros" vai chamar resetPage(). Mesmo mecanismo, mais callers. O investimento em infra aqui paga-se lá.

Mudança 7 — Manager: 4 regAction
regAction(DepartamentoProdutoList.TabAtivos.class);
regAction(DepartamentoProdutoList.TabInativos.class);
regAction(DepartamentoProdutoList.TabArquivados.class);
regAction(DepartamentoProdutoList.TabTodos.class);

Cada action precisa ser registrada no Manager. regAction grava um XML interno que o framework consulta em cada requisição pra saber qual classe instanciar. Sem o registro, o link(TabAtivos.class).ajax() renderiza uma URL que, ao ser clicada, resulta em "action não encontrada".

Posicionamento: depois de QuickSearch e antes de DeleteFromList — a ordem não afeta funcionamento, mas segue o fluxo visual da classe (helpers de ação → tabs → delete).

O que NÃO faz parte das Tabs de Status

Três coisas aparecem naturalmente quando a gente pensa em "tabs de status", mas ficam fora desta etapa — viram pendência ou são rejeitadas por escopo:

ItemFaz parte?Por quê
Contador de registros por aba (ex: "Ativos (3)")NãoExige query de contagem extra por aba em cada render — custo não justifica na etapa mínima. Pode entrar depois como feature de conveniência
Filtros avançados (data, range de valor)NãoEtapa própria. Reusará o resetPage() que nasceu aqui
Método DomStatus.label(Integer)Não (removido)Gerado inicialmente junto com domain(), mas nenhum lugar do Departamento chama — deletado na revisão de escopo
Cascade de status (status do pai propaga pros filhos)NãoEspecífico de entidades com hierarquia — Produto não tem pai/filho de status

Sobre o DomStatus.label(): foi implementado na primeira versão seguindo o espelho do LabProdutoBean. A revisão de simplificação (rodada logo depois da implementação Java) identificou que nenhum caller existia no Departamento — dead code. Deletado. O label() só entra quando algum componente visual precisar traduzir Integer → String, o que não acontece nessa etapa.

Resumo — o que mudou
ArquivoTipoEdição
departamento.produto (banco)novoColuna st_produto integer + backfill UPDATE marcando todos como Ativo
DepartamentoProdutoBeaneditarCampo st_produto + getter/setter, inner class DomStatus (ATIVO/INATIVO/ARQUIVADO + domain()), constante ST_PRODUTO, 2 imports
DepartamentoProdutoDAOeditar1 bloco if com 1 linha de where = no daoWhere
DepartamentoProdutoFormeditarImport Radio, método st_produto(), plugado na form()
DepartamentoProdutoListeditarImport TabStrip, default ATIVO no execute(), row tabstrip no window(), método tbs(), método resetPage(), consumer em lView(), resetPage() no QuickSearch, 4 inner classes Tab*, 6 constantes novas
DepartamentoManagereditar4 regAction novos

1 arquivo novo (banco), 5 editados. A maior parte da complexidade vive na List. O padrão resetPage() estabelecido aqui vai reaparecer em Filtros avançados — foi investimento, não custo.