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.
// ... 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.
// 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() + "\">×</button>");
lv.addLine(line);
}
Button cmd_add = ui().button(" Associar Produto ").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'> Novo Produto </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.
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.
// ... 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.
A tela da Pessoa em modo update fica dividida em duas regiões empilhadas:
| Região | Div ID | O que tem |
|---|---|---|
| Master (topo) | JasapPage.DIV_MASTER | Form da Pessoa — nome, apelido, CPF, status, observações. Idêntico ao que já existia antes |
| Detail (baixo) | JasapPage.DIV_DETAIL | ListView 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.
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.
| Ação do usuário | Action chamada | Antes do update |
|---|---|---|
| Clica "Associar Produto" → escolhe no Select → Select chama callback | AddExistingProduto | Executa daoLinkPessoa, depois updateParent(DIV_DETAIL, ...) |
| Abre o form de um produto via detail → edita → fecha modal | RefreshDetail (via setOnCloseURL) | Nada — só recarrega a lista (dados podem ter mudado no form) |
| Digita busca no campo → pressiona Enter | SearchProduto | Grava qs_produto em S_QS_PRODUTO na sessão |
| Clica na paginação ("‹ 1 de 3 ›") | DetailChangePage | Framework grava a página destino em S_DET, action só recarrega o conteúdo |
| Clica lixeirinha laranja → confirma no swal | DesvincularProduto | Executa 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.
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.
daoUpdate?daoUpdate(bean) existe e grava o bean inteiro via update().execute(bean, TABLE). Isso significaria:
daoSingle)fk_pessoa_produtoDois 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.
SQL.value(...) — contra SQL injectionSQL.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.
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.
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:
| Modo | Layout |
|---|---|
| Update | Form compacto em cima (peso 1%), linha separadora, detail ocupando o resto (peso 99%) |
| Insert | Form 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.
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.
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.
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.
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.
col_del.setHtmlData("<button ... onclick=\"event.stopPropagation();"
+ link(DesvincularProduto.class)
.putInteger(DepartamentoProdutoBean.ID_PRODUTO, bean.getId_produto())
.ajax() + "\">×</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).
| Botão | O 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.
.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.
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().
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.
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.
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.
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())).
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.
ShowInsert lê S_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.
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").
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:
| Esqueceu | Sintoma |
|---|---|
AddExistingProduto | Escolher pessoa no Select não vincula produto |
RefreshDetail | Fechar modal de edição do produto não atualiza o detail |
SearchProduto | Digitar na busca do detail e pressionar Enter não filtra |
DetailChangePage | Botões de paginação do detail não respondem |
DesvincularProduto | Lixeirinha 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?
Cinco coisas que parecem relacionadas mas não são tocadas:
| Item | Por 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) |
| DepartamentoProdutoBean | Inalterado. O objeto Pessoa embutido do LinkBox já basta — o detail lê os produtos inteiros, não precisa de novas propriedades no Bean |
| DepartamentoProdutoWBean | O filtro fk_pessoa_produto já existia desde o LinkBox (usado no daoWhere). O detail só reusa |
| DepartamentoProdutoList | Lista principal de produtos continua idêntica. O detail usa uma ListView separada montada no PessoaForm |
| DepartamentoProdutoSelect | Reaproveitado 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.
| Arquivo | Tipo | Edição |
|---|---|---|
| DepartamentoProdutoDAO | editar | 2 métodos novos: daoLinkPessoa (UPDATE com FK preenchida) e daoUnlinkPessoa (UPDATE com FK=NULL) |
| DepartamentoPessoaForm | editar | 9 imports, 5 inner classes (AddExistingProduto, RefreshDetail, SearchProduto, DetailChangePage, DesvincularProduto), window() divide em master/detail, método produtoDetail(), 2 constantes (S_QS_PRODUTO, S_DET) |
| DepartamentoProdutoForm | editar | 2 linhas no ShowInsert pra ler S_FROM_PESSOA e pré-preencher a FK + 1 constante S_FROM_PESSOA |
| DepartamentoManager | editar | 5 regAction novas, todas inner classes do PessoaForm |
| Banco | — | Nada — 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.
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).