CRUD MASTER/DETAIL — Pessoa exibe os produtos que lhe pertencem 4 edits, 0 arquivos novos

0:00 / 0:00

No LinkBox, o filho (Produto) apontava pro pai (Pessoa) — o form do Produto tinha um campo "Responsável" com o nome da pessoa vinculada. Master/Detail é a visão inversa da mesma FK: agora é o form da Pessoa que exibe, em baixo, a lista de produtos que ela controla, com botões pra associar produto existente, criar produto novo já vinculado, e lixeirinha pra desvincular.

A FK fk_pessoa_produto já existe desde o LinkBox — nada muda no banco. Não tem tabela de associação nova, não é N-N. O que entra é só a leitura e manipulação dessa FK a partir do lado do pai. 4 arquivos editados, 0 arquivos novos: o ProdutoDAO ganha 2 métodos SQL, o PessoaForm é o grosso da feature (5 inner actions + produtoDetail() + divisão da window em master/detail), o ProdutoForm ganha um ajuste de 3 linhas pra pré-preencher a FK quando vier do detail, e o Manager registra as 5 actions novas.

CÓDIGO COMPLETO — DepartamentoProdutoDAO
0:00 / 0:00
// ... daoList, daoInsert, daoSingle, daoUpdate, daoDelete, qs_produto, daoWhere (sem alteração) ...

// Métodos novos:
public void daoLinkPessoa(Integer id_produto, Integer id_pessoa) throws Exception {
    String sql = "UPDATE " + DepartamentoProdutoBean.TABLE
            + " SET " + DepartamentoProdutoBean.FK_PESSOA_PRODUTO + "=" + SQL.value(id_pessoa)
            + " WHERE " + DepartamentoProdutoBean.ID_PRODUTO + "=" + SQL.value(id_produto);
    Query query = getDataBase().getQuery(sql);
    getDataBase().executeUpdate(query);
    query.release();
}

public void daoUnlinkPessoa(Integer id_produto) throws Exception {
    String sql = "UPDATE " + DepartamentoProdutoBean.TABLE
            + " SET " + DepartamentoProdutoBean.FK_PESSOA_PRODUTO + "=NULL"
            + " WHERE " + DepartamentoProdutoBean.ID_PRODUTO + "=" + SQL.value(id_produto);
    Query query = getDataBase().getQuery(sql);
    getDataBase().executeUpdate(query);
    query.release();
}

Mudanças neste arquivo: 2 métodos novos. Um UPDATE com FK preenchida (linkar), outro com FK=NULL (desvincular). Nenhuma tabela nova, nenhum import novo — Query, SQL e DepartamentoProdutoBean já eram usados.

CÓDIGO COMPLETO — DepartamentoPessoaForm (apenas blocos novos)
0:00 / 0:00
// Imports novos:
import br.jasap.gui.ListColumn;
import br.jasap.gui.ListLine;
import br.jasap.gui.ListView;
import br.jasap.gui.form.LinkBox;
import br.jasap.util.ModalConfig;
import br.xt.app.departamento.produto.DepartamentoProdutoBean;
import br.xt.app.departamento.produto.DepartamentoProdutoForm;
import br.xt.app.departamento.produto.DepartamentoProdutoSelect;
import br.xt.app.departamento.produto.DepartamentoProdutoWBean;

// ... ShowInsert, Insert, ShowUpdate, Update, Cancelar, Delete (sem alteração) ...

// 5 inner classes novas:
public static class AddExistingProduto extends DepartamentoPessoaForm {
    public AddExistingProduto() { super.getFilters().add(new TransactionFilter()); }
    @Override
    public Effect execute() throws Exception {
        Integer id_produto = getInput().getInteger(DepartamentoProdutoBean.ID_PRODUTO);
        getFactory().departamento().proModel().daoLinkPessoa(id_produto, id_pessoa());
        updateParent(JasapPage.DIV_DETAIL, produtoDetail());
        evalParent(Js.CLOSE_SUB_WINDOWS);
        return new Response();
    }
}

public static class RefreshDetail extends DepartamentoPessoaForm {
    @Override
    public Effect execute() throws Exception {
        update(JasapPage.DIV_DETAIL, produtoDetail());
        return new Response();
    }
}

public static class SearchProduto extends DepartamentoPessoaForm {
    @Override
    public Effect execute() throws Exception {
        getSession().addObj(S_QS_PRODUTO, getInput().getString(DepartamentoProdutoBean.QS_PRODUTO));
        update(JasapPage.DIV_DETAIL, produtoDetail());
        return new Response();
    }
}

public static class DetailChangePage extends DepartamentoPessoaForm {
    @Override
    public Effect execute() throws Exception {
        update(JasapPage.DIV_DETAIL, produtoDetail());
        return new Response();
    }
}

public static class DesvincularProduto extends DepartamentoPessoaForm {
    public DesvincularProduto() { super.getFilters().add(new TransactionFilter()); }
    @Override
    public Effect execute() throws Exception {
        Integer id_produto = getInput().getInteger(DepartamentoProdutoBean.ID_PRODUTO);
        if (JasapFunctions.equals(getInput().getInteger(CONFIRM), 1)) {
            getFactory().departamento().proModel().daoUnlinkPessoa(id_produto);
            update(JasapPage.DIV_DETAIL, produtoDetail());
        } else {
            eval(Js.swalConfirm("Deseja desvincular esse produto?",
                    link(DesvincularProduto.class)
                            .putInteger(CONFIRM, 1)
                            .putInteger(DepartamentoProdutoBean.ID_PRODUTO, id_produto)
                            .ajax(), ""));
        }
        return new Response();
    }
}

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

// window() alterado — divide em master + detail quando em modo update:
public Table window() throws Exception {
    Table w = new Table(getManager()).setSize("100%", "100%");
    w.rowC("1%",  JasapPage.DIV_TITLE, ui().title("CADASTRO DE PESSOA"));
    w.rowC("1%",  null, ui().line());
    if (isUpdate(FORM)) {
        w.rowC("1%").setId(JasapPage.DIV_MASTER).setContent(form()).table();
        w.rowC("1%",  null, ui().line());
        w.rowC("99%").setId(JasapPage.DIV_DETAIL).setContent(produtoDetail()).table();
    } else {
        w.rowC("auto").setId(JasapPage.DIV_MASTER).setContent(form()).table();
        w.rowC("1%",  null, ui().line());
        w.rowC("99%", JasapPage.DIV_DETAIL, "<div style='padding:20px;color:#aaa;font-size:14px;font-style:italic;'>Salve o registro para associar produtos a essa pessoa.</div>");
    }
    w.rowC("1%",  null, ui().line());
    w.rowC("1%",  JasapPage.DIV_BOTTOM, br());
    return w;
}

// Método novo — monta a ListView do detail:
public Table produtoDetail() throws Exception {
    ListView lv = ui().lView();
    lv.setPageAction(url(DetailChangePage.class))
            .setPage(getSession().getInteger(S_DET.concat(lv.getPAGE()), getInput()))
            .ajax();

    String qs = (String) getSession().getObject(S_QS_PRODUTO);
    DepartamentoProdutoWBean filtro = new DepartamentoProdutoWBean();
    filtro.setFk_pessoa_produto(id_pessoa());
    if (qs != null && !qs.isEmpty()) filtro.setQs_produto(qs);
    lv.setFiltro(filtro);
    getFactory().departamento().proModel().daoList(lv.getData());

    ListColumn col_nome = lv.newColumn("Nome").setWidth(300).setPadding(";padding:10 8 10 8;");
    ListColumn col_vl   = lv.newColumn("Valor").setWidth(100).setPadding(";padding:10 8 10 8;").alignCenter();
    ListColumn col_qtd  = lv.newColumn("Qtd").setWidth(80).setPadding(";padding:10 8 10 8;").alignCenter();
    ListColumn col_obs  = lv.newColumn("Observação").setPadding(";padding:10 8 10 8;");
    ListColumn col_del  = lv.newColumn("").setWidth(50).setPadding(";padding:6 4 6 4;").alignCenter();

    while (lv.hasNext()) {
        DepartamentoProdutoBean bean = (DepartamentoProdutoBean) lv.next();
        ListLine line = lv.createLine()
            .setOnclick(link(DepartamentoProdutoForm.ShowUpdate.class)
                .putInteger(DepartamentoProdutoBean.ID_PRODUTO, bean.getId_produto())
                .modal(new ModalConfig().setWidth("752").setHeight("570").setOnCloseURL(url(RefreshDetail.class))));
        col_nome.setContent(bean.getNome_produto());
        col_vl.setContent(d2(bean.getVl_produto()));
        col_qtd.setContent(bean.getQtd_produto());
        col_obs.setContent(bean.getObs_produto());
        col_del.setHtmlData("<button type='button' class='close' style='opacity:0.6;font-size:20px;color:#e8820c;float:none;' title='Desvincular' onclick=\"event.stopPropagation();" + link(DesvincularProduto.class).putInteger(DepartamentoProdutoBean.ID_PRODUTO, bean.getId_produto()).ajax() + "\">&times;</button>");
        lv.addLine(line);
    }

    Button cmd_add = ui().button("&nbsp;&nbsp;Associar Produto&nbsp;&nbsp;").setCss("btn btn-primary btn-lg").setNoSize()
            .setOnClick(link(DepartamentoProdutoSelect.class)
                .putString(LinkBox.CALLBACK_URL.concat("_PRODUTO"), url(AddExistingProduto.class))
                .modal(new ModalConfig().setWidth("750").setHeight("550").setOnCloseURL(url(RefreshDetail.class))));

    Button cmd_novo = ui().button("<span style='color:white'>&nbsp;&nbsp;Novo Produto&nbsp;&nbsp;</span>").setCss("btn btn-warning btn-lg").setNoSize()
            .setOnClick(link(DepartamentoProdutoForm.ShowInsert.class)
                .putInteger(DepartamentoProdutoForm.S_FROM_PESSOA, id_pessoa())
                .modal(new ModalConfig().setWidth("752").setHeight("570").setOnCloseURL(url(RefreshDetail.class))));

    Text qs_field = ui().text(DepartamentoProdutoBean.QS_PRODUTO)
            .setStyle("width:500; height:32")
            .setMaxlength(300)
            .setValue(qs)
            .setOnkeyup(Js.pressEnter(link(SearchProduto.class)
                            .putScript(DepartamentoProdutoBean.QS_PRODUTO, Js.SELF_VALUE)
                            .ajax()));

    // ... monta a tabela auxiliar com barra de botões, linha, ListView, campo de busca e nav ...

    return aux;
}

// Constantes novas:
public static String S_QS_PRODUTO = ROOT.concat("__S_QS_PRODUTO");
public static String S_DET        = ROOT.concat("__S_DET");

Mudanças neste arquivo: 9 imports novos, 5 inner classes novas, window() alterado pra dividir em master/detail quando em update (e placeholder em insert), método novo produtoDetail(), 2 constantes de sessão. ShowInsert/Insert/ShowUpdate/Update/Cancelar/Delete existentes ficam inalterados.

CÓDIGO COMPLETO — DepartamentoProdutoForm (apenas blocos novos)
0:00 / 0:00
public static class ShowInsert extends DepartamentoProdutoForm {
    @Override
    public Effect execute() throws Exception {
        setInsert(FORM);
        getSession().remove(ROOT.concat(DepartamentoProdutoBean.ID_PRODUTO));
        Integer fromPessoa = getInput().getInteger(S_FROM_PESSOA);   // ADICIONADO
        if (fromPessoa != null) proBean().setFk_pessoa_produto(fromPessoa);   // ADICIONADO
        proBean().setInsert_chk(getInput().getInteger(DepartamentoProdutoBean.INSERT_CHK));
        render();
        return new Response();
    }
}

// ... Insert, ShowUpdate, Update, Cancelar, Delete (sem alteração) ...
// ... render(), window(), br(), form(), getters, Fk_pessoa_produto (sem alteração) ...

// Constante nova:
public static String S_FROM_PESSOA = ROOT.concat("__FROM_PESSOA");

Mudanças neste arquivo: 2 linhas no ShowInsert + 1 constante. Se a URL chegar com S_FROM_PESSOA, o form já nasce com a FK preenchida. É o que faz o botão "Novo Produto" do detail abrir o form do Produto já com o Responsável preenchido.

CÓDIGO COMPLETO — DepartamentoManager
0:00 / 0:00
// ... regActions do Home, ProdutoList, ProdutoForm, PessoaList (sem alteração) ...

// Bloco do PessoaForm — 5 regActions novas adicionadas ao final:
regAction(DepartamentoPessoaForm.class);
regAction(DepartamentoPessoaForm.ShowInsert.class);
regAction(DepartamentoPessoaForm.ShowUpdate.class);
regAction(DepartamentoPessoaForm.Insert.class);
regAction(DepartamentoPessoaForm.Update.class);
regAction(DepartamentoPessoaForm.Cancelar.class);
regAction(DepartamentoPessoaForm.Delete.class);
regAction(DepartamentoPessoaForm.AddExistingProduto.class);
regAction(DepartamentoPessoaForm.RefreshDetail.class);
regAction(DepartamentoPessoaForm.SearchProduto.class);
regAction(DepartamentoPessoaForm.DetailChangePage.class);
regAction(DepartamentoPessoaForm.DesvincularProduto.class);

// ... bloco do PessoaSelect e ProdutoSelect (sem alteração) ...

Mudanças neste arquivo: 5 regAction novas, todas inner classes do DepartamentoPessoaForm. Nenhum import novo — a classe do Form já era importada desde o CRUD da Pessoa.

O mecanismo — como o detail mora dentro do form

A tela da Pessoa em modo update fica dividida em duas regiões empilhadas:

RegiãoDiv IDO que tem
Master (topo)JasapPage.DIV_MASTERForm da Pessoa — nome, apelido, CPF, status, observações. Idêntico ao que já existia antes
Detail (baixo)JasapPage.DIV_DETAILListView paginada dos produtos vinculados + barra de busca rápida + 2 botões (Associar/Novo) + lixeirinha de desvínculo por linha

Em modo insert a região DIV_DETAIL mostra apenas um placeholder ("Salve o registro para associar produtos a essa pessoa.") — só depois de existir o id_pessoa é que faz sentido pendurar filhos nele.

Por que os IDs (DIV_MASTER / DIV_DETAIL) importam

São os pontos de ancoragem que as actions de refresh usam pra injetar HTML novo sem redesenhar a tela inteira. Quando RefreshDetail, SearchProduto ou DetailChangePage executam, fazem:

update(JasapPage.DIV_DETAIL, produtoDetail());

O framework localiza o div com esse ID na página aberta e substitui o conteúdo interno. O Master (form da Pessoa) não é tocado — o usuário mantém os campos que digitou.

Cenários que disparam refresh do detail

Ação do usuárioAction chamadaAntes do update
Clica "Associar Produto" → escolhe no Select → Select chama callbackAddExistingProdutoExecuta daoLinkPessoa, depois updateParent(DIV_DETAIL, ...)
Abre o form de um produto via detail → edita → fecha modalRefreshDetail (via setOnCloseURL)Nada — só recarrega a lista (dados podem ter mudado no form)
Digita busca no campo → pressiona EnterSearchProdutoGrava qs_produto em S_QS_PRODUTO na sessão
Clica na paginação ("‹ 1 de 3 ›")DetailChangePageFramework grava a página destino em S_DET, action só recarrega o conteúdo
Clica lixeirinha laranja → confirma no swalDesvincularProdutoExecuta daoUnlinkPessoa, depois update(DIV_DETAIL, ...)

A única que precisa de updateParent (não update) é AddExistingProduto — porque ela roda dentro do modal do Select, então o Detail está na janela pai.

Mudança 1 — ProdutoDAO: daoLinkPessoa e daoUnlinkPessoa
public void daoLinkPessoa(Integer id_produto, Integer id_pessoa) throws Exception {
    String sql = "UPDATE " + DepartamentoProdutoBean.TABLE
            + " SET " + DepartamentoProdutoBean.FK_PESSOA_PRODUTO + "=" + SQL.value(id_pessoa)
            + " WHERE " + DepartamentoProdutoBean.ID_PRODUTO + "=" + SQL.value(id_produto);
    Query query = getDataBase().getQuery(sql);
    getDataBase().executeUpdate(query);
    query.release();
}

public void daoUnlinkPessoa(Integer id_produto) throws Exception {
    String sql = "UPDATE " + DepartamentoProdutoBean.TABLE
            + " SET " + DepartamentoProdutoBean.FK_PESSOA_PRODUTO + "=NULL"
            + " WHERE " + DepartamentoProdutoBean.ID_PRODUTO + "=" + SQL.value(id_produto);
    Query query = getDataBase().getQuery(sql);
    getDataBase().executeUpdate(query);
    query.release();
}

A base SQL da feature. Dois métodos praticamente idênticos — o único diferencial é o valor da coluna: um seta com o ID, outro com NULL.

Por que não reaproveitar daoUpdate?

daoUpdate(bean) existe e grava o bean inteiro via update().execute(bean, TABLE). Isso significaria:

  • Precisar carregar o bean completo do banco primeiro (daoSingle)
  • Alterar só o campo fk_pessoa_produto
  • Gravar o bean todo de volta

Dois roundtrips no banco pra trocar 1 campo. Os métodos cirúrgicos (daoLinkPessoa/daoUnlinkPessoa) fazem em 1 roundtrip. Mesma técnica que o DeleteFromList usa — SQL direto pra operações pontuais que não precisam do bean inteiro.

Uso de SQL.value(...) — contra SQL injection

SQL.value(id_pessoa) e SQL.value(id_produto) escapam o valor antes de concatenar na string SQL. Protege contra SQL injection mesmo em método cujo parâmetro vem do usuário (via getInput().getInteger()). Sem isso, uma string mal-intencionada no input poderia sair a string SQL quebrando a query.

Por que query.release()

Libera o Statement JDBC e o cursor no banco. Sem release, em alta carga o pool de conexões do PostgreSQL fica retendo recursos. É padrão em todos os métodos do DAO — contrato do Jasap.

Mudança 2 — PessoaForm: window() divide em master + detail
public Table window() throws Exception {
    Table w = new Table(getManager()).setSize("100%", "100%");
    w.rowC("1%",  JasapPage.DIV_TITLE, ui().title("CADASTRO DE PESSOA"));
    w.rowC("1%",  null, ui().line());
    if (isUpdate(FORM)) {
        w.rowC("1%").setId(JasapPage.DIV_MASTER).setContent(form());
        w.rowC("1%",  null, ui().line());
        w.rowC("99%").setId(JasapPage.DIV_DETAIL).setContent(produtoDetail());
    } else {
        w.rowC("auto").setId(JasapPage.DIV_MASTER).setContent(form()).table();
        w.rowC("1%",  null, ui().line());
        w.rowC("99%", JasapPage.DIV_DETAIL, "<div style='padding:20px;color:#aaa;font-size:14px;font-style:italic;'>Salve o registro para associar produtos a essa pessoa.</div>");
    }
    w.rowC("1%",  null, ui().line());
    w.rowC("1%",  JasapPage.DIV_BOTTOM, br());
    return w;
}

A única mudança no window(): o if (isUpdate(FORM)) no meio. Antes do Master/Detail, o window() só tinha uma linha central com o form. Agora tem uma bifurcação:

ModoLayout
UpdateForm compacto em cima (peso 1%), linha separadora, detail ocupando o resto (peso 99%)
InsertForm auto-sized em cima, linha separadora, placeholder cinza italico avisando pra salvar antes

O modo Insert não chama produtoDetail() de propósito: sem id_pessoa gravado, a consulta de produtos vinculados ainda não tem critério válido — retornaria tudo ou nada, confundindo o usuário.

Por que DIV_MASTER e DIV_DETAIL em vez de IDs soltos?

Constantes do framework (br.jasap.gui.JasapPage). Usadas em várias partes do Jasap pra marcar regiões atualizáveis — ListView da List principal também usa DIV_MASTER, modais usam DIV_WINDOW. Padronizar os IDs facilita o framework localizar as regiões em operações de update Ajax.

Mudança 3 — PessoaForm: produtoDetail()

Método que monta a ListView do detail. Parece grande, mas é o mesmo esqueleto do listV() do Select do LinkBox com pequenas adaptações.

Filtro com FK fixa + busca rápida opcional

String qs = (String) getSession().getObject(S_QS_PRODUTO);
DepartamentoProdutoWBean filtro = new DepartamentoProdutoWBean();
filtro.setFk_pessoa_produto(id_pessoa());
if (qs != null && !qs.isEmpty()) filtro.setQs_produto(qs);
lv.setFiltro(filtro);
getFactory().departamento().proModel().daoList(lv.getData());

O filtro sempre tem fk_pessoa_produto = id_pessoa() — só traz produtos dessa pessoa. Se o usuário digitou algo na busca rápida, soma o qs_produto. O daoWhere do ProdutoDAO (que já existia) aplica as duas condições com AND.

Linha clicável abre o form do Produto num modal

ListLine line = lv.createLine()
    .setOnclick(link(DepartamentoProdutoForm.ShowUpdate.class)
        .putInteger(DepartamentoProdutoBean.ID_PRODUTO, bean.getId_produto())
        .modal(new ModalConfig().setWidth("752").setHeight("570").setOnCloseURL(url(RefreshDetail.class))));

Detalhe importante: setOnCloseURL(url(RefreshDetail.class)). Quando o usuário fecha o modal do produto (ou salva e o modal fecha sozinho), o framework dispara a URL de RefreshDetail — que re-renderiza o detail. Assim qualquer edição que o usuário fez no produto (ex: mudou o nome, o valor) aparece refletida na linha.

Lixeirinha por linha

col_del.setHtmlData("<button ... onclick=\"event.stopPropagation();"
    + link(DesvincularProduto.class)
        .putInteger(DepartamentoProdutoBean.ID_PRODUTO, bean.getId_produto())
        .ajax() + "\">&times;</button>");

HTML custom injetado na coluna via setHtmlData. O event.stopPropagation() impede que o clique na lixeira propague pra linha (senão abriria o form do produto junto).

Dois botões de adicionar

BotãoO que faz
Associar Produto (azul)Abre o DepartamentoProdutoSelect num modal. O Select chama de volta AddExistingProduto com o id_produto escolhido. Pra vincular um produto que já existe
Novo Produto (laranja)Abre o DepartamentoProdutoForm.ShowInsert num modal, passando S_FROM_PESSOA=id_pessoa(). O Form nasce já com a FK preenchida. Pra criar um produto novo e já associar numa tacada

Os dois modais usam setOnCloseURL(url(RefreshDetail.class)) — qualquer que seja o caminho, o detail é recarregado ao fechar.

Busca rápida com Enter

.setOnkeyup(Js.pressEnter(link(SearchProduto.class)
                .putScript(DepartamentoProdutoBean.QS_PRODUTO, Js.SELF_VALUE)
                .ajax()));

Padrão já conhecido do Sort + QuickSearch. Pressiona Enter → dispara SearchProduto com o valor digitado → grava em S_QS_PRODUTO → re-renderiza o detail com o filtro aplicado.

Mudança 4 — PessoaForm: 5 inner actions do detail

Todas as 5 estendem DepartamentoPessoaForm (e não DepartamentoPessoaAction) porque precisam do método produtoDetail() pra montar o HTML de resposta. Herdam também pesBean(), id_pessoa(), url(), link().

AddExistingProduto — vincular produto existente

public static class AddExistingProduto extends DepartamentoPessoaForm {
    public AddExistingProduto() { super.getFilters().add(new TransactionFilter()); }
    @Override
    public Effect execute() throws Exception {
        Integer id_produto = getInput().getInteger(DepartamentoProdutoBean.ID_PRODUTO);
        getFactory().departamento().proModel().daoLinkPessoa(id_produto, id_pessoa());
        updateParent(JasapPage.DIV_DETAIL, produtoDetail());
        evalParent(Js.CLOSE_SUB_WINDOWS);
        return new Response();
    }
}

É o callback do Select de Produto — análogo ao Fk_pessoa_produto do LinkBox, mas no sentido inverso (aqui é a pessoa recebendo o produto escolhido). updateParent em vez de update porque a action roda dentro do modal do Select; o detail está na janela pai. TransactionFilter porque escreve no banco — se o UPDATE falhar por qualquer motivo, rollback automático.

RefreshDetail — recarregar após fechar modais

public static class RefreshDetail extends DepartamentoPessoaForm {
    @Override
    public Effect execute() throws Exception {
        update(JasapPage.DIV_DETAIL, produtoDetail());
        return new Response();
    }
}

A mais simples: só recarrega o detail. É chamada pelo setOnCloseURL de vários modais (linha do detail, botão Associar, botão Novo). Nenhum acesso ao banco de escrita, nenhum filtro — só garante que o detail reflita o estado atual da tabela.

SearchProduto — busca rápida dentro do detail

public static class SearchProduto extends DepartamentoPessoaForm {
    @Override
    public Effect execute() throws Exception {
        getSession().addObj(S_QS_PRODUTO, getInput().getString(DepartamentoProdutoBean.QS_PRODUTO));
        update(JasapPage.DIV_DETAIL, produtoDetail());
        return new Response();
    }
}

Grava o termo digitado em S_QS_PRODUTO antes de re-renderizar. O produtoDetail() lê essa chave de volta e adiciona ao filtro.

DetailChangePage — paginação

public static class DetailChangePage extends DepartamentoPessoaForm {
    @Override
    public Effect execute() throws Exception {
        update(JasapPage.DIV_DETAIL, produtoDetail());
        return new Response();
    }
}

O framework grava o número da página destino em S_DET (prefixo da chave do PAGE) antes de disparar a action. A action em si só recarrega o detail — o produtoDetail() lê a página de volta via setPage(getSession().getInteger(S_DET.concat(lv.getPAGE()), getInput())).

DesvincularProduto — lixeirinha com confirmação

public static class DesvincularProduto extends DepartamentoPessoaForm {
    public DesvincularProduto() { super.getFilters().add(new TransactionFilter()); }
    @Override
    public Effect execute() throws Exception {
        Integer id_produto = getInput().getInteger(DepartamentoProdutoBean.ID_PRODUTO);
        if (JasapFunctions.equals(getInput().getInteger(CONFIRM), 1)) {
            getFactory().departamento().proModel().daoUnlinkPessoa(id_produto);
            update(JasapPage.DIV_DETAIL, produtoDetail());
        } else {
            eval(Js.swalConfirm("Deseja desvincular esse produto?",
                    link(DesvincularProduto.class)
                            .putInteger(CONFIRM, 1)
                            .putInteger(DepartamentoProdutoBean.ID_PRODUTO, id_produto)
                            .ajax(), ""));
        }
        return new Response();
    }
}

Mesmo padrão do Delete do form de edição: 1ª chamada mostra swal, 2ª chamada (com CONFIRM=1) desvincula. Detalhe: o link de confirmação precisa transportar o id_produto também — senão a 2ª chamada não sabe qual produto desvincular.

Mudança 5 — ProdutoForm: ShowInsertS_FROM_PESSOA
public static class ShowInsert extends DepartamentoProdutoForm {
    @Override
    public Effect execute() throws Exception {
        setInsert(FORM);
        getSession().remove(ROOT.concat(DepartamentoProdutoBean.ID_PRODUTO));
        Integer fromPessoa = getInput().getInteger(S_FROM_PESSOA);   // ADICIONADO
        if (fromPessoa != null) proBean().setFk_pessoa_produto(fromPessoa);   // ADICIONADO
        proBean().setInsert_chk(getInput().getInteger(DepartamentoProdutoBean.INSERT_CHK));
        render();
        return new Response();
    }
}

public static String S_FROM_PESSOA = ROOT.concat("__FROM_PESSOA");   // NOVA CONSTANTE

Ajuste pequeno mas de UX. Quando o usuário clica "Novo Produto" no detail do PessoaForm, o link embutido passa S_FROM_PESSOA=id_pessoa(). O ShowInsert lê esse input e, se veio, pré-preenche a FK no bean — o LinkBox "Responsável" do Form do Produto já aparece com o nome da pessoa atual, economizando um clique.

Por que input e não sessão?

Input viaja apenas nessa requisição — depois some. Se fosse sessão, sobreviveria entre chamadas: se o usuário abrisse o form normalmente (via lista de Produtos), ainda veria a FK pré-preenchida de uma interação anterior no detail. Seria bug sutil. Input é efêmero e escopado ao caminho exato ("clicou em Novo Produto no detail desta pessoa agora").

Mudança 6 — Manager: 5 regAction novas
regAction(DepartamentoPessoaForm.AddExistingProduto.class);
regAction(DepartamentoPessoaForm.RefreshDetail.class);
regAction(DepartamentoPessoaForm.SearchProduto.class);
regAction(DepartamentoPessoaForm.DetailChangePage.class);
regAction(DepartamentoPessoaForm.DesvincularProduto.class);

5 linhas novas, todas no bloco do DepartamentoPessoaForm (logo após o Delete). Sem elas, todas as 5 inner classes seriam "fantasmas" — existem em código mas 404 quando chamadas. Sintomas:

EsqueceuSintoma
AddExistingProdutoEscolher pessoa no Select não vincula produto
RefreshDetailFechar modal de edição do produto não atualiza o detail
SearchProdutoDigitar na busca do detail e pressionar Enter não filtra
DetailChangePageBotões de paginação do detail não respondem
DesvincularProdutoLixeirinha laranja não abre swal

Todos falham silenciosos — compila, o link é gerado, só não funciona. Checklist obrigatório ao adicionar inner class de action: registrou no Manager?

O que NÃO faz parte do Master/Detail

Cinco coisas que parecem relacionadas mas não são tocadas:

ItemPor que fica de fora
Banco (DDL)A FK fk_pessoa_produto já existia desde o LinkBox. Nenhum ALTER TABLE, nenhuma tabela de associação. Se fosse N-N, precisaria de uma — mas a regra de negócio aqui é 1-N (um produto → uma pessoa)
DepartamentoProdutoBeanInalterado. O objeto Pessoa embutido do LinkBox já basta — o detail lê os produtos inteiros, não precisa de novas propriedades no Bean
DepartamentoProdutoWBeanO filtro fk_pessoa_produto já existia desde o LinkBox (usado no daoWhere). O detail só reusa
DepartamentoProdutoListLista principal de produtos continua idêntica. O detail usa uma ListView separada montada no PessoaForm
DepartamentoProdutoSelectReaproveitado do LinkBox sem mudanças. O "Associar Produto" do detail abre o mesmo modal que o LinkBox do Form do Produto — só muda o callback (AddExistingProduto em vez de Fk_pessoa_produto)

A economia vem da FK 1-N do LinkBox. Se esta sequência fosse implementada antes do LinkBox (ou com relacionamento N-N), o escopo dobraria: DDL, tabela de associação, métodos de join no DAO, reorganização do Bean.

Resumo — o que mudou
ArquivoTipoEdição
DepartamentoProdutoDAOeditar2 métodos novos: daoLinkPessoa (UPDATE com FK preenchida) e daoUnlinkPessoa (UPDATE com FK=NULL)
DepartamentoPessoaFormeditar9 imports, 5 inner classes (AddExistingProduto, RefreshDetail, SearchProduto, DetailChangePage, DesvincularProduto), window() divide em master/detail, método produtoDetail(), 2 constantes (S_QS_PRODUTO, S_DET)
DepartamentoProdutoFormeditar2 linhas no ShowInsert pra ler S_FROM_PESSOA e pré-preencher a FK + 1 constante S_FROM_PESSOA
DepartamentoManagereditar5 regAction novas, todas inner classes do PessoaForm
BancoNada — FK já existia

4 arquivos editados, 0 arquivos novos, nenhuma mudança de schema. Resultado: tela da Pessoa em modo edição passa a mostrar uma lista de produtos vinculados embaixo do form, com busca, paginação, botões de "Associar Produto" (modal de seleção) e "Novo Produto" (form pré-preenchido), e lixeirinha de desvínculo em cada linha. Todas as interações mantêm o form da Pessoa intacto em cima — só o detail é re-renderizado via Ajax.

Jornada Completa — o agente em ação

Agora que cada peça foi construída, escute o agente percorrendo o prédio e usando tudo que a gente montou. 5 cenários: abertura do form em edit, Associar Produto existente, Desvincular com swalConfirm, Novo Produto com S_FROM_PESSOA, e o padrão de refresh (Buscar, Paginar, Editar inline).

0:00 / 0:00