Até aqui, Produto e Pessoa viviam em tabelas separadas, cada uma com seu CRUD próprio. O LinkBox é o primeiro vínculo entre elas: cada produto passa a ter uma pessoa responsável, representada no formulário por um campo de leitura com botão verde ao lado — clicar abre um modal de seleção, escolher uma pessoa preenche o nome no campo e grava a FK no bean ao salvar.
É uma FK 1-N vista do lado do filho: o Produto aponta pra uma Pessoa (coluna fk_pessoa_produto em departamento.produto), e cada Pessoa pode ter várias produtos sob sua responsabilidade. A mudança envolve 4 arquivos: o Bean do Produto ganha um objeto Pessoa embutido, o Form do Produto ganha o campo e a action de callback, o arquivo novo DepartamentoPessoaSelect é o modal de seleção, e o Manager registra as 4 actions novas.
package br.xt.app.departamento.produto;
import br.jasap.dao.DBInfo;
import br.jasap.util.DomainValue;
import br.jasap.util.JasapList;
import br.xt.app.departamento.pessoa.DepartamentoPessoaBean;
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;
private DepartamentoPessoaBean pessoa;
@DBInfo(serial=true, pk=true)
public Integer getId_produto() { return id_produto; }
public void setId_produto(Integer id_produto) { this.id_produto = id_produto; }
// ... getters/setters dos outros campos (sem alteração) ...
public DepartamentoPessoaBean getPessoa() {
if (pessoa == null) pessoa = new DepartamentoPessoaBean();
return pessoa;
}
public void setPessoa(DepartamentoPessoaBean pessoa) { this.pessoa = pessoa; }
public Integer getFk_pessoa_produto() { return getPessoa().getId_pessoa(); }
public void setFk_pessoa_produto(Integer fk) { getPessoa().setId_pessoa(fk); }
// ... DomStatus (sem alteração) ...
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 FK_PESSOA_PRODUTO = "fk_pessoa_produto";
public static String INSERT_CHK = "insert_chk";
}
Mudanças neste arquivo: 1 import novo (DepartamentoPessoaBean), campo private pessoa, getter/setter de pessoa (com lazy init), getter/setter de fk_pessoa_produto delegando pro objeto embutido (sem campo próprio), constante FK_PESSOA_PRODUTO. A coluna no banco não muda.
package br.xt.app.departamento.pessoa;
import br.jasap.core.Effect;
import br.jasap.effect.Response;
import br.jasap.gui.Bar;
import br.jasap.gui.JasapPage;
import br.jasap.gui.ListColumn;
import br.jasap.gui.ListLine;
import br.jasap.gui.ListView;
import br.jasap.gui.Table;
import br.jasap.gui.form.LinkBox;
import br.jasap.gui.form.Text;
import br.jasap.util.JasapList;
import br.jasap.util.Js;
import br.xt.acore.view.XtPage;
public class DepartamentoPessoaSelect extends DepartamentoPessoaAction {
@Override
public Effect execute() throws Exception {
getSession().remove(S_ROOT);
render();
return new Response();
}
public static class Sort extends DepartamentoPessoaSelect {
@Override
public Effect execute() throws Exception {
update(listV().getDIV_HEADER(), listV().getHeader());
update(listV().getDIV_BODY(), listV().getBody());
update(listV().getDIV_NAVIGATE(), listV().getNavForm());
return new Response();
}
}
public static class QuickSearch extends DepartamentoPessoaSelect {
@Override
public Effect execute() throws Exception {
getFiltroS().setQs_pessoa(getInput().getString(DepartamentoPessoaBean.QS_PESSOA));
update(listV().getDIV_BODY(), listV().getBody());
update(listV().getDIV_NAVIGATE(), listV().getNavForm());
return new Response();
}
}
public void render() throws Exception {
XtPage page = new XtPage(getManager());
if (isAjaxCall()) {
update(JasapPage.DIV_WINDOW, page.content(window().toHtml()));
eval(Js.CLOSE_SUB_WINDOWS);
} else {
page.getTable().setBorder(4).rowC("100%")
.setContent(page.content(window().toHtml()))
.setStyle(ui().modalBorder());
page.setWinTitle("Selecionar Pessoa");
getOutput().write(this, page);
}
}
public Table window() throws Exception {
Table w = new Table(getManager()).setSize("100%", "100%");
w.rowC("1%", JasapPage.DIV_TITLE, ui().title("SELEÇÃO DE PESSOA"));
w.rowC("99%").setId(JasapPage.DIV_MASTER).setContent(listV()).table();
w.rowC("1%", null, ui().line());
w.rowC("1%", null, qs_pessoa());
w.rowC("1%", JasapPage.DIV_BOTTOM, br());
return w;
}
public String qs_pessoa() throws Exception {
Text qs = ui().text(DepartamentoPessoaBean.QS_PESSOA)
.setLabel("Consulta")
.setStyle("width:300")
.setMaxlength(300)
.setValue(getFiltroS().getQs_pessoa())
.setOnkeyup(Js.pressEnter(
link(QuickSearch.class)
.putScript(DepartamentoPessoaBean.QS_PESSOA, Js.SELF_VALUE)
.putInteger(listV().getPAGE(), 0)
.ajax()));
Table aux = new Table(getManager(), "100%", "30")
.setStyle(ui().bgDark())
.rowC()
.setAlign(Table.ALIGN_CENTER)
.setVerticalAlign(Table.ALIGN_MIDDLE)
.setContent(qs)
.table();
return aux.toHtml();
}
private Bar br() throws Exception {
return ui().bar().addCenter(listV().nav(listV().getNavForm()));
}
private ListView lv = null;
public ListView listV() throws Exception {
if (lv == null) {
lv = ui().lView();
lv.setCallBackURL(getSession().getString(LinkBox.CALLBACK_URL.concat("_PESSOA"), getInput()))
.setSortAct(url(Sort.class))
.setPageAction(url(Sort.class))
.setPage(getSession().getInteger(S_LIST.concat(lv.getPAGE()), getInput()))
.setOrderBy(getSession().getString(S_LIST.concat(lv.getORDER_BY()), getInput()))
.setSort(getSession().getString(S_LIST.concat(lv.getSORT()), getInput()))
.ajax();
if (lv.getData().getOrderBy() == null) {
lv.getData().setOrderBy(DepartamentoPessoaBean.NOME_PESSOA);
lv.getData().setSort(JasapList.SORT_ASC);
}
lv.setFiltro(getFiltroS());
getFactory().departamento().pesModel().daoList(lv.getData());
ListColumn col_nome = lv.newColumn("Nome").setOrderBy(DepartamentoPessoaBean.NOME_PESSOA).setPadding(";padding:10 8 10 8;");
ListColumn col_apelido = lv.newColumn("Apelido").setOrderBy(DepartamentoPessoaBean.APELIDO_PESSOA).setWidth(180).setPadding(";padding:10 8 10 8;");
while (lv.hasNext()) {
DepartamentoPessoaBean lb = (DepartamentoPessoaBean) lv.next();
ListLine line = lv.createLine()
.setOnclick(link(lv.getCallBackURL())
.putInteger(DepartamentoPessoaBean.ID_PESSOA, lb.getId_pessoa())
.noWait().ajax());
col_nome.setContent(lb.getNome_pessoa());
col_apelido.setContent(lb.getApelido_pessoa());
lv.addLine(line);
}
}
return lv;
}
public DepartamentoPessoaWBean getFiltroS() throws Exception {
DepartamentoPessoaWBean filtro = (DepartamentoPessoaWBean) getSession().getObject(S_FILTRO);
if (filtro == null) {
filtro = pesWBean();
getSession().addObj(S_FILTRO, filtro);
}
return filtro;
}
public static final String S_ROOT = ROOT.concat("__PESSOA_SELECT/");
public static final String S_LIST = S_ROOT.concat("__LIST/");
public static final String S_FILTRO = S_LIST.concat("__FILTRO");
}
Arquivo novo. 3 actions (principal + Sort + QuickSearch), método listV() com memoization, filtro em sessão, callback via LinkBox.CALLBACK_URL + "_PESSOA".
// Imports novos:
import br.jasap.gui.form.LinkBox;
import br.xt.app.departamento.pessoa.DepartamentoPessoaBean;
import br.xt.app.departamento.pessoa.DepartamentoPessoaSelect;
// ... ShowInsert, Insert, ShowUpdate, Update, Cancelar, Delete, render(), window(), br() (sem alteração) ...
// Linha adicionada no form(), entre st_produto e obs_produto:
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(fk_pessoa_produto(), "150"); // ADICIONADA
frm.line().add(obs_produto(), "150");
frm.line().add(insert_chk(), "150");
return frm.getTable().toHtml();
}
// ... nome_produto(), vl_produto(), qtd_produto(), st_produto(), obs_produto() (sem alteração) ...
// Método novo:
private LinkBox fk_pessoa_produto = null;
public LinkBox fk_pessoa_produto() throws Exception {
if (fk_pessoa_produto == null) {
if (proBean().getFk_pessoa_produto() != null) {
getFactory().departamento().pesModel().daoSingle(proBean().getPessoa());
}
fk_pessoa_produto = ui().linkBox("FK_PESSOA", "500", "400")
.setLabel("Responsável")
.setValue(proBean().getPessoa().getNome_pessoa())
.setWidth(250)
.addKey(DepartamentoProdutoBean.FK_PESSOA_PRODUTO, proBean().getFk_pessoa_produto())
.setSelectLnk(link(DepartamentoPessoaSelect.class)).setCallBackID("_PESSOA")
.setCallBackURL(url(Fk_pessoa_produto.class));
}
return fk_pessoa_produto;
}
// Inner class nova:
public static class Fk_pessoa_produto extends DepartamentoProdutoForm {
@Override
public Effect execute() throws Exception {
proBean().setFk_pessoa_produto(getInput().getInteger(DepartamentoPessoaBean.ID_PESSOA));
if (proBean().getFk_pessoa_produto() != null) {
updateParent("FK_PESSOA", fk_pessoa_produto().textValue());
evalParent(Js.CLOSE_SUB_WINDOWS);
} else {
update("FK_PESSOA", fk_pessoa_produto().textValue());
}
return new Response();
}
}
Mudanças neste arquivo: 3 imports novos, 1 linha no form(), método fk_pessoa_produto(), inner class Fk_pessoa_produto. O resto do Form (ShowInsert, Insert, Update, Delete, render, window, br, getters dos outros campos) fica igual.
package br.xt.app.departamento;
import br.xt.app.departamento.pessoa.DepartamentoPessoaForm;
import br.xt.app.departamento.pessoa.DepartamentoPessoaList;
import br.xt.app.departamento.pessoa.DepartamentoPessoaSelect;
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 do Home, ProdutoList, ProdutoForm (Show, Insert, Update, Cancelar, Delete) ...
regAction(DepartamentoProdutoForm.Fk_pessoa_produto.class);
// ... regAction do PessoaList, PessoaForm (Show, Insert, Update, Cancelar, Delete) ...
regAction(DepartamentoPessoaSelect.class);
regAction(DepartamentoPessoaSelect.Sort.class);
regAction(DepartamentoPessoaSelect.QuickSearch.class);
}
}
Mudanças neste arquivo: 1 import novo (DepartamentoPessoaSelect), 4 regAction novas — 1 pra inner class de callback do Form e 3 pras actions do Select.
O LinkBox é uma FK vista do lado do filho: na tela aparece como um input de texto (mostra o nome do pai) + um botão verde ao lado. O usuário não digita — clica no botão, escolhe no modal e o texto aparece. Três peças formam o circuito:
| Peça | Papel | Onde mora |
|---|---|---|
| LinkBox | Input visual com botão. Guarda a FK num hidden e o nome do pai visível | fk_pessoa_produto() no DepartamentoProdutoForm |
| Select | Modal que lista os candidatos e dispara callback ao clicar numa linha | Arquivo novo DepartamentoPessoaSelect |
| Callback | Action que recebe o ID escolhido, grava no bean, atualiza a tela | Inner class Fk_pessoa_produto no Form |
Os dois lados (LinkBox e Select) não se referenciam diretamente — a ponte é a sessão HTTP: o LinkBox grava a URL de callback numa chave identificada por um sufixo (_PESSOA), e o Select lê a mesma chave pra saber pra onde chamar de volta.
| Passo | Quem dispara | O que acontece |
|---|---|---|
| 1 | Form do Produto renderiza | fk_pessoa_produto() monta o LinkBox, grava na sessão a URL de callback (url(Fk_pessoa_produto.class)) sob a chave LinkBox.CALLBACK_URL + "_PESSOA" |
| 2 | Usuário clica no botão verde | Link setSelectLnk abre o modal DepartamentoPessoaSelect em Ajax |
| 3 | Select carrega | listV() lê da sessão a URL de callback e configura cada linha com setOnclick(link(callbackURL).putInteger(ID_PESSOA, ...).ajax()) |
| 4 | Usuário clica numa pessoa | Browser dispara a URL de callback com id_pessoa=42. Chega na inner class Fk_pessoa_produto.execute() |
| 5 | Callback executa | proBean().setFk_pessoa_produto(42) → via delegação, vai pra getPessoa().setId_pessoa(42). updateParent("FK_PESSOA", textValue()) injeta o texto novo no Form. evalParent(CLOSE_SUB_WINDOWS) fecha o modal |
| 6 | Usuário clica "Salvar" | Update roda, o hidden fk_pessoa_produto viaja junto com o form, populateBean o grava no bean, daoUpdate emite UPDATE ... SET fk_pessoa_produto=42 |
private DepartamentoPessoaBean pessoa;
public DepartamentoPessoaBean getPessoa() {
if (pessoa == null) pessoa = new DepartamentoPessoaBean();
return pessoa;
}
public void setPessoa(DepartamentoPessoaBean pessoa) { this.pessoa = pessoa; }
public Integer getFk_pessoa_produto() { return getPessoa().getId_pessoa(); }
public void setFk_pessoa_produto(Integer fk) { getPessoa().setId_pessoa(fk); }
A mudança é pequena em linhas mas conceitual: o Bean do Produto deixa de guardar fk_pessoa_produto como campo próprio e passa a guardar um objeto Pessoa embutido. A FK vira uma delegação — pega o id_pessoa da pessoa embutida.
private Integer fk_pessoa_produto como antes?Porque o LinkBox precisa exibir o nome da pessoa vinculada, não só o ID. Guardando só um Integer, o Form teria que fazer uma consulta extra em cada render pra descobrir o nome. Com o objeto embutido, basta o DAO chamar daoSingle(proBean().getPessoa()) uma vez e o nome fica disponível em getPessoa().getNome_pessoa().
Lazy init = "criar só quando precisar". O campo pessoa começa null; só vira objeto de verdade na primeira chamada a getPessoa(). Duas razões:
ResultSet e deixa pessoa como null. Quem chamar getPessoa() depois garante que retorna um objeto, nunca null| Situação | O que acontece |
|---|---|
| DAO lê do banco | setFk_pessoa_produto(42) → getPessoa().setId_pessoa(42). Objeto pessoa criado com só o ID |
| Form carrega o nome | daoSingle(proBean().getPessoa()) preenche os demais campos (nome, apelido, etc) |
| LinkBox exibe | proBean().getPessoa().getNome_pessoa() — já tá lá |
| DAO grava no banco | getFk_pessoa_produto() → getPessoa().getId_pessoa() → ID correto pra coluna |
Do ponto de vista do banco, nada muda — a coluna fk_pessoa_produto continua existindo e sendo escrita com o ID. A diferença é interna: o valor passa por uma ponte (objeto pessoa) em vez de ficar isolado num campo Integer.
fk_pessoa_produto() no form()
frm.line().add(fk_pessoa_produto(), "150"); // ADICIONADA, entre st_produto e obs_produto
private LinkBox fk_pessoa_produto = null;
public LinkBox fk_pessoa_produto() throws Exception {
if (fk_pessoa_produto == null) {
if (proBean().getFk_pessoa_produto() != null) {
getFactory().departamento().pesModel().daoSingle(proBean().getPessoa());
}
fk_pessoa_produto = ui().linkBox("FK_PESSOA", "500", "400")
.setLabel("Responsável")
.setValue(proBean().getPessoa().getNome_pessoa())
.setWidth(250)
.addKey(DepartamentoProdutoBean.FK_PESSOA_PRODUTO, proBean().getFk_pessoa_produto())
.setSelectLnk(link(DepartamentoPessoaSelect.class)).setCallBackID("_PESSOA")
.setCallBackURL(url(Fk_pessoa_produto.class));
}
return fk_pessoa_produto;
}
Se o produto já tem responsável (cenário update), vai no banco buscar os dados da pessoa. O daoSingle recebe o objeto pessoa (que já tem o id_pessoa preenchido, graças à delegação no Bean) e enche os outros campos — inclusive o nome_pessoa, que é o que o LinkBox vai exibir. Se a FK é null, pula.
| Trecho | O que faz |
|---|---|
ui().linkBox("FK_PESSOA", "500", "400") | Cria o LinkBox com ID FK_PESSOA (usado nos update/updateParent) e tamanho do modal 500×400 |
.setLabel("Responsável") | Texto à esquerda do campo |
.setValue(proBean().getPessoa().getNome_pessoa()) | Pré-preenche o texto com o nome da pessoa (ou null) |
.setWidth(250) | Largura do campo de texto (não do modal) |
.addKey(FK_PESSOA_PRODUTO, getFk_pessoa_produto()) | Adiciona um hidden com a FK. Viaja junto no submit do Form; o populateBean do Update o lê e seta a FK antes do UPDATE |
.setSelectLnk(link(DepartamentoPessoaSelect.class)) | Link disparado ao clicar no botão verde — abre o Select |
.setCallBackID("_PESSOA") | Sufixo anexado à chave de sessão LinkBox.CALLBACK_URL. Os dois lados precisam combinar o mesmo sufixo |
.setCallBackURL(url(Fk_pessoa_produto.class)) | URL que o Select vai chamar. O LinkBox grava essa URL na sessão sob CALLBACK_URL + "_PESSOA" |
Lazy init (if (fk_pessoa_produto == null)) igual aos outros campos do Form — cria uma vez por requisição e reaproveita.
Fk_pessoa_produto (callback)
public static class Fk_pessoa_produto extends DepartamentoProdutoForm {
@Override
public Effect execute() throws Exception {
proBean().setFk_pessoa_produto(getInput().getInteger(DepartamentoPessoaBean.ID_PESSOA));
if (proBean().getFk_pessoa_produto() != null) {
updateParent("FK_PESSOA", fk_pessoa_produto().textValue());
evalParent(Js.CLOSE_SUB_WINDOWS);
} else {
update("FK_PESSOA", fk_pessoa_produto().textValue());
}
return new Response();
}
}
Esta action é o "outro lado" do LinkBox. Chamada pelo DepartamentoPessoaSelect quando o usuário clica numa pessoa da lista (o Select dispara o callback configurado pelo LinkBox).
| Linha | O que faz |
|---|---|
setFk_pessoa_produto(getInput().getInteger(ID_PESSOA)) | Lê o id_pessoa que o Select passou como parâmetro e grava no bean. Via delegação, também preenche getPessoa().setId_pessoa(...) |
updateParent("FK_PESSOA", fk_pessoa_produto().textValue()) | Atualiza o texto do campo na janela pai (o Form atrás do modal). textValue() retorna o HTML com o nome já preenchido |
evalParent(Js.CLOSE_SUB_WINDOWS) | Fecha o modal do Select — retorna o foco pro Form |
update("FK_PESSOA", ...) (else) | Cenário em que a FK veio null: só atualiza local, sem fechar modal |
extends DepartamentoProdutoForm | Herda fk_pessoa_produto(), proBean() e textValue(). Essencial pra re-montar o componente e pegar o texto atualizado |
Por que é public static class dentro do Form e não um arquivo separado? Jasap reaproveita o escopo da classe externa: proBean(), fk_pessoa_produto() etc são visíveis sem refatoração. É o mesmo padrão de ShowInsert, Insert, Delete.
DepartamentoPessoaSelect: o modal de seleção
Arquivo novo e o maior da sequência. Um Select no Jasap é uma ListView em miniatura dentro de um modal: parecido com a List comum (ordenação, paginação, busca), mas com duas diferenças:
S_ROOT) — separado da List principal e de outros Selects| Action | Quando é chamada | O que faz |
|---|---|---|
DepartamentoPessoaSelect (principal) | Quando o LinkBox abre o modal pela primeira vez | remove(S_ROOT) limpa toda a sub-árvore de sessão (zera busca/página anteriores) e chama render() |
.Sort | Clique no cabeçalho de uma coluna ou troca de página | Re-renderiza só header + body + navegação via Ajax — não redesenha o modal inteiro |
.QuickSearch | Enter no campo de busca | Grava o termo no filtro em sessão, re-renderiza body + navegação |
lv.setCallBackURL(getSession().getString(LinkBox.CALLBACK_URL.concat("_PESSOA"), getInput()))
O LinkBox, ao ser montado no Form, gravou a URL de callback na sessão sob a chave LinkBox.CALLBACK_URL + "_PESSOA". O Select lê essa chave e configura cada linha pra disparar aquela URL com o id_pessoa do registro clicado:
ListLine line = lv.createLine()
.setOnclick(link(lv.getCallBackURL())
.putInteger(DepartamentoPessoaBean.ID_PESSOA, lb.getId_pessoa())
.noWait().ajax());
public DepartamentoPessoaWBean getFiltroS() throws Exception {
DepartamentoPessoaWBean filtro = (DepartamentoPessoaWBean) getSession().getObject(S_FILTRO);
if (filtro == null) {
filtro = pesWBean();
getSession().addObj(S_FILTRO, filtro);
}
return filtro;
}
O WBean fica salvo sob S_FILTRO. Primeira chamada cria em branco e salva; chamadas seguintes (Sort, QuickSearch, paginação) retornam o mesmo filtro. Isso permite que a busca digitada no QuickSearch persista entre reordenações — sem o cache, cada clique recriaria o filtro em branco.
public static final String S_ROOT = ROOT.concat("__PESSOA_SELECT/");
public static final String S_LIST = S_ROOT.concat("__LIST/");
public static final String S_FILTRO = S_LIST.concat("__FILTRO");
| Constante | Papel |
|---|---|
S_ROOT | Raiz. remove(S_ROOT) apaga tudo abaixo de uma vez — garante que reabrir o modal começa do zero |
S_LIST | Subárvore dos parâmetros da ListView (página, coluna, direção) |
S_FILTRO | O WBean cacheado com o termo de busca |
O método listV() tem guard if (lv == null) — a ListView é montada uma única vez por requisição. Sort, QuickSearch e render() podem chamar listV() várias vezes e recebem sempre o mesmo objeto já configurado. Sem esse cache, a lista seria re-consultada do banco a cada chamada.
regAction novas
// Bloco do Form do Produto — uma regAction nova:
regAction(DepartamentoProdutoForm.Fk_pessoa_produto.class);
// Bloco do Select — bloco inteiro novo:
regAction(DepartamentoPessoaSelect.class);
regAction(DepartamentoPessoaSelect.Sort.class);
regAction(DepartamentoPessoaSelect.QuickSearch.class);
Toda action nova precisa ser registrada no Manager, senão o Jasap não a encontra no mapa global e a chamada retorna 404.
| regAction | Onde no Manager | Por quê |
|---|---|---|
DepartamentoProdutoForm.Fk_pessoa_produto | Final do bloco do Form do Produto — depois de Delete | Inner class que recebe o callback do LinkBox. Sem isso, o Select chama a URL e o Jasap não reconhece |
DepartamentoPessoaSelect | Bloco novo no final | Action principal — entrada chamada pelo botão do LinkBox |
DepartamentoPessoaSelect.Sort | Bloco do Select | Reordenação e paginação dentro do modal |
DepartamentoPessoaSelect.QuickSearch | Bloco do Select | Busca rápida digitada no campo |
Convenção do projeto: ordem dos regAction reflete a ordem conceitual — cada entidade em bloco (List → Form → Select). O Fk_pessoa_produto pertence ao Form (é callback de campo do Form), então fecha o bloco do ProdutoForm. O DepartamentoPessoaSelect é componente auxiliar, vem depois da entidade "dona" dele.
Compila, o link é gerado com a URL correta, mas o Manager não conhece a classe. No browser, o sintoma é mudo — nada explode, nada responde:
| Sintoma | Causa real |
|---|---|
| Clicar numa pessoa no modal não faz nada | Fk_pessoa_produto não registrado |
| Botão verde do LinkBox não abre modal | DepartamentoPessoaSelect não registrado |
| Reordenar colunas ou digitar busca no modal não faz nada | Sort ou QuickSearch não registrados |
Quatro coisas parecem relacionadas mas não são tocadas:
| Item | Por que fica de fora |
|---|---|
| DDL do banco | A coluna fk_pessoa_produto já existia na tabela. Nenhum ALTER TABLE — só um objeto embutido no Bean que delega pra mesma coluna |
| DepartamentoProdutoDAO | O daoInsert/daoUpdate continuam iguais. O populateBean do framework lê a coluna e chama setFk_pessoa_produto — que agora delega, mas a assinatura externa é a mesma |
| DepartamentoProdutoWBean | Filtros da listagem de Produto não mudam. Filtrar produtos por responsável é cenário futuro (Master/Detail) |
| DepartamentoPessoaForm | O Form da Pessoa fica igual — não precisa saber que produtos existem vinculados. O LinkBox é unidirecional: o filho aponta pro pai, o pai não "sabe" dos filhos |
Ver os produtos a partir do lado da Pessoa é a próxima etapa da sequência (Master/Detail) — aí sim o PessoaForm ganha uma tabela de detail, e aí sim o ProdutoDAO ganha daoLinkPessoa/daoUnlinkPessoa.
| Arquivo | Tipo | Edição |
|---|---|---|
| DepartamentoProdutoBean | editar | 1 import, campo private pessoa, getter/setter com lazy init, getFk/setFk delegando, constante FK_PESSOA_PRODUTO |
| DepartamentoPessoaSelect | novo arquivo | Action principal + Sort + QuickSearch, listV() com memoization, filtro em sessão, callback via CALLBACK_URL + "_PESSOA" |
| DepartamentoProdutoForm | editar | 3 imports, 1 linha no form(), método fk_pessoa_produto(), inner class Fk_pessoa_produto |
| DepartamentoManager | editar | 1 import, 4 regAction (1 pro callback, 3 pro Select) |
| Banco | — | Nada — coluna fk_pessoa_produto já existia |
1 arquivo novo + 3 edits, nenhuma mudança de schema. Resultado: campo "Responsável" no form do Produto com modal de seleção de Pessoa, callback que atualiza texto e FK, e o UPDATE do Produto grava a FK junto com os outros campos.