CRUD Pai-Filho: exemplo de nota fiscal

CRUD Pai-Filho: exemplo de nota fiscal

No post CRUD Avançado com Ext recebi sugestão de exemplificar um CRUD Pai-Filho. Nessa interface temos um registro X que contém N registros Y, como por exemplo os produtos de uma nota fiscal, justamente o exemplo que irei abordar agora. O que difere esse tutorial do crud avançado postado anteriormente, é que esse usa um conceito muito bacana que chamo de buffer de dados. Todos os N produtos que serão associados a nota fiscal ficarão em um buffer, para que só depois quando o usuário clique em salvar eles possam ser persistidos. Quando isso acontecer, uma rotina irá percorrer cada registro do buffer, extrair os dados e enviar ao servidor. Também iremos ver nesse tutorial uma extensão muito bacana para gerar somatórios em grid.

Preparando Terreno

Assim como nos outros tutoriais usaremos a pasta examples do Ext JS 3.0 para criar nossa aplicação. Nela criarei a pasta notaFiscal.

Estrutura Final da Aplicação

Estrutura Final da Aplicação

Nota Fiscal

Atenção: Essa tela não é uma nota fiscal eletrônica e nem respeita todo o regulamento para se criar uma. Aproveitei apenas o bom exemplo de vários produtos em uma nota fiscal para criar esse tutorial.

Seguindo o conceito já explicado em outros tutorias de que cada interface é representada por uma classe, vamos criar a estrutura básica de nossa nota fiscal com uma classe extendida de Ext.Panel:

Ext.onReady(function()
{
//cria janela e coloca painel nota fiscal dentro
new Ext.Window({
layout : 'fit'
,title : 'Nota Fiscal'
,width : 700
,height : 500
,x : 10
,y : 110
,items : {
xtype : 'b-notafiscal'
,border : false
}
}).show();

});

/**
* Classe que representa a NotaFiscal
*/
var NotaFiscal = Ext.extend(Ext.Panel,{

//Config {

//super
layout : 'anchor'
,bodyStyle : 'padding:5px;'
,autoScroll : true

//}

//Inits {

,initComponent: function()
{
//TODO: colocar os stores aqui

//Components
Ext.apply(this,{
defaults : { anchor:'-17' }
,buttons : [{
text : 'Salvar'
,iconCls: 'icon-save'
,scope : this
,handler: this._onBtnSalvarClick
}]
,items : [{
//cabeçalho
xtype : 'box'
,cls : 'nf-header'
,autoEl : {
tag: 'div'
,cn: '<img src="'+Ext.BLANK_IMAGE_URL+'" alt="" />'+
'
<h1>Nota Fiscal</h1>
<h2>'+new Date().getTime()+'</h2>
'
}
}
//TODO: Criar demais componentes
]
});

//super
NotaFiscal.superclass.initComponent.call(this);
}

,initEvents: function()
{
//super
NotaFiscal.superclass.initEvents.call(this);

//TODO: Criar eventos
}

//}

//Overrides {

,onDestroy: function()
{
//super
NotaFiscal.superclass.onDestroy.apply(this, arguments);

//TODO: limpar referências
}

//}

//Listeners {

,_onBtnSalvarClick: function()
{
//TODO: fazer ação salvar
}
//}

});
Ext.reg('b-notafiscal',NotaFiscal);

O primeiro elemento visual é o cabeçalho, que não irei abordar. O segundo é um combo para seleção de cliente, com um painel para maiores informações abaixo. Toda vez que um cliente for selecionado o painel de informações irá mostrar os dados do cliente. O legal dessa rotina é ver como podemos extrair dados de uma seleção de um combo e interagir com outros componentes a partir desses dados.

No método initComponent definimos o store do cliente, e também o combo e o painel de informações:

//Colocar store na área dos stores
this._dsClientes = new Ext.data.JsonStore({
proxy : new Ext.data.MemoryProxy( Dummy.clientes )
,idProperty : 'clienteID'
,fields : [
{name: 'clienteID' , type: 'int' }
,{name: 'nomeFantasia' , type: 'string'}
,{name: 'razaoSocial' , type: 'string'}
,{name: 'cidade' , type: 'string'}
,{name: 'endereco' , type: 'string'}
]
});

//...

//colocar esta definição nos items do painel
{
//fieldset clientes
xtype : 'fieldset'
,title : 'Cliente'
,autoHeight : true
,defaults : { anchor: '0' }
,items : [
this._comboCliente = new Ext.form.ComboBox({
displayField : 'nomeFantasia'
,valueField : 'clienteID'
,emptyText : 'Selecione um cliente'
,triggerAction : 'all'
,hideLabel : true
,forceSelection : true
,allowBlank : false
,store : this._dsClientes
,tpl : new Ext.XTemplate(
''
,'
<div class="x-combo-list-item combo-cliente-item">'
,'
<h1><strong>{nomeFantasia}</strong> - {razaoSocial}</h1>
'
,'<span>{cidade} - {endereco}</span>'
,'

'
,''
)
}),
this._panelInfoCliente = new Ext.BoxComponent({
cls: 'info-cliente'
,tpl: new Ext.Template(
'
<div><label>Nome fantasia:</label><span>{nomeFantasia}</span></div>
'
,'
<div><label>Razão Social:</label><span>{razaoSocial}</span></div>
'
,'
<div><label>Localização:</label><span>{cidade} - {endereco}</span></div>
'
)
})]
}

Perceba que estamos instanciando os componentes e guardando as suas referências (this._comboCliente, this._panelInfoCliente ). Fazemos isso porque no método initEvents vou associar o evento no combo para que toda vez que um item seja selecionado, o painel de informações possa ser populado. Utilizo templates para fazer isso. Com ele você pode definir uma estrutura HTML que futuramente será preenchida com dados.

//Colocar esse código em initEvents
this._comboCliente.on({
scope : this
,select : function(combo, record)
{
this._panelInfoCliente.show();
this._panelInfoCliente.tpl.overwrite( this._panelInfoCliente.el , record.data );
}
,blur: function(combo)
{
if(Ext.isEmpty(combo.getValue()))
this._panelInfoCliente.hide();
}
});

Feito isto podemos ver o segundo elemento visual, que é o mais importante, o fieldset de produtos. Nele temos um combo para selecionar o produto, um numberfield para informar a quantidade, um botão de adicionar, e a listagem de produtos da nota fiscal. Eu utilizei um pequeno formulário para adicionar o produto ao grid, você poderia abrir uma janela a parte ou qualquer outra interface que quisesse, contando que você tenha como extrair os dados do produto qualquer interface serve.

//Define um dataStore que irá buscar os produtos no combo,
//e outro para armazenar os produtos no grid

this._dsProdutosCombo = new Ext.data.JsonStore({
proxy : new Ext.data.MemoryProxy( Dummy.produtos )
,idProperty : 'produtoID'
,fields : [
{name: 'produtoID' , type: 'int' }
,{name: 'nome' , type: 'string'}
,{name: 'valor' , type: 'float' }
]
});

this._dsProdutosGrid = new Ext.data.JsonStore({
data : []
,fields : [
{name: 'nome' , type: 'string'}
,{name: 'quantidade', type: 'int' }
,{name: 'valor' , type: 'float' }
,{name: 'total' , type: 'float' }
]
});

//logo abaixo das definições do store cria o plugin e um renderer que serão usados no grid

//renderer
var rendererReal = function(v)
{
return Ext.util.Format.usMoney(v).replace('$','R$');
}

// utilize custom extension for Group Summary
var summary = new Ext.ux.grid.GridSummary();

//...

//Nos itens da nota fiscal define o fieldset de produtos
{
//fieldset produtos
xtype : 'fieldset'
,title : 'Produtos'
,autoHeight : true
,labelWidth : 80
,items : [
this._comboProduto = new Ext.form.ComboBox({
displayField : 'nome'
,valueField : 'produtoID'
,emptyText : 'Selecione um produto'
,triggerAction : 'all'
,fieldLabel : 'Produto'
,anchor : '0'
,forceSelection : true
,store : this._dsProdutosCombo
})
,this._txtQuantidade = new Ext.form.NumberField({
fieldLabel : 'Quantidade'
,width : 100
}),{
xtype : 'button'
,text : 'adicionar'
,iconCls: 'silk-add'
,style : 'margin-left:85px;'
,scope : this
,handler: this._onBtnAdicionarProdutoClick
},
this._gridProdutos = new Ext.grid.GridPanel({
title : 'Produtos Selecionados'
,style : 'margin-top:10px;'
,autoExpandColumn : 'nome'
,height : 200
,store : this._dsProdutosGrid
,plugins : summary
,columns : [{
header : '&amp;nbsp;'
,dataIndex : 'nome'
,align : 'center'
,width : 40
,fixed : true
,renderer : function()
{
return '<img class="silk-delete" style="cursor: pointer;" src="'+Ext.BLANK_IMAGE_URL+'" alt="" width="16" height="16" />'
}
},{
header : 'Produto'
,dataIndex : 'nome'
,id : 'nome'
,width : 300
},{
header : 'Quantidade'
,dataIndex : 'quantidade'
,summaryType: 'sum'
,width : 80
,align : 'center'
},{
header : 'Valor Unitário'
,dataIndex : 'valor'
,width : 80
,align : 'center'
,summaryType: 'max'
,renderer : rendererReal
},{
header : 'Valor Total'
,dataIndex : 'total'
,align : 'center'
,id : 'valorTotal'
,summaryType : 'sum'
,width : 80
,renderer : rendererReal
,summaryRenderer: rendererReal
}]
})]
}

//...

//No initEvents criar essa associação de evento
//grid produtos
this._gridProdutos.on({
scope : this
,cellclick : this._onGridProdutosCellClick
})

//...

//Esse método vai na região de listeners
,_onGridProdutosCellClick: function(grid, row, col, e)
{
if(col !== 0)
return;

//busca registro
var record = grid.store.getAt(row);

//remove do store
record.store.remove(record);
}

Tendo o fieldset definido, vamos a interação mais importante da tela, a adição de produtos. Essa adição se resume simplesmente em:

  • extrair os dados do formulário
  • instanciar um novo registro com base na definição de registros armazenado no datastore
  • adicionar o novo registro ao store
/**
* Listener disparado ao clicar no botão de adicionar produto
*/
,_onBtnAdicionarProdutoClick: function()
{
//busca os dados
var recordProduto = this._comboProduto.store.getById(this._comboProduto.getValue());
var qtde = this._txtQuantidade.getValue();

//valida
if( !recordProduto || !qtde )
{
Ext.Msg.alert('Atenção','É necessário selecionar um produto e informar uma quantidade');
return;
}

//cria registro
var newRecord = new this._dsProdutosGrid.recordType({
nome : recordProduto.get('nome')
,quantidade : qtde
,valor : recordProduto.get('valor')
,total : (qtde * recordProduto.get('valor')).toFixed(2)
});

//adiciona
this._dsProdutosGrid.add(newRecord);

//reseta
this._comboProduto.reset();
this._txtQuantidade.reset();
}

E a última ação, salvar! Devemos extrair os dados que estão em buffer e mandá-los para o servidor. É uma rotina muito simples que acaba enrrolando muitos programadores por aí. Segue exemplo:

,_onBtnSalvarClick: function()
{
//valida
if( !this._comboCliente.isValid() )
return;</p>
if(this._dsProdutosGrid.getCount()===0)
{
Ext.Msg.alert('Atenção','É preciso adicionar ao menos um produto')
return;
}

//extrai produtos da nota fiscal
var produtos = [];
this._dsProdutosGrid.each(function( record )
{
produtos.push( Ext.encode(record.data) );
})

//alerta
Ext.Msg.alert('Requisição feita (visualize com firebug)','Parâmetros= ' + Ext.encode({
fnTarget : 'inserirNF'
,clienteID : this._comboCliente.getValue()
,'produtos[]' : produtos
}).replace(/\\\"/gi,'"'));

//requisição de exemplo
Ext.Ajax.request({
url : 'NotaFiscal.ajax.php'
,params : {
fnTarget : 'inserirNF'
,clienteID : this._comboCliente.getValue()
,'produtos[]' : produtos
}
});
}

Não podemos esquecer também de limpar todas as referências no método onDestroy:

,onDestroy: function()
{
//super
NotaFiscal.superclass.onDestroy.apply(this, arguments);

//limpa referências alocadas
this._panelInfoCliente = this._comboCliente = this._comboProduto =
this._txtQuantidade = this._gridProdutos = null;
}

É é isso aí pessoal. Temos uma interface de nota fiscal, com interação de combo e cadastro de vários produtos. Uma continuação desse tutorial seria criar um grid de nota fiscais e fazer com que ao inserir uma nova nota fiscal, o grid que estaria abaixo fosse recarregado. Creio que em algum momento você vá utilizar esse tutorial, porque CRUD Pai-Filho é uma situação muito comum de qualquer sistema Ext. Com essa abordagem contornamos qualquer solução que utiliza tabelas temporárias na database ou coisa parecida. Tudo é feito no cliente, salvo em buffer, e só depois enviado ao servidor.

Agradecimento à todos que participaram do post Crud Avançado em destaque ao Miguel Cartagena que foi quem sugeriu o post e interagiu comigo antes que o post fosse publicado.

E não se esqueçam de participar pessoal! Aproveito o post de hoje para agradecer o grande número de comentários, emails e discussões no MSN que estão sendo feitas. Forte abraço e até a próxima!

server-side.zip

Código Completo

Demo Online

Demo Online

Posts relacionados:

  1. Criando Ext.Window para edição de dados de um grid – CRUD Local
  2. CRUD Avançado com Ext JS 3.0