No Master/Detail a Pessoa exibia, dentro do próprio form, a lista de produtos que tinha recebido — a navegação do vínculo N-N era de cima pra baixo (Pessoa → Produtos). Detail Recíproco é o caminho inverso: agora o form do Produto exibe, em baixo, a lista de pessoas que receberam aquele produto. Mais a lista principal de produtos ganha uma coluna "Pessoas" mostrando quantas pessoas estão vinculadas a cada item.
O conceito-chave é simetria: a tabela de junção departamento.produto_pessoa não tem dono. O vínculo pode ser navegado dos dois lados. O que aprendi no Master/Detail (pessoa puxando produtos) vale espelhado pro produto puxando pessoas — fk_pessoa vira fk_produto, qt_recebida permanece em ambos os lados, a estrutura do detail é a mesma trocando colunas.
9 arquivos editados, 0 novos. Nenhuma alteração no banco — a tabela de junção já existia. O que entra é só leitura espelhada (subqueries, filtros, ListView), mais 3 bugs descobertos no caminho que ganharam soluções genéricas pro Jasap.
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";
public static String TABLE_PRODUTO_PESSOA = "departamento.produto_pessoa";
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 Integer qt_recebida;
private Integer ct_pessoas;
@DBInfo(serial=true, pk=true)
public Integer getId_produto() { return id_produto; }
public void setId_produto(Integer id_produto) { this.id_produto = id_produto; }
public String getNome_produto() { return nome_produto; }
public void setNome_produto(String nome_produto) { this.nome_produto = nome_produto; }
public Double getVl_produto() { return vl_produto; }
public void setVl_produto(Double vl_produto) { this.vl_produto = vl_produto; }
public Integer getQtd_produto() { return qtd_produto; }
public void setQtd_produto(Integer qtd_produto) { this.qtd_produto = qtd_produto; }
public String getObs_produto() { return obs_produto; }
public void setObs_produto(String obs_produto) { this.obs_produto = obs_produto; }
public String getQs_produto() { return qs_produto; }
public void setQs_produto(String qs_produto) { this.qs_produto = qs_produto; }
public Integer getSt_produto() { return st_produto; }
public void setSt_produto(Integer st_produto) { this.st_produto = st_produto; }
public Integer getInsert_chk() { return insert_chk; }
public void setInsert_chk(Integer insert_chk) { this.insert_chk = insert_chk; }
public Integer getQt_recebida() { return qt_recebida; }
public void setQt_recebida(Integer qt_recebida) { this.qt_recebida = qt_recebida; }
public Integer getCt_pessoas() { return ct_pessoas; }
public void setCt_pessoas(Integer ct_pessoas) { this.ct_pessoas = ct_pessoas; }
public static class ProPessoaBean implements Serializable {
private Integer fk_produto;
private Integer fk_pessoa;
private Integer qt_recebida;
@DBInfo(pk=true)
public Integer getFk_produto() { return fk_produto; }
public void setFk_produto(Integer v) { fk_produto = v; }
@DBInfo(pk=true)
public Integer getFk_pessoa() { return fk_pessoa; }
public void setFk_pessoa(Integer v) { fk_pessoa = v; }
public Integer getQt_recebida() { return qt_recebida; }
public void setQt_recebida(Integer v) { qt_recebida = v; }
}
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";
public static String QT_RECEBIDA = "qt_recebida";
public static String CT_PESSOAS = "ct_pessoas";
public static String FK_PRODUTO = "fk_produto";
public static String FK_PESSOA = "fk_pessoa";
}
Adições: campo transient ct_pessoas + getter/setter + constante CT_PESSOAS. Sem @DBInfo — não corresponde a coluna física.
package br.xt.app.departamento.produto;
import br.jasap.core.AppManager;
import br.jasap.dao.Query;
import br.jasap.dao.SQL;
import br.jasap.util.JasapFunctions;
import br.jasap.util.JasapList;
import br.xt.AppsRootDAO;
public class DepartamentoProdutoDAO extends AppsRootDAO {
public DepartamentoProdutoDAO() {
}
public DepartamentoProdutoDAO(AppManager manager) {
setManager(manager);
setDataBase(manager.getDataBase());
}
public void daoList(JasapList list) throws Exception {
StringBuilder sql = new StringBuilder();
DepartamentoProdutoWBean filtro = (DepartamentoProdutoWBean) list.getFiltro();
String qtRecebidaCol = "";
if (filtro != null && filtro.getFk_pessoa() != null) {
qtRecebidaCol = ", (SELECT pp.qt_recebida FROM " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA
+ " pp WHERE pp.fk_produto = a.id_produto AND pp.fk_pessoa = " + SQL.value(filtro.getFk_pessoa())
+ ") AS qt_recebida";
}
String ctPessoasCol = ", (SELECT COUNT(*) FROM " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA
+ " pp WHERE pp.fk_produto = a.id_produto) AS ct_pessoas";
sql.append("select a.*" + qtRecebidaCol + ctPessoasCol + " from " + DepartamentoProdutoBean.TABLE + " a");
sql.append(" where 1=1 <where> <orderby>");
Query query = getDataBase().getQuery(sql.toString());
daoWhere(list.getFiltro());
select().executeList(query, list);
while (query.next()) {
DepartamentoProdutoBean bean = new DepartamentoProdutoBean();
query.populateBean(bean);
list.getList().add(bean);
}
query.release();
}
public void daoInsert(DepartamentoProdutoBean bean) throws Exception {
qs_produto(bean);
insert().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void daoSingle(DepartamentoProdutoBean bean) throws Exception {
select().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void daoUpdate(DepartamentoProdutoBean bean) throws Exception {
qs_produto(bean);
update().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void daoDelete(DepartamentoProdutoBean bean) throws Exception {
delete().execute(bean, DepartamentoProdutoBean.TABLE);
}
public void daoLinkPessoa(Integer id_produto, Integer id_pessoa, Integer qt_recebida) throws Exception {
DepartamentoProdutoBean.ProPessoaBean b = new DepartamentoProdutoBean.ProPessoaBean();
b.setFk_produto(id_produto);
b.setFk_pessoa(id_pessoa);
b.setQt_recebida(qt_recebida);
insert().execute(b, DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA);
daoBaixaEstoque(id_produto, qt_recebida);
}
public void daoUnlinkPessoa(Integer id_produto, Integer id_pessoa) throws Exception {
Integer qt = daoSingleVinculo(id_produto, id_pessoa);
DepartamentoProdutoBean.ProPessoaBean b = new DepartamentoProdutoBean.ProPessoaBean();
b.setFk_produto(id_produto);
b.setFk_pessoa(id_pessoa);
delete().execute(b, DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA);
if (qt != null && qt > 0) daoBaixaEstoque(id_produto, -qt);
}
public Integer daoSingleVinculo(Integer id_produto, Integer id_pessoa) throws Exception {
Query query = getDataBase().getQuery(
"select qt_recebida from " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA +
" where fk_produto=" + SQL.value(id_produto) +
" and fk_pessoa=" + SQL.value(id_pessoa) + " <where> <orderby>");
select().executeList(query, new JasapList());
Integer qt = null;
if (query.next()) qt = query.getInteger("qt_recebida");
query.release();
return qt;
}
public void daoUpdateVinculo(Integer id_produto, Integer id_pessoa, Integer qt_nova) throws Exception {
Integer qt_antiga = daoSingleVinculo(id_produto, id_pessoa);
if (qt_antiga == null) qt_antiga = 0;
Integer delta = qt_nova - qt_antiga;
Query upd = getDataBase().getQuery(
"update " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA +
" set qt_recebida=" + SQL.value(qt_nova) +
" where fk_produto=" + SQL.value(id_produto) +
" and fk_pessoa=" + SQL.value(id_pessoa));
getDataBase().executeUpdate(upd);
if (delta != 0) daoBaixaEstoque(id_produto, delta);
}
private void daoBaixaEstoque(Integer id_produto, Integer delta) throws Exception {
if (delta == 0) return;
String op = delta > 0 ? " - " : " + ";
Integer abs = Math.abs(delta);
Query upd = getDataBase().getQuery(
"update " + DepartamentoProdutoBean.TABLE +
" set " + DepartamentoProdutoBean.QTD_PRODUTO +
" = COALESCE(" + DepartamentoProdutoBean.QTD_PRODUTO + ", 0)" + op + SQL.value(abs) +
" where " + DepartamentoProdutoBean.ID_PRODUTO + "=" + SQL.value(id_produto));
getDataBase().executeUpdate(upd);
}
public Integer daoEstoqueAtual(Integer id_produto) throws Exception {
Query query = getDataBase().getQuery(
"select " + DepartamentoProdutoBean.QTD_PRODUTO + " as qtd from " + DepartamentoProdutoBean.TABLE +
" where " + DepartamentoProdutoBean.ID_PRODUTO + "=" + SQL.value(id_produto) + " <where> <orderby>");
select().executeList(query, new JasapList());
Integer qt = null;
if (query.next()) qt = query.getInteger("qtd");
query.release();
return qt != null ? qt : 0;
}
public Integer daoCountPessoasVinculadas(Integer id_produto) throws Exception {
JasapList list = new JasapList();
Query query = getDataBase().getQuery(
"select count(*) as qt from " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA +
" where fk_produto=" + SQL.value(id_produto) + " <where> <orderby>");
select().executeList(query, list);
if (query.next()) {
Integer qt = query.getInteger("qt");
query.release();
return qt != null ? qt : 0;
}
query.release();
return 0;
}
public void qs_produto(DepartamentoProdutoBean bean) throws Exception {
bean.setQs_produto(JasapFunctions.searchString(
bean.getNome_produto() + " " +
bean.getObs_produto()
));
}
public void daoWhere(Object objWhere) throws Exception {
String where = "";
if (objWhere != null) {
DepartamentoProdutoWBean filtro = (DepartamentoProdutoWBean) objWhere;
if (filtro.getSt_produto() != null)
where = SQL.and(where, SQL.equals(SQL.column(DepartamentoProdutoBean.ST_PRODUTO), SQL.value(filtro.getSt_produto())));
if (filtro.getFk_pessoa() != null)
where = SQL.and(where, "a." + DepartamentoProdutoBean.ID_PRODUTO + " IN (SELECT fk_produto FROM " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA + " WHERE fk_pessoa=" + SQL.value(filtro.getFk_pessoa()) + ")");
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);
}
}
}
Adições: subquery COUNT sempre presente no daoList, populando ct_pessoas. Diferente do qtRecebidaCol (condicional ao filtro), o ctPessoasCol é fixo: toda listagem de produto traz a contagem.
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;
import br.jasap.util.JasapFunctions;
import br.jasap.util.Js;
import br.jasap.util.ModalConfig;
import br.jasap.util.exceptions.SQLConstraintException;
import br.xt.acore.view.IconButton;
import br.xt.acore.view.XtPage;
public class DepartamentoProdutoList extends DepartamentoProdutoAction {
@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();
}
public void render() throws Exception {
XtPage page = new XtPage(getManager());
if (isAjaxCall()) {
update(JasapPage.DIV_WINDOW, page.content(window().toHtml()));
} else {
page.getTable()
.setBorder(4)
.rowC("100%")
.setContent(page.content(window().toHtml()))
.setStyle(ui().stretchBorder());
page.setWinTitle("Produtos");
getOutput().write(this, page);
}
}
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);
}
protected Text qs_produto = null;
public String qs_produto() throws Exception {
qs_produto = ui().text(DepartamentoProdutoBean.QS_PRODUTO)
.setLabel("Consulta")
.setStyle("width:400;height:30px;font-size:14px")
.setMaxlength(300)
.setValue(getFiltro().getQs_produto())
.setOnkeyup(Js.pressEnter(
link(QuickSearch.class)
.putScript(DepartamentoProdutoBean.QS_PRODUTO, Js.SELF_VALUE)
.ajax()));
Table aux = new Table(getManager(), "100%", "45")
.setStyle(ui().bgDark())
.rowC()
.setAlign(Table.ALIGN_CENTER)
.setVerticalAlign(Table.ALIGN_MIDDLE)
.setContent(qs_produto.toHtml() + "<span class=\"glyphicon glyphicon-search xt-ico-white\"></span>")
.table();
return aux.toHtml();
}
public Bar br() throws Exception {
Button cmd_novo = ui().button(" Novo Registro ").setCss("btn btn-success btn-lg").setNoSize()
.setOnClick(link(DepartamentoProdutoForm.ShowInsert.class)
.modal(new ModalConfig().setWidth("750").setHeight("570")
.setOnCloseURL(url(Sort.class))));
return ui().bar()
.addCenter(lView().nav(lView().getNavForm()))
.addRight(cmd_novo);
}
private ListView lv = null;
public ListView lView() throws Exception {
if (lv == null) {
lv = ui().lView();
String orderBy = getSession().getString(LIST.concat(lv.getORDER_BY()), getInput());
if (orderBy == null) orderBy = DepartamentoProdutoBean.NOME_PRODUTO;
lv.setSortAct(url(Sort.class))
.setPageAction(url(Sort.class))
.setPage(getSession().getInteger(LIST.concat(lv.getPAGE()), getInput()))
.setOrderBy(orderBy)
.setSort(getSession().getString(LIST.concat(lv.getSORT()), getInput()))
.ajax();
lv.setPageSize(50);
if (getSession().isSet(RESET_PAGE)) {
lv.setPage(1);
getSession().remove(RESET_PAGE);
}
lv.setFiltro(getFiltro());
getFactory().departamento().proModel().daoList(lv.getData());
ListColumn col_nome = lv.newColumn("Nome").setOrderBy(DepartamentoProdutoBean.NOME_PRODUTO).setWidth(220).setPadding(";padding:10 8 10 8;");
ListColumn col_vl = lv.newColumn("Valor").setOrderBy(DepartamentoProdutoBean.VL_PRODUTO).setWidth(100).setPadding(";padding:10 8 10 8;").alignCenter();
ListColumn col_qtd = lv.newColumn("Qtd").setOrderBy(DepartamentoProdutoBean.QTD_PRODUTO).setWidth(80).setPadding(";padding:10 8 10 8;").alignCenter();
ListColumn col_pessoas = lv.newColumn("Pessoas").setWidth(80).setPadding(";padding:10 8 10 8;").alignCenter();
ListColumn col_obs = lv.newColumn("Observação").setOrderBy(DepartamentoProdutoBean.OBS_PRODUTO).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();
line.setOnclick(link(DepartamentoProdutoForm.ShowUpdate.class)
.putInteger(DepartamentoProdutoBean.ID_PRODUTO, bean.getId_produto())
.modal(new ModalConfig().setWidth("1000").setHeight("720")
.setOnCloseURL(url(Sort.class))));
col_nome.setContent(bean.getNome_produto());
col_vl.setContent(bean.getVl_produto());
col_qtd.setContent(bean.getQtd_produto());
col_pessoas.setContent(bean.getCt_pessoas());
col_obs.setContent(bean.getObs_produto());
col_del.setHtmlData(new IconButton("trash")
.setColor("#d9534f")
.setTitle("Excluir")
.setOnclick(link(DeleteFromList.class).putInteger(DepartamentoProdutoBean.ID_PRODUTO, bean.getId_produto()).ajax())
.toHtml());
lv.addLine(line);
}
}
return lv;
}
public DepartamentoProdutoWBean getFiltro() throws Exception {
DepartamentoProdutoWBean filtro = (DepartamentoProdutoWBean) getSession().getObject(FILTRO);
if (filtro == null) {
filtro = proWBean();
getSession().addObj(FILTRO, filtro);
}
return filtro;
}
public static class Sort extends DepartamentoProdutoList { /* ... */ }
public static class QuickSearch extends DepartamentoProdutoList { /* ... */ }
public static class TabAtivos extends DepartamentoProdutoList { /* ... */ }
public static class TabInativos extends DepartamentoProdutoList { /* ... */ }
public static class TabArquivados extends DepartamentoProdutoList { /* ... */ }
public static class TabTodos extends DepartamentoProdutoList { /* ... */ }
public static class DeleteFromList extends DepartamentoProdutoList { /* ... */ }
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");
}
Adições: 1 coluna nova "Pessoas" entre Qtd e Observação (sem orderBy — coluna calculada via subquery), modal do ShowUpdate aumentado pra 1000×720 pra caber o detail. As inner classes (Sort, QuickSearch, TabAtivos, etc.) ficam idênticas — omitidas pra economia visual.
package br.xt.app.departamento.pessoa;
import br.jasap.dao.DBInfo;
import br.jasap.util.DomainValue;
import br.jasap.util.JasapList;
import java.io.Serializable;
public class DepartamentoPessoaBean implements Serializable {
public static String TABLE = "departamento.pessoa";
private Integer id_pessoa;
private String nome_pessoa;
private String apelido_pessoa;
private String cpf_pessoa;
private String obs_pessoa;
private String qs_pessoa;
private Integer st_pessoa;
private Integer insert_chk;
private Integer fk_produto;
private Integer qt_recebida;
@DBInfo(serial=true, pk=true)
public Integer getId_pessoa() { return id_pessoa; }
public void setId_pessoa(Integer id_pessoa) { this.id_pessoa = id_pessoa; }
public String getNome_pessoa() { return nome_pessoa; }
public void setNome_pessoa(String nome_pessoa) { this.nome_pessoa = nome_pessoa; }
public String getApelido_pessoa() { return apelido_pessoa; }
public void setApelido_pessoa(String apelido_pessoa) { this.apelido_pessoa = apelido_pessoa; }
public String getCpf_pessoa() { return cpf_pessoa; }
public void setCpf_pessoa(String cpf_pessoa) { this.cpf_pessoa = cpf_pessoa; }
public String getObs_pessoa() { return obs_pessoa; }
public void setObs_pessoa(String obs_pessoa) { this.obs_pessoa = obs_pessoa; }
public String getQs_pessoa() { return qs_pessoa; }
public void setQs_pessoa(String qs_pessoa) { this.qs_pessoa = qs_pessoa; }
public Integer getSt_pessoa() { return st_pessoa; }
public void setSt_pessoa(Integer st_pessoa) { this.st_pessoa = st_pessoa; }
public Integer getInsert_chk() { return insert_chk; }
public void setInsert_chk(Integer insert_chk) { this.insert_chk = insert_chk; }
public Integer getFk_produto() { return fk_produto; }
public void setFk_produto(Integer fk_produto) { this.fk_produto = fk_produto; }
public Integer getQt_recebida() { return qt_recebida; }
public void setQt_recebida(Integer qt_recebida) { this.qt_recebida = qt_recebida; }
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_PESSOA = "id_pessoa";
public static String NOME_PESSOA = "nome_pessoa";
public static String APELIDO_PESSOA = "apelido_pessoa";
public static String CPF_PESSOA = "cpf_pessoa";
public static String OBS_PESSOA = "obs_pessoa";
public static String QS_PESSOA = "qs_pessoa";
public static String ST_PESSOA = "st_pessoa";
public static String INSERT_CHK = "insert_chk";
public static String FK_PRODUTO = "fk_produto";
public static String QT_RECEBIDA = "qt_recebida";
}
Adições: 2 campos transient — fk_produto (filtro) e qt_recebida (subquery). Sem @DBInfo — não correspondem a colunas físicas da tabela departamento.pessoa.
package br.xt.app.departamento.pessoa;
import br.jasap.core.AppManager;
import br.jasap.dao.Query;
import br.jasap.dao.SQL;
import br.jasap.util.JasapFunctions;
import br.jasap.util.JasapList;
import br.xt.AppsRootDAO;
import br.xt.app.departamento.produto.DepartamentoProdutoBean;
public class DepartamentoPessoaDAO extends AppsRootDAO {
public DepartamentoPessoaDAO() {
}
public DepartamentoPessoaDAO(AppManager manager) {
setManager(manager);
setDataBase(manager.getDataBase());
}
public void daoList(JasapList list) throws Exception {
StringBuilder sql = new StringBuilder();
DepartamentoPessoaWBean filtro = (DepartamentoPessoaWBean) list.getFiltro();
String qtRecebidaCol = "";
if (filtro != null && filtro.getFk_produto() != null) {
qtRecebidaCol = ", (SELECT pp.qt_recebida FROM " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA
+ " pp WHERE pp.fk_pessoa = a.id_pessoa AND pp.fk_produto = " + SQL.value(filtro.getFk_produto())
+ ") AS qt_recebida";
}
sql.append("select a.*" + qtRecebidaCol + " from " + DepartamentoPessoaBean.TABLE + " a");
sql.append(" where 1=1 <where> <orderby>");
Query query = getDataBase().getQuery(sql.toString());
daoWhere(list.getFiltro());
select().executeList(query, list);
while (query.next()) {
DepartamentoPessoaBean bean = new DepartamentoPessoaBean();
query.populateBean(bean);
list.getList().add(bean);
}
query.release();
}
public void daoInsert(DepartamentoPessoaBean bean) throws Exception {
qs_pessoa(bean);
insert().execute(bean, DepartamentoPessoaBean.TABLE);
}
public void daoSingle(DepartamentoPessoaBean bean) throws Exception {
select().execute(bean, DepartamentoPessoaBean.TABLE);
}
public void daoUpdate(DepartamentoPessoaBean bean) throws Exception {
qs_pessoa(bean);
update().execute(bean, DepartamentoPessoaBean.TABLE);
}
public void daoDelete(DepartamentoPessoaBean bean) throws Exception {
delete().execute(bean, DepartamentoPessoaBean.TABLE);
}
public void qs_pessoa(DepartamentoPessoaBean bean) throws Exception {
bean.setQs_pessoa(JasapFunctions.searchString(
bean.getNome_pessoa() + " " +
bean.getApelido_pessoa() + " " +
bean.getCpf_pessoa() + " " +
bean.getObs_pessoa()
));
}
public void daoWhere(Object objWhere) throws Exception {
String where = "";
if (objWhere != null) {
DepartamentoPessoaWBean filtro = (DepartamentoPessoaWBean) objWhere;
if (filtro.getSt_pessoa() != null)
where = SQL.and(where, SQL.equals(SQL.column(DepartamentoPessoaBean.ST_PESSOA), SQL.value(filtro.getSt_pessoa())));
if (filtro.getFk_produto() != null)
where = SQL.and(where, "a." + DepartamentoPessoaBean.ID_PESSOA + " IN (SELECT fk_pessoa FROM " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA + " WHERE fk_produto=" + SQL.value(filtro.getFk_produto()) + ")");
if (filtro.getQs_pessoa() != null && !JasapFunctions.equals(filtro.getQs_pessoa(), "")) {
String[] cols = { SQL.column(DepartamentoPessoaBean.QS_PESSOA) };
where = where + qsWhere(filtro.getQs_pessoa(), cols);
}
filtro.setWhere(where);
}
}
}
Adições: import do DepartamentoProdutoBean, subquery qt_recebida condicional ao filtro fk_produto, filtro IN (SELECT ...) no daoWhere. Espelho das adições do ProdutoDAO, índices trocados.
package br.xt.app.departamento.produto;
import br.jasap.core.Effect;
import br.jasap.effect.Response;
import br.jasap.gui.Bar;
import br.jasap.gui.Button;
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.Toast;
import br.jasap.gui.form.Check;
import br.jasap.gui.form.Form;
import br.jasap.gui.form.Radio;
import br.jasap.gui.form.Text;
import br.jasap.gui.form.Textarea;
import br.jasap.util.DomainValue;
import br.jasap.util.JasapFunctions;
import br.jasap.util.JasapList;
import br.jasap.util.Js;
import br.jasap.util.ModalConfig;
import br.jasap.util.exceptions.SQLConstraintException;
import br.jasap.util.filters.TransactionFilter;
import br.xt.acore.view.XtPage;
import br.xt.app.departamento.pessoa.DepartamentoPessoaBean;
import br.xt.app.departamento.pessoa.DepartamentoPessoaForm;
import br.xt.app.departamento.pessoa.DepartamentoPessoaWBean;
public class DepartamentoProdutoForm extends DepartamentoProdutoAction {
@Override
public Effect execute() throws Exception { render(); return new Response(); }
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);
if (fromPessoa != null) getSession().addInt(S_FROM_PESSOA, fromPessoa);
else getSession().remove(S_FROM_PESSOA);
proBean().setInsert_chk(getInput().getInteger(DepartamentoProdutoBean.INSERT_CHK));
render();
return new Response();
}
}
public static class Insert extends DepartamentoProdutoAction {
public Insert() { super.getFilters().add(new TransactionFilter()); }
@Override
public Effect execute() throws Exception {
Integer chk = proBean().getInsert_chk();
proBean().setInsert_chk(null);
getFactory().departamento().proModel().insert(proBean());
Integer fromPessoa = getSession().getInteger(S_FROM_PESSOA);
if (fromPessoa != null) {
getSession().remove(S_FROM_PESSOA);
String onCloseUrl;
if (JasapFunctions.equals(chk, 1)) {
getSession().addInt(DepartamentoPessoaForm.S_CONTINUAR_FROM_PESSOA, 1);
onCloseUrl = url(DepartamentoPessoaForm.AfterEntregaContinuar.class);
} else {
onCloseUrl = url(DepartamentoPessoaForm.RefreshDetail.class);
}
evalParent(Js.CLOSE_SUB_WINDOWS + " " + link(DepartamentoVinculoForm.ShowInsert.class)
.putInteger(DepartamentoProdutoBean.ID_PRODUTO, proBean().getId_produto())
.putInteger(DepartamentoPessoaBean.ID_PESSOA, fromPessoa)
.modal(new ModalConfig().setWidth("520").setHeight("420").setOnCloseURL(onCloseUrl)));
return new Response();
}
if (JasapFunctions.equals(chk, 1)) {
eval(link(ShowInsert.class).putInteger(DepartamentoProdutoBean.INSERT_CHK, 1).noWait().ajax());
evalParent(link(DepartamentoProdutoList.Sort.class).noWait().ajax(false));
} else {
evalParent(Js.CLOSE_SUB_WINDOWS);
}
return new Response();
}
}
public static class ShowUpdate extends DepartamentoProdutoForm {
@Override
public Effect execute() throws Exception {
setUpdate(FORM);
proBean().setId_produto(getInput().getInteger(DepartamentoProdutoBean.ID_PRODUTO));
getSession().addInt(ROOT.concat(DepartamentoProdutoBean.ID_PRODUTO), proBean().getId_produto());
getFactory().departamento().proModel().daoSingle(proBean());
render();
return new Response();
}
}
public static class RefreshDetail extends DepartamentoProdutoForm {
@Override
public Effect execute() throws Exception {
update(JasapPage.DIV_DETAIL, pesDetail());
return new Response();
}
}
public static class DetailChangePage extends DepartamentoProdutoForm {
@Override
public Effect execute() throws Exception {
update(JasapPage.DIV_DETAIL, pesDetail());
return new Response();
}
}
public static class DesvincularPes extends DepartamentoProdutoForm {
public DesvincularPes() { super.getFilters().add(new TransactionFilter()); }
@Override
public Effect execute() throws Exception {
Integer id_pessoa = getInput().getInteger(DepartamentoPessoaBean.ID_PESSOA);
if (JasapFunctions.equals(getInput().getInteger(CONFIRM), 1)) {
getFactory().departamento().proModel().daoUnlinkPessoa(id_produto(), id_pessoa);
update(JasapPage.DIV_DETAIL, pesDetail());
} else {
Integer qt = getFactory().departamento().proModel().daoSingleVinculo(id_produto(), id_pessoa);
if (qt == null) qt = 0;
eval(Js.swalConfirm("Desvincular essa pessoa? Devolverá " + qt + " unidade(s) ao estoque.",
submit(DesvincularPes.class)
.putInteger(CONFIRM, 1)
.putInteger(DepartamentoPessoaBean.ID_PESSOA, id_pessoa)
.ajax().toHtml(), ""));
}
return new Response();
}
}
public static class Update extends DepartamentoProdutoAction { /* ... */ }
public static class Cancelar extends DepartamentoProdutoForm { /* ... */ }
public static class Delete extends DepartamentoProdutoAction { /* ... */ }
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("Produto");
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("CADASTRO DE PRODUTO"));
w.rowC("1%", null, ui().line());
w.rowC("1%", JasapPage.DIV_BOTTOM, br());
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(pesDetail());
} else {
w.rowC("auto").setId(JasapPage.DIV_MASTER).setContent(form()).table();
}
return w;
}
public Table pesDetail() throws Exception {
ListView lv = ui().lView();
lv.setPageAction(url(DetailChangePage.class))
.setPage(getSession().getInteger(S_DET.concat(lv.getPAGE()), getInput()))
.ajax();
DepartamentoPessoaWBean filtro = new DepartamentoPessoaWBean();
filtro.setFk_produto(id_produto());
lv.setFiltro(filtro);
getFactory().departamento().pesModel().daoList(lv.getData());
ListColumn col_nome = lv.newColumn("Nome").setWidth(220).setPadding(";padding:10 8 10 8;");
ListColumn col_apelido = lv.newColumn("Apelido").setWidth(150).setPadding(";padding:10 8 10 8;");
ListColumn col_cpf = lv.newColumn("CPF").setWidth(120).setPadding(";padding:10 8 10 8;").alignCenter();
ListColumn col_recebida = lv.newColumn("Recebida").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()) {
DepartamentoPessoaBean bean = (DepartamentoPessoaBean) lv.next();
ListLine line = lv.createLine();
col_nome.setContent(bean.getNome_pessoa());
col_apelido.setContent(bean.getApelido_pessoa());
col_cpf.setContent(bean.getCpf_pessoa());
col_recebida.setContent(bean.getQt_recebida());
col_obs.setContent(bean.getObs_pessoa());
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(DesvincularPes.class).putInteger(DepartamentoPessoaBean.ID_PESSOA, bean.getId_pessoa()).ajax() + "\">×</button>");
lv.addLine(line);
}
Table aux = new Table(getManager()).setSize("100%", "100%");
aux.rowC("99%", null, lv);
aux.rowC("1%", null, ui().bar().addCenter(lv.nav(lv.getNavForm())).toHtml());
return aux;
}
/* ... br(), form(), getters de campo (nome_produto, vl_produto, qtd_produto, st_produto, obs_produto, insert_chk) — sem alteração ... */
public static String FORM = ROOT.concat("__FORM");
public static String CONFIRM = ROOT.concat("__CONFIRM");
public static String S_FROM_PESSOA = ROOT.concat("__FROM_PESSOA");
public static String S_DET = ROOT.concat("__DET/");
}
Adições: 7 imports, 3 inner classes (RefreshDetail, DetailChangePage, DesvincularPes), Insert alterado pra desviar fluxo "Novo Produto via Pessoa", window() divide em master/detail no update, método novo pesDetail(), 1 constante. Métodos secundários (br(), form() e os getters de campo) ficam idênticos — omitidos.
package br.xt.app.departamento.produto;
import br.jasap.core.Effect;
import br.jasap.effect.Response;
import br.jasap.gui.Bar;
import br.jasap.gui.Button;
import br.jasap.gui.JasapPage;
import br.jasap.gui.Table;
import br.jasap.gui.Toast;
import br.jasap.gui.form.Form;
import br.jasap.gui.form.Text;
import br.jasap.util.JasapFunctions;
import br.jasap.util.Js;
import br.jasap.util.ModalConfig;
import br.jasap.util.filters.TransactionFilter;
import br.xt.AppsRootAction;
import br.xt.acore.view.XtPage;
import br.xt.app.departamento.DepartamentoManager;
import br.xt.app.departamento.pessoa.DepartamentoPessoaBean;
import br.xt.app.departamento.pessoa.DepartamentoPessoaForm;
public class DepartamentoVinculoForm extends AppsRootAction {
public static final String ROOT = DepartamentoManager.F_ACESSO_MODULO.concat("__VINCULO/");
public Integer id_produto() throws Exception { return getSession().getInteger(ROOT.concat(DepartamentoProdutoBean.ID_PRODUTO)); }
public Integer id_pessoa() throws Exception { return getSession().getInteger(ROOT.concat(DepartamentoPessoaBean.ID_PESSOA)); }
@Override
public Effect execute() throws Exception { render(); return new Response(); }
public static class ShowInsert extends DepartamentoVinculoForm { /* ... carrega produto, qt=1, render ... */ }
public static class ShowUpdate extends DepartamentoVinculoForm { /* ... carrega produto + qt existente, render ... */ }
public static class Save extends DepartamentoVinculoForm { /* ... valida qt, daoLinkPessoa OU daoUpdateVinculo, close ... */ }
public static class Delete extends DepartamentoVinculoForm { /* ... swalConfirm, daoUnlinkPessoa, close ... */ }
public static class Cancelar extends DepartamentoVinculoForm { /* ... close ... */ }
public static class RefreshAfterEditProduto extends DepartamentoVinculoForm { /* ... */ }
public void render() throws Exception { /* ... XtPage + window + modalBorder ... */ }
public Table window() throws Exception { /* ... title + line + master + line + bottom ... */ }
public String form() throws Exception { /* ... nome_produto + estoque_atual + qt_recebida ... */ }
protected Bar br() throws Exception {
Button salvar = ui().button(" Salvar ").setCss("btn btn-success btn-lg").setNoSize()
.setOnClick(submit(Save.class).validate().toHtml());
Button excluir = ui().button(" Desvincular ").setCss("btn btn-danger btn-lg").setNoSize()
.setOnClick(link(Delete.class).ajax())
.setVisible(isUpdate(FORM));
Button cancelar = ui().button(" Cancelar ").setCss("btn btn-primary btn-lg").setNoSize()
.setOnClick(link(Cancelar.class).ajax());
Button editProd = ui().button(" Editar Produto ").setCss("btn btn-warning btn-lg").setNoSize()
.setOnClick(link(DepartamentoProdutoForm.ShowUpdate.class)
.putInteger(DepartamentoProdutoBean.ID_PRODUTO, id_produto())
.setTarget("parent")
.modal(new ModalConfig().setId("IMODAL_EDIT_PRO").setWidth("1000").setHeight("720").setOnCloseURL(url(DepartamentoPessoaForm.RefreshDetail.class))));
Bar bar = ui().bar();
bar.addLeft(editProd);
bar.addRight(cancelar);
bar.addRight(" ");
bar.addRight(excluir);
bar.addRight(" ");
bar.addRight(salvar);
return bar;
}
/* ... getProduto(), getVinculo() — sem alteração ... */
protected Text nome_produto = null;
public Text nome_produto() throws Exception {
if (nome_produto == null) {
nome_produto = ui().text("__nome_produto_readonly")
.setLabel("Produto")
.setStyle("width:350;height:25;background-color:#eee;color:#666;cursor:not-allowed")
.setValue(getProduto().getNome_produto())
.setReadonly(true);
}
return nome_produto;
}
protected Text estoque_atual = null;
public Text estoque_atual() throws Exception {
if (estoque_atual == null) {
estoque_atual = ui().text("__estoque_atual_readonly")
.setLabel("Estoque atual")
.setStyle("width:80;text-align:right;height:25;background-color:#eee;color:#666;cursor:not-allowed")
.setValue(getProduto().getQtd_produto())
.setReadonly(true);
}
return estoque_atual;
}
/* ... qt_recebida() — sem alteração ... */
public static String FORM = ROOT.concat("__FORM");
public static String CONFIRM = ROOT.concat("__CONFIRM");
public static String QT_FIELD = "qt_recebida";
}
Adições: 1 import, setTarget("parent") + setId("IMODAL_EDIT_PRO") + setOnCloseURL trocado no botão "Editar Produto" (resolvem os 3 bugs do modal aninhado), CSS readonly visual nos campos nome_produto e estoque_atual. Inner classes (ShowInsert, ShowUpdate, Save, Delete, Cancelar, RefreshAfterEditProduto) e os métodos render()/window()/form() ficam idênticos — omitidos.
package br.xt.app.departamento.pessoa;
// ... imports (incluindo DepartamentoVinculoForm, ModalConfig, Toast, etc.) ...
public class DepartamentoPessoaForm extends DepartamentoPessoaAction {
@Override
public Effect execute() throws Exception { render(); return new Response(); }
public static class ShowInsert extends DepartamentoPessoaForm { /* ... */ }
public static class Insert extends DepartamentoPessoaAction { /* ... insert + insert_chk + close ... */ }
public static class ShowUpdate extends DepartamentoPessoaForm { /* ... carrega bean por id, render ... */ }
public static class AddExistingPro extends DepartamentoPessoaForm {
@Override
public Effect execute() throws Exception {
Integer id_produto = getInput().getInteger(DepartamentoProdutoBean.ID_PRODUTO);
Integer existente = getFactory().departamento().proModel().daoSingleVinculo(id_produto, id_pessoa());
if (existente != null) {
evalParent(new Toast("Produto já vinculado a esta pessoa.").fail() + "; " + Js.CLOSE_SUB_WINDOWS);
return new Response();
}
evalParent(Js.CLOSE_SUB_WINDOWS + " " + link(DepartamentoVinculoForm.ShowInsert.class)
.putInteger(DepartamentoProdutoBean.ID_PRODUTO, id_produto)
.putInteger(DepartamentoPessoaBean.ID_PESSOA, id_pessoa())
.modal(new ModalConfig().setWidth("520").setHeight("420").setOnCloseURL(url(RefreshDetail.class))));
return new Response();
}
}
public static class RefreshDetail extends DepartamentoPessoaForm {
@Override
public Effect execute() throws Exception {
update(JasapPage.DIV_DETAIL, proDetail());
return new Response();
}
}
public static class AfterEntregaContinuar extends DepartamentoPessoaForm {
@Override
public Effect execute() throws Exception {
update(JasapPage.DIV_DETAIL, proDetail());
Integer continuar = getSession().getInteger(S_CONTINUAR_FROM_PESSOA);
if (continuar != null && JasapFunctions.equals(continuar, 1)) {
getSession().remove(S_CONTINUAR_FROM_PESSOA);
eval(link(DepartamentoProdutoForm.ShowInsert.class)
.putInteger(DepartamentoProdutoForm.S_FROM_PESSOA, id_pessoa())
.putInteger(DepartamentoProdutoBean.INSERT_CHK, 1)
.modal(new ModalConfig().setWidth("752").setHeight("570").setOnCloseURL(url(RefreshDetail.class))));
}
return new Response();
}
}
public static class SearchPro extends DepartamentoPessoaForm { /* ... busca rápida no detail ... */ }
public static class DetailChangePage extends DepartamentoPessoaForm { /* ... paginação do detail ... */ }
public static class DesvincularPro extends DepartamentoPessoaForm { /* ... swalConfirm + daoUnlinkPessoa ... */ }
public static class Update extends DepartamentoPessoaAction { /* ... update + close ... */ }
public static class Cancelar extends DepartamentoPessoaForm { /* ... close ... */ }
public static class Delete extends DepartamentoPessoaAction { /* ... swalConfirm + daoDelete + close ... */ }
/* ... render(), window(), proDetail(), br(), form() e getters de campo — sem alteração ... */
public static String FORM = ROOT.concat("__FORM");
public static String CONFIRM = ROOT.concat("__CONFIRM");
public static String S_QS_PRODUTO = ROOT.concat("__S_QS_PRODUTO");
public static String S_DET = ROOT.concat("__DET/");
public static String S_CONTINUAR_FROM_PESSOA = ROOT.concat("__CONTINUAR_FROM_PESSOA");
}
Adições: inner class AfterEntregaContinuar nova (fecha o ciclo "continuar inserindo via Pessoa"), AddExistingPro alterado pra combinar os dois evalParent num único call (resolve bug do frame destruído mid-loop), 1 constante de sessão. As outras inner classes (Insert, Update, Delete, etc.) e os métodos auxiliares ficam idênticos — omitidos.
package br.xt.app.departamento;
import br.xt.app.departamento.pessoa.DepartamentoPessoaForm;
import br.xt.app.departamento.pessoa.DepartamentoPessoaList;
import br.xt.app.departamento.produto.DepartamentoProdutoForm;
import br.xt.app.departamento.produto.DepartamentoProdutoList;
import br.xt.app.departamento.produto.DepartamentoProdutoSelect;
import br.xt.app.departamento.produto.DepartamentoVinculoForm;
import br.xt.app.painel.PnlManager;
public class DepartamentoManager extends PnlManager {
public static final String F_ACESSO_MODULO = "XT.PAINEL_CONTROLE.ACESSO_MODULO.DEPARTAMENTO";
@Override
public void config() throws Exception {
regFun("PAINEL DE CONTROLE", "Acesso ao Módulo", "DEPARTAMENTO", F_ACESSO_MODULO);
regAction(DepartamentoHome.class);
regAction(DepartamentoHome.Title.class);
regAction(DepartamentoHome.MenuItem.class);
regAction(DepartamentoHome.MenuInicial.class);
regAction(DepartamentoProdutoList.class);
regAction(DepartamentoProdutoList.Sort.class);
regAction(DepartamentoProdutoList.QuickSearch.class);
regAction(DepartamentoProdutoList.TabAtivos.class);
regAction(DepartamentoProdutoList.TabInativos.class);
regAction(DepartamentoProdutoList.TabArquivados.class);
regAction(DepartamentoProdutoList.TabTodos.class);
regAction(DepartamentoProdutoList.DeleteFromList.class);
regAction(DepartamentoProdutoForm.class);
regAction(DepartamentoProdutoForm.ShowInsert.class);
regAction(DepartamentoProdutoForm.ShowUpdate.class);
regAction(DepartamentoProdutoForm.Insert.class);
regAction(DepartamentoProdutoForm.Update.class);
regAction(DepartamentoProdutoForm.Cancelar.class);
regAction(DepartamentoProdutoForm.Delete.class);
regAction(DepartamentoProdutoForm.RefreshDetail.class);
regAction(DepartamentoProdutoForm.DetailChangePage.class);
regAction(DepartamentoProdutoForm.DesvincularPes.class);
regAction(DepartamentoProdutoSelect.class);
regAction(DepartamentoProdutoSelect.Sort.class);
regAction(DepartamentoProdutoSelect.QuickSearch.class);
regAction(DepartamentoVinculoForm.class);
regAction(DepartamentoVinculoForm.ShowInsert.class);
regAction(DepartamentoVinculoForm.ShowUpdate.class);
regAction(DepartamentoVinculoForm.Save.class);
regAction(DepartamentoVinculoForm.Delete.class);
regAction(DepartamentoVinculoForm.Cancelar.class);
regAction(DepartamentoVinculoForm.RefreshAfterEditProduto.class);
regAction(DepartamentoPessoaList.class);
regAction(DepartamentoPessoaList.Sort.class);
regAction(DepartamentoPessoaList.QuickSearch.class);
regAction(DepartamentoPessoaList.TabAtivos.class);
regAction(DepartamentoPessoaList.TabInativos.class);
regAction(DepartamentoPessoaList.TabArquivados.class);
regAction(DepartamentoPessoaList.TabTodos.class);
regAction(DepartamentoPessoaList.DeleteFromList.class);
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.AddExistingPro.class);
regAction(DepartamentoPessoaForm.RefreshDetail.class);
regAction(DepartamentoPessoaForm.AfterEntregaContinuar.class);
regAction(DepartamentoPessoaForm.SearchPro.class);
regAction(DepartamentoPessoaForm.DetailChangePage.class);
regAction(DepartamentoPessoaForm.DesvincularPro.class);
}
}
Adições: 4 regAction novas — 3 do DepartamentoProdutoForm (refresh, paginação e desvincular do detail) + 1 do DepartamentoPessoaForm (callback do continuar inserindo). Sem isso, viram fantasmas — código existe, URL não responde.
A tabela departamento.produto_pessoa tem 3 colunas: fk_produto, fk_pessoa, qt_recebida. Nenhuma das três é "principal" — a tabela existe pra registrar associações N-N. Na prática isso significa que o vínculo pode ser navegado dos dois lados:
| Lado | Pergunta | SQL gerado |
|---|---|---|
| Pessoa | "Que produtos a pessoa X recebeu?" | SELECT * FROM produto WHERE id_produto IN (SELECT fk_produto FROM produto_pessoa WHERE fk_pessoa=X) |
| Produto | "Que pessoas receberam o produto Y?" | SELECT * FROM pessoa WHERE id_pessoa IN (SELECT fk_pessoa FROM produto_pessoa WHERE fk_produto=Y) |
O Master/Detail original implementava só o lado de cima. O Detail Recíproco pluga o lado de baixo — espelho perfeito.
| Mecanismo | Lado Pessoa (já existia) | Lado Produto (novo) |
|---|---|---|
| Filtro no WBean | fk_pessoa em ProdutoWBean | fk_produto em PessoaBean |
Subquery com qt_recebida | WHERE pp.fk_produto = a.id_produto AND pp.fk_pessoa = ? | WHERE pp.fk_pessoa = a.id_pessoa AND pp.fk_produto = ? |
Filtro IN no daoWhere | id_produto IN (SELECT fk_produto WHERE fk_pessoa = ?) | id_pessoa IN (SELECT fk_pessoa WHERE fk_produto = ?) |
| Detail no Form | proDetail() em PessoaForm | pesDetail() em ProdutoForm |
Mesma estrutura, índices trocados. Quem entendeu o lado da Pessoa não precisa aprender nada novo — só inverter mentalmente.
3 coisas do lado Pessoa que não tem equivalente no Produto Form:
| Item da Pessoa | Por que não tem no Produto |
|---|---|
| Botão "Associar Produto" | Iniciar a entrega do lado da Pessoa faz mais sentido pedagogicamente. Duplicar o fluxo geraria 2 caminhos pra mesma coisa |
| Botão "Novo Produto" | Cadastro de produto novo a partir do detail de uma pessoa específica é caso comum. O inverso (cadastrar pessoa nova a partir de um produto) é raro |
| Barra de busca rápida no detail | Detail do produto raramente tem dezenas de pessoas. Se virar problema, dá pra adicionar depois |
O Detail Recíproco é read + desvincular. Inserções e novos cadastros continuam pelo lado da Pessoa.
ct_pessoas + Detail recíproco no ProdutoForm
Subquery COUNT no daoList popula um campo transient ct_pessoas de cada bean. Visualmente, vira uma coluna nova na lista — entre Qtd e Observação:
String ctPessoasCol = ", (SELECT COUNT(*) FROM " + DepartamentoProdutoBean.TABLE_PRODUTO_PESSOA
+ " pp WHERE pp.fk_produto = a.id_produto) AS ct_pessoas";
sql.append("select a.*" + qtRecebidaCol + ctPessoasCol + " from " + ...);
Por que campo transient (sem @DBInfo)? A coluna não existe em departamento.produto — é resultado de subquery. Mas o framework popula o bean automaticamente quando o nome da coluna do SELECT bate com o nome do setter (setCt_pessoas). Padrão idêntico ao qt_recebida que já estava no Bean por causa do Master/Detail.
Por que sem orderBy? Ordenar por subquery exige reescrever a SQL principal pra incluir a contagem no SELECT de ordenação — mais complexo do que vale a pena pra essa coluna informativa.
Em modo update, a janela do produto agora se divide em master + detail (mesmo padrão do PessoaForm):
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(pesDetail());
} else {
w.rowC("auto").setId(JasapPage.DIV_MASTER).setContent(form()).table();
}
Em modo insert, o detail nem existe — sem id_produto não há vínculos pra mostrar.
O pesDetail() é cópia do proDetail() do PessoaForm com colunas trocadas (Nome, Apelido, CPF, Recebida, Observação) e o filtro fixo em fk_produto = id_produto(). As 3 actions de apoio (RefreshDetail, DetailChangePage, DesvincularPes) são espelhos diretos das do PessoaForm.
Vínculo do N-N envolve qt_recebida + baixa transacional do estoque. Permitir associação dos dois lados duplicaria a UX e os pontos de manutenção. A entrada do vínculo continua só pelo lado da Pessoa (via VinculoForm). O lado do Produto é read-only + desvincular.
No fluxo "Novo Produto" do detail da Pessoa, a versão antiga fazia: cria produto com qtd_produto=12, depois daoLinkPessoa(produto, pessoa, 12) automaticamente. Resultado: estoque criado E imediatamente zerado, todas as 12 unidades indo direto pra pessoa. Confuso — o usuário esperava 12 em estoque inicial e escolher quanto entregar.
Em vez de daoLinkPessoa direto, o Insert do ProdutoForm agora desvia o fluxo:
if (fromPessoa != null) {
getSession().remove(S_FROM_PESSOA);
String onCloseUrl;
if (JasapFunctions.equals(chk, 1)) {
getSession().addInt(DepartamentoPessoaForm.S_CONTINUAR_FROM_PESSOA, 1);
onCloseUrl = url(DepartamentoPessoaForm.AfterEntregaContinuar.class);
} else {
onCloseUrl = url(DepartamentoPessoaForm.RefreshDetail.class);
}
evalParent(Js.CLOSE_SUB_WINDOWS + " " + link(DepartamentoVinculoForm.ShowInsert.class)
.putInteger(DepartamentoProdutoBean.ID_PRODUTO, proBean().getId_produto())
.putInteger(DepartamentoPessoaBean.ID_PESSOA, fromPessoa)
.modal(new ModalConfig().setWidth("520").setHeight("420").setOnCloseURL(onCloseUrl)));
return new Response();
}
Agora o produto é criado com estoque cheio, e o VinculoForm aparece pra escolher quanto entregar. Estoque baixa só na quantidade definida.
O Insert escolhe um onCloseURL diferente conforme o checkbox "Continuar inserindo":
| Checkbox | onCloseURL | Comportamento ao fechar VinculoForm |
|---|---|---|
| Desmarcado | RefreshDetail | Atualiza detail da pessoa. Fim. |
| Marcado | AfterEntregaContinuar | Atualiza detail + reabre ShowInsert com FROM_PESSOA + INSERT_CHK |
AfterEntregaContinuar usa um flag S_CONTINUAR_FROM_PESSOA na sessão pra saber se deve reabrir o ShowInsert ou só fazer refresh. Flag setado pelo Insert antes de redirecionar pro VinculoForm; lido + limpo pelo AfterEntregaContinuar.
O fluxo Pessoa → produto vinculado → "Editar Produto" abre 3 modais aninhados — caso que o laboratório nunca expôs. Três bugs do framework apareceram em sequência. Cada um tem fix genérico, vale pra qualquer feature do Jasap que monte fluxo aninhado profundo.
evalParent empilhado destrói o frame mid-loopSintoma: ao escolher um produto no Select, o modal fechava e nada mais acontecia.
Causa: a action retornava 2 entradas no response.data:
evalParent(Js.CLOSE_SUB_WINDOWS); // (1) fecha o Select
evalParent(link(...).modal(...)); // (2) abre VinculoForm
O JS no cliente itera o array chamando parent.takeAction(json). A entrada (1) executa closeAllFrameModal(1) no parent, que faz document.body.removeChild(fm) — e o frame onde o loop está rodando é destruído. A iteração para. (2) nunca dispara.
Fix: combinar num único evalParent:
evalParent(Js.CLOSE_SUB_WINDOWS + " " + link(...).modal(...));
O parent recebe UMA entrada com 2 instruções concatenadas, executa as duas no contexto dele. O frame de origem pode morrer depois sem prejuízo — o loop já acabou.
Sintoma: ao clicar "Editar Produto" no VinculoForm, o ProdutoForm aparecia cortado dentro de um quadrado de 520×420 (tamanho do VinculoForm), sem header, sem borda do modal.
Causa: o JS ifModal em XTLibJqueryV1.js cria o iframe novo via document.body.appendChild(fm) — body do frame atual, não da raiz. Como o VinculoForm já é um modal, o ProdutoForm vira filho DOM dele e fica limitado ao seu tamanho.
Fix: setTarget("parent") no link do "Editar Produto":
.setOnClick(link(DepartamentoProdutoForm.ShowUpdate.class)
.putInteger(DepartamentoProdutoBean.ID_PRODUTO, id_produto())
.setTarget("parent")
.modal(new ModalConfig().setId("IMODAL_EDIT_PRO")...));
O JS gerado vira parent.ifModal(...). O iframe novo é criado no DOM do PessoaForm (que é maximize) — tem espaço de sobra. O setId também é importante: o id padrão de TODO modal é "IMODAL", e o JS reusaria o iframe do VinculoForm (mesmo id) em vez de criar novo.
Sintoma: ao salvar/excluir/cancelar o ProdutoForm, o cadastro da PessoaForm "virava" o cadastro do produto. A pessoa "sumia" visualmente, mas o registro continuava no banco.
Causa: com setTarget("parent"), o ProdutoForm tem como pai direto o PessoaForm (não mais o VinculoForm). O setOnCloseURL apontava pra RefreshAfterEditProduto (action do VinculoForm). Quando disparado, executa no contexto do PessoaForm, fazendo update(DIV_MASTER, vinculoFormHtml) — sobrescreve o form da pessoa pelo form do vínculo.
Fix: trocar o onCloseURL pra DepartamentoPessoaForm.RefreshDetail:
.modal(new ModalConfig().setId("IMODAL_EDIT_PRO")
.setWidth("1000").setHeight("720")
.setOnCloseURL(url(DepartamentoPessoaForm.RefreshDetail.class)));
Quando o ProdutoForm fecha, o callback executa no contexto do PessoaForm e atualiza o detail dele. Faz sentido — é o efeito desejado depois de editar/excluir um produto vinculado.
| Cenário | Bug 1 | Bug 2 | Bug 3 |
|---|---|---|---|
| Select com callback que abre outro modal | Sim | — | — |
| Modal abre dentro de outro modal (3+ níveis) | — | Sim | Possível |
| setTarget("parent") + setOnCloseURL | — | — | Sim |
Os 3 fixes ficam catalogados como padrões pro Jasap — qualquer feature futura com modal aninhado profundo pode reaproveitar.
| Arquivo | Tipo | Edição |
|---|---|---|
| DepartamentoProdutoBean | editar | Campo transient ct_pessoas + getter/setter + constante CT_PESSOAS |
| DepartamentoProdutoDAO | editar | Subquery COUNT sempre presente no daoList |
| DepartamentoProdutoList | editar | Coluna "Pessoas" entre Qtd e Observação (sem orderBy), modal de update aumentado pra 1000×720 |
| DepartamentoPessoaBean | editar | Campos transient fk_produto e qt_recebida + getters/setters + 2 constantes |
| DepartamentoPessoaDAO | editar | Import novo, daoList com subquery condicional, daoWhere com filtro fk_produto via IN |
| DepartamentoProdutoForm | editar | 7 imports, 3 inner classes (RefreshDetail, DetailChangePage, DesvincularPes), Insert alterado pra desviar fluxo "Novo Produto via Pessoa", window() divide em master/detail, método novo pesDetail(), 1 constante |
| DepartamentoVinculoForm | editar | 1 import, setTarget("parent") + setId + setOnCloseURL trocado no botão "Editar Produto", CSS readonly visual |
| DepartamentoPessoaForm | editar | Inner class AfterEntregaContinuar, AddExistingPro com evalParent combinado, 1 constante |
| DepartamentoManager | editar | 4 regAction novas |
| Banco | — | Nada — tabela produto_pessoa já existia |
9 arquivos editados, 0 novos, sem schema. Resultado: lista de produtos ganha coluna "Pessoas" com a contagem, form do produto em update mostra a lista de pessoas que receberam ele (com lixeirinha de desvínculo). O fluxo "Novo Produto via Pessoa" agora abre o VinculoForm pra escolher quanto entregar (em vez de zerar o estoque automaticamente). E 3 bugs do framework com modal aninhado profundo viraram padrões reaproveitáveis.