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()<br />
{<br />
	//cria janela e coloca painel nota fiscal dentro<br />
	new Ext.Window({<br />
		 layout	: 'fit'<br />
		,title	: 'Nota Fiscal'<br />
		,width	: 700<br />
		,height	: 500<br />
		,x		: 10<br />
		,y		: 110<br />
		,items	: {<br />
			 xtype	: 'b-notafiscal'<br />
			,border	: false<br />
		}<br />
	}).show();</p>
<p>});</p>
<p>/**<br />
 * Classe que representa a NotaFiscal<br />
 */<br />
var NotaFiscal = Ext.extend(Ext.Panel,{</p>
<p>	//Config {</p>
<p>		//super<br />
		 layout		: 'anchor'<br />
		,bodyStyle	: 'padding:5px;'<br />
		,autoScroll	: true</p>
<p>	//}</p>
<p>	//Inits {</p>
<p>		,initComponent: function()<br />
		{<br />
			//TODO: colocar os stores aqui</p>
<p>			//Components<br />
			Ext.apply(this,{<br />
				 defaults	: { anchor:'-17' }<br />
				,buttons	: [{<br />
					 text	: 'Salvar'<br />
					,iconCls: 'icon-save'<br />
					,scope	: this<br />
					,handler: this._onBtnSalvarClick<br />
				}]<br />
				,items		: [{<br />
				//cabeçalho<br />
					 xtype	: 'box'<br />
					,cls	: 'nf-header'<br />
					,autoEl	: {<br />
						tag: 'div'<br />
						,cn: '<img src="'+Ext.BLANK_IMAGE_URL+'" />'+<br />
							'<br />
<h1>Nota Fiscal</h1>
<h2>'+new Date().getTime()+'</h2>
</p><p>'<br />
					}<br />
				}<br />
				//TODO: Criar demais componentes<br />
				]<br />
			});</p>
<p>			//super<br />
			NotaFiscal.superclass.initComponent.call(this);<br />
		}</p>
<p>		,initEvents: function()<br />
		{<br />
			//super<br />
			NotaFiscal.superclass.initEvents.call(this);</p>
<p>			//TODO: Criar eventos<br />
		}</p>
<p>	//}</p>
<p>	//Overrides {</p>
<p>		,onDestroy: function()<br />
		{<br />
			//super<br />
			NotaFiscal.superclass.onDestroy.apply(this, arguments);</p>
<p>			//TODO: limpar referências<br />
		}</p>
<p>	//}</p>
<p>	//Listeners {</p>
<p>		,_onBtnSalvarClick: function()<br />
		{<br />
			//TODO: fazer ação salvar<br />
		}<br />
	//}</p>
<p>});<br />
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<br />
this._dsClientes = new Ext.data.JsonStore({<br />
	 proxy		: new Ext.data.MemoryProxy( Dummy.clientes )<br />
	,idProperty	: 'clienteID'<br />
	,fields		: [<br />
		 {name: 'clienteID'		, type: 'int'	}<br />
		,{name: 'nomeFantasia'	, type: 'string'}<br />
		,{name: 'razaoSocial'	, type: 'string'}<br />
		,{name: 'cidade'		, type: 'string'}<br />
		,{name: 'endereco'		, type: 'string'}<br />
	]<br />
});</p>
<p>//...</p>
<p>//colocar esta definição nos items do painel<br />
{<br />
//fieldset clientes<br />
	 xtype		: 'fieldset'<br />
	,title		: 'Cliente'<br />
	,autoHeight	: true<br />
	,defaults	: { anchor: '0' }<br />
	,items		: [<br />
	this._comboCliente = new Ext.form.ComboBox({<br />
		 displayField	: 'nomeFantasia'<br />
		,valueField		: 'clienteID'<br />
		,emptyText		: 'Selecione um cliente'<br />
		,triggerAction	: 'all'<br />
		,hideLabel		: true<br />
		,forceSelection	: true<br />
		,allowBlank		: false<br />
		,store			: this._dsClientes<br />
		,tpl			: new Ext.XTemplate(<br />
			'<tpl for=".">'<br />
				,'
<div class="x-combo-list-item combo-cliente-item">'<br />
					,'<br />
<h1><b>{nomeFantasia}</b> - {razaoSocial}</h1>
<p>'<br />
					,'<span>{cidade} - {endereco}</span>'<br />
				,'</p></div>
<p>'<br />
			,'</p></tpl>'<br />
		)<br />
	}),<br />
	this._panelInfoCliente = new Ext.BoxComponent({<br />
		 cls: 'info-cliente'<br />
		,tpl: new Ext.Template(<br />
			 '
<div><label>Nome fantasia:</label><span>{nomeFantasia}</span></div>
</p><p>'<br />
			,'
<div><label>Razão Social:</label><span>{razaoSocial}</span></div>
</p><p>'<br />
			,'
<div><label>Localização:</label><span>{cidade} - {endereco}</span></div>
</p><p>'<br />
		)<br />
	})]<br />
}

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<br />
this._comboCliente.on({<br />
	 scope	: this<br />
	,select	: function(combo, record)<br />
	{<br />
		this._panelInfoCliente.show();<br />
		this._panelInfoCliente.tpl.overwrite( this._panelInfoCliente.el , record.data );<br />
	}<br />
	,blur: function(combo)<br />
	{<br />
		if(Ext.isEmpty(combo.getValue()))<br />
			this._panelInfoCliente.hide();<br />
	}<br />
});

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,<br />
//e outro para armazenar os produtos no grid</p>
<p>this._dsProdutosCombo = new Ext.data.JsonStore({<br />
	 proxy		: new Ext.data.MemoryProxy( Dummy.produtos )<br />
	,idProperty	: 'produtoID'<br />
	,fields		: [<br />
		 {name: 'produtoID'	, type: 'int'	}<br />
		,{name: 'nome'		, type: 'string'}<br />
		,{name: 'valor'		, type: 'float'	}<br />
	]<br />
});</p>
<p>this._dsProdutosGrid = new Ext.data.JsonStore({<br />
	 data	: []<br />
	,fields	: [<br />
		 {name: 'nome'		, type: 'string'}<br />
		,{name: 'quantidade', type: 'int'	}<br />
		,{name: 'valor'		, type: 'float'	}<br />
		,{name: 'total'		, type: 'float'	}<br />
	]<br />
});</p>
<p>//logo abaixo das definições do store cria o plugin e um renderer que serão usados no grid</p>
<p>//renderer<br />
var rendererReal = function(v)<br />
{<br />
	return Ext.util.Format.usMoney(v).replace('$','R$');<br />
}</p>
<p>// utilize custom extension for Group Summary<br />
var summary = new Ext.ux.grid.GridSummary();</p>
<p>//...</p>
<p>//Nos itens da nota fiscal define o fieldset de produtos<br />
{<br />
//fieldset produtos<br />
	 xtype		: 'fieldset'<br />
	,title		: 'Produtos'<br />
	,autoHeight	: true<br />
	,labelWidth	: 80<br />
	,items		: [<br />
	this._comboProduto = new Ext.form.ComboBox({<br />
		 displayField	: 'nome'<br />
		,valueField		: 'produtoID'<br />
		,emptyText		: 'Selecione um produto'<br />
		,triggerAction	: 'all'<br />
		,fieldLabel		: 'Produto'<br />
		,anchor			: '0'<br />
		,forceSelection	: true<br />
		,store			: this._dsProdutosCombo<br />
	})<br />
	,this._txtQuantidade = new Ext.form.NumberField({<br />
		 fieldLabel	: 'Quantidade'<br />
		,width		: 100<br />
	}),{<br />
		 xtype	: 'button'<br />
		,text	: 'adicionar'<br />
		,iconCls: 'silk-add'<br />
		,style	: 'margin-left:85px;'<br />
		,scope	: this<br />
		,handler: this._onBtnAdicionarProdutoClick<br />
	},<br />
	this._gridProdutos = new Ext.grid.GridPanel({<br />
	 	 title				: 'Produtos Selecionados'<br />
		,style	 			: 'margin-top:10px;'<br />
		,autoExpandColumn	: 'nome'<br />
		,height				: 200<br />
		,store				: this._dsProdutosGrid<br />
		,plugins			: summary<br />
		,columns			: [{<br />
			 header		: '&amp;nbsp;'<br />
			,dataIndex	: 'nome'<br />
			,align		: 'center'<br />
			,width		: 40<br />
			,fixed		: true<br />
			,renderer	: function()<br />
			{<br />
				return '<img src="'+Ext.BLANK_IMAGE_URL+'" width="16" height="16" class="silk-delete" style="cursor:pointer;" />'<br />
			}<br />
		},{<br />
			 header		: 'Produto'<br />
			,dataIndex	: 'nome'<br />
			,id			: 'nome'<br />
			,width		: 300<br />
		},{<br />
			 header		: 'Quantidade'<br />
			,dataIndex	: 'quantidade'<br />
			,summaryType: 'sum'<br />
			,width		: 80<br />
			,align		: 'center'<br />
		},{<br />
			 header		: 'Valor Unitário'<br />
			,dataIndex	: 'valor'<br />
			,width		: 80<br />
			,align		: 'center'<br />
			,summaryType: 'max'<br />
			,renderer	: rendererReal<br />
		},{<br />
			 header			: 'Valor Total'<br />
			,dataIndex		: 'total'<br />
			,align			: 'center'<br />
			,id				: 'valorTotal'<br />
			,summaryType	: 'sum'<br />
			,width			: 80<br />
			,renderer		: rendererReal<br />
				,summaryRenderer: rendererReal<br />
		}]<br />
	})]<br />
}</p>
<p>//...</p>
<p>//No initEvents criar essa associação de evento<br />
//grid produtos<br />
this._gridProdutos.on({<br />
	 scope		: this<br />
	,cellclick	: this._onGridProdutosCellClick<br />
})</p>
<p>//...</p>
<p>//Esse método vai na região de listeners<br />
,_onGridProdutosCellClick: function(grid, row, col, e)<br />
{<br />
	if(col !== 0)<br />
		return;</p>
<p>	//busca registro<br />
	var record = grid.store.getAt(row);</p>
<p>	//remove do store<br />
	record.store.remove(record);<br />
}

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

/**<br />
 * Listener disparado ao clicar no botão de adicionar produto<br />
 */<br />
,_onBtnAdicionarProdutoClick: function()<br />
{<br />
	//busca os dados<br />
	var recordProduto = this._comboProduto.store.getById(this._comboProduto.getValue());<br />
	var qtde = this._txtQuantidade.getValue();</p>
<p>	//valida<br />
	if( !recordProduto || !qtde )<br />
	{<br />
		Ext.Msg.alert('Atenção','É necessário selecionar um produto e informar uma quantidade');<br />
		return;<br />
	}</p>
<p>	//cria registro<br />
	var newRecord = new this._dsProdutosGrid.recordType({<br />
		 nome		: recordProduto.get('nome')<br />
		,quantidade	: qtde<br />
		,valor		: recordProduto.get('valor')<br />
		,total		: (qtde * recordProduto.get('valor')).toFixed(2)<br />
	});</p>
<p>	//adiciona<br />
	this._dsProdutosGrid.add(newRecord);</p>
<p>	//reseta<br />
	this._comboProduto.reset();<br />
	this._txtQuantidade.reset();<br />
}

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()<br />
{<br />
	//valida<br />
	if( !this._comboCliente.isValid() )<br />
		return;</p>
<p>	if(this._dsProdutosGrid.getCount()===0)<br />
	{<br />
		Ext.Msg.alert('Atenção','É preciso adicionar ao menos um produto')<br />
		return;<br />
	}</p>
<p>	//extrai produtos da nota fiscal<br />
	var produtos = [];<br />
	this._dsProdutosGrid.each(function( record )<br />
	{<br />
		produtos.push( Ext.encode(record.data) );<br />
	})</p>
<p>	//alerta<br />
	Ext.Msg.alert('Requisição feita (visualize com firebug)','Parâmetros= ' + Ext.encode({<br />
		 fnTarget		: 'inserirNF'<br />
		,clienteID		: this._comboCliente.getValue()<br />
		,'produtos[]' 	: produtos<br />
	}).replace(/\\\"/gi,'"'));</p>
<p>	//requisição de exemplo<br />
	Ext.Ajax.request({<br />
		 url	: 'NotaFiscal.ajax.php'<br />
		,params	: {<br />
			 fnTarget		: 'inserirNF'<br />
			,clienteID		: this._comboCliente.getValue()<br />
			,'produtos[]' 	: produtos<br />
		}<br />
	});<br />
}

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

,onDestroy: function()<br />
{<br />
	//super<br />
	NotaFiscal.superclass.onDestroy.apply(this, arguments);</p>
<p>	//limpa referências alocadas<br />
	this._panelInfoCliente = this._comboCliente = this._comboProduto =<br />
	this._txtQuantidade = this._gridProdutos = null;<br />
}

É é 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

Related posts:

  1. Criando Ext.Window para edição de dados de um grid – CRUD Local
  2. CRUD Avançado com Ext JS 3.0
  • Comments [13]
    • Share