Novos tipos de documento
João Borsoi, #256, maio 2017Palavras-chave: cms, tipo de pasta, tutorial
Este artigo descreve passo a passo como criar um novo tipo de pasta/documento para um site feito com o CMS do avalanche, criando também no menu uma área para gerenciar o cadastro.
Em nosso exemplo vamos criar um novo tipo "Contrato", contendo os seguintes campos:
- Número
- Início
- Vencimento
- Classe (opções fornecedor ou colaborador)
- Tipo (fornecedor: serviços, bens; colaborador: CLT, estágio, autônomo, associado)
- Descrição
- Valor
- Ativo (booleano)
- Documentos
Banco de dados
A primeira etapa consiste em ajustes no banco de dados, criando as tabelas e informações necessárias para a geração do formulário e armazenamento dos contratos.
Os campos classe e tipo serão de seleção (select) e precisam de tabelas associadas que irão conter as opções, assim como a dependência entre classe e tipo. As tabelas de opções ficam da seguinte maneira:
CREATE TABLE KB_ClasseContrato (
classId INTEGER UNSIGNED NOT NULL PRIMARY KEY,
value VARCHAR(255) NOT NULL
) ENGINE=InnoDB;
INSERT INTO KB_ClasseContrato
(classId, value) VALUES
(1, 'Fornecedores'),
(2, 'Colaboradores');
CREATE TABLE KB_TipoContrato (
typeId INTEGER UNSIGNED NOT NULL PRIMARY KEY,
classId INTEGER UNSIGNED NOT NULL,
INDEX(classId),
FOREIGN KEY (classId) REFERENCES KB_ClasseContrato(classId),
value VARCHAR(255) NOT NULL
) ENGINE=InnoDB;
INSERT INTO KB_TipoContrato
(typeId, classId, value) VALUES
(1,1,'Serviços'),
(2,1,'Bens'),
(3,2,'CLT'),
(4,2,'Estágio'),
(5,2,'Autônomo'),
(6,2,'Associado');
A tabela que irá armazenar os contratos deve ter uma chave estrangeira para a tabela LIB_Document pois todo contrato será um documento do avalanche, e estará sob as condições de permissionamento.
CREATE TABLE KB_Contrato (
docId INTEGER UNSIGNED NOT NULL PRIMARY KEY,
FOREIGN KEY (docId) REFERENCES LIB_Document(docId) ON DELETE CASCADE,
numero VARCHAR(255) NOT NULL,
inicio DATE NOT NULL,
vencimento DATE NOT NULL,
classId INTEGER UNSIGNED NOT NULL,
typeId INTEGER UNSIGNED NOT NULL,
INDEX(classId, typeId),
FOREIGN KEY(classId,typeId) REFERENCES KB_TipoContrato (classId, typeId),
descr TEXT NOT NULL,
valor DECIMAL(40,2) NOT NULL,
ativo CHAR(1) NOT NULL DEFAULT '1'
) ENGINE=InnoDB;
Note que além da chave extrangeira para o LIB_Document, os campos classId e typeId referenciam as tabelas de opções previamente criadas.
O próximo passo consiste em inserir as meta-informações sobre cada campo da nova tabela para que o avalanche possa gerar o formulário. Estas informações são inseridas na tabela FORM_Field:
INSERT INTO LANG_Label
(labelId, lang, value) VALUES
('kbNumero','pt_BR','Número'),
('kbInicio','pt_BR','Início'),
('kbVencimento','pt_BR','Vencimento'),
('kbClasse','pt_BR','Classe'),
('kbTipo','pt_BR','Tipo'),
('kbDescr','pt_BR','Descrição'),
('kbValor','pt_BR','Valor'),
('kbAtivo','pt_BR','Ativo'),
('kbDocumentos','pt_BR','Documentos');
INSERT INTO FORM_Field
(tableName, fieldName, labelId, type, size, attributes, orderby, consistType, required, allowNull, trimField, uniq, keyField, accessMode, staticProp, template, langTemplate, outputFuncRef, `maxValue`, minValue) VALUES
('KB_Contrato', 'docId', NULL, 'dummy', NULL, NULL, 0, 'integer', '0', '1', '1', '1', '1', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'numero', 'kbNumero', 'text', 255, NULL, 4010, NULL, '1', '0', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'inicio', 'kbInicio', 'date', NULL, NULL, 4020, NULL, '1', '0', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'vencimento', 'kbVencimento', 'date', NULL, NULL, 4025, NULL, '1', '0', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'classId', 'kbClasse', 'select', NULL, NULL, 4030, 'integer', '1', '0', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'typeId', 'kbTipo', 'select', NULL, NULL, 4040, 'integer', '1', '0', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'descr', 'kbDescr', 'textArea', NULL, NULL, 4050, NULL, '1', '0', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'valor', 'kbValor', 'currency', NULL, NULL, 4060, NULL, '1', '0', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'ativo', 'kbAtivo', 'boolean', NULL, NULL, 4070, 'boolean', '0', '1', '1', '0', '0', 'rw', '1', NULL, '0', NULL, NULL, NULL),
('KB_Contrato', 'docs', 'kbDocumentos', 'fileList', NULL, NULL, 4130, 'objectList', '0', '1', '0', '0', '0', '', '1', NULL, '0', NULL, NULL, NULL);
O campo de documentos assim como os campos select precisam de informações adicionais, que são registradas respectivamente nas tabelas LIB_FileListField e FORM_SelectField:
INSERT INTO LIB_FileListField
(tableName,fieldName,folderId,contentFileTable,contentFileFK,maxFiles) VALUES
('KB_Contrato','docs',(SELECT folderId FROM LIB_Folder WHERE path='/files/'),'LIB_ContentFile','nodeId',0);
INSERT INTO FORM_SelectField
(tableName, fieldName, optionTable, optionTableKey, optionTableValue, selectedTable, sourceField, targetField, orderField, ascOrder) VALUES
('KB_Contrato', 'classId', 'KB_ClasseContrato', 'classId', 'value', NULL, NULL, 'typeId', 'value', '1'),
('KB_Contrato', 'typeId', 'KB_TipoContrato', 'typeId', 'value', NULL, 'classId', NULL, 'value', '1');
Por fim, é necessário registrar o novo tipo de pasta que irá armazenar os contratos, assim como criar uma pasta associada a este novo tipo no menu do site:
SET @contratoId = (SELECT MAX(id)+1 FROM LIB_FolderType);
INSERT INTO LIB_FolderType
(id, name,contentModule,views) VALUES
(@contratoId,NULL,NULL,'{
"mainView": {
"templateUrl": "contratos.html"
}
}');
INSERT INTO LIB_FolderTables
(id, tableName, printOrder) VALUES
(@contratoId, 'LIB_Node', 1),
(@contratoId, 'LIB_Document', 2),
(@contratoId, 'KB_Contrato', 3);
INSERT INTO LIB_DocReturnFields
(id,tableName,fieldName,printOrder) VALUES
(@contratoId,'KB_Contrato','numero',1),
(@contratoId,'KB_Contrato','inicio',2),
(@contratoId,'KB_Contrato','vencimento',3),
(@contratoId,'KB_Contrato','classId',4),
(@contratoId,'KB_Contrato','typeId',5),
(@contratoId,'KB_Contrato','descr',6),
(@contratoId,'KB_Contrato','valor',7);
INSERT INTO LIB_Node
(nodeId, userId, groupId, userRight, groupRight, otherRight, creationDate, lastChanged, lastChangedUserId) VALUES
(NULL,1,3,'rw','rw','',NOW(),NOW(),1);
INSERT INTO LANG_Label
(labelId,lang,value) VALUES
('kbContratos','pt_BR','Contratos');
INSERT INTO LIB_Folder
(folderId, level, path, fixedOrder, folderTables, restrictedDeletion, title, content, contentType, menuTitle, defGroupId, defUserRight, defGroupRight, defOtherRight, descr) VALUES
(LAST_INSERT_ID(), 4, '/content/Menu/Contratos/', 1, @contratoId, 0, NULL, NULL, NULL, 'kbContratos', 3, 'rw', 'rw', '', NULL);
O campo views na tabela LIB_FolderType está associado aos estados da aplicação no front-end (ui-router). No front-end, cada item do menu (pastas /content/Menu/*) vai estar associado a um estado da aplicação.
A tabela LIB_FolderTables especifica quais tabelas compõe o novo tipo e a LIB_DocReturnFields indica os campos retornados por padrão nas buscas.
API
Com os ajustes mencionados no banco de dados, as APIs do avalanche já devem funcionar corretamente com o novo tipo de documento. Neste exemplo não é necessário a criação de um novo endpoint para a API, portanto esta etapa consiste somente na verificação das APIs existentes, com objetivo de depurar eventuais problemas. Uma ferramenta muito útil para estes testes é o Postman, que trata-se de um plugin para o Google Chrome.
Veja abaixo alguns exemplos de testes e resultados esperados:
Método GET API documents
URL:
http://localhost:8080/api/documents/?path=/content/Menu/Contratos
Resultado:
{
"attrs": {
"folderTables": "700002",
"numero": null,
"inicio": null,
"vencimento": null,
"classId": null,
"typeId": null,
"descr": null,
"valor": null,
"ativo": "1",
"docs": null,
"docId": null,
"tabHeader": null,
"properties": null,
"_tabHeader": null,
"propertiesTab": null,
"userId": null,
"groupId": null,
"ownerTable": null,
"userRight": "rw",
"groupRight": "rw",
"otherRight": "",
"_ownerTable": null,
"creationDate": null,
"lastChanged": null,
"_propertiesTab": null,
"keywords": ""
},
"children": [...]
}
Método POST API documents
URL:
http://localhost:8080/api/documents/
Request Body:
{
"path": "/content/Menu/Contratos",
"numero": "001",
"inicio": "2017-01-01 00:00:00",
"vencimento": "2018-01-01 00:00:00",
"classId": "1",
"typeId": "1",
"descr": "Teste de descrição",
"valor": "1000.00"
}
Resultado:
{
"attrs": {
"folderTables": "700002",
"docId": "187",
"numero": "001",
"inicio": "2017-01-01 00:00:00",
"vencimento": "2018-01-01 00:00:00",
"classId": "1",
"typeId": "1",
"descr": "Teste de descrição",
"valor": "1000.00",
"ativo": "1",
"docs": [],
"nodeId": "187",
"tabHeader": null,
"properties": null,
"_tabHeader": null,
"propertiesTab": null,
"userId": "3",
"groupId": "3",
"ownerTable": null,
"userRight": "rw",
"groupRight": "rw",
"otherRight": "",
"_ownerTable": null,
"creationDate": "2017-06-07 12:11:46",
"lastChanged": "2017-06-07 12:11:46",
"_propertiesTab": null,
"keywords": ""
},
"children": [...]
}
Método POST API search
URL:
http://localhost:8080/api/search
Request Body:
{
"path": "/content/Menu/Contratos",
"limit": 10
}
Resultado:
{
"totalResults": "2",
"children": [
{
"docId": "185",
"path": "/content/Menu/Contratos/",
"folderTables": "700002",
"fixedOrder": "2",
"numero": "002",
"inicio": "2017-06-13 00:00:00",
"vencimento": "2018-06-13 00:00:00",
"classIdText": "Fornecedores",
"classId": "1",
"typeIdText": "Serviços",
"typeId": "1",
"descr": "Sistema de informação",
"valor": "50000.00",
"maxMatches": "0",
"sumMatches": "0",
"inicio_timestamp": 1497322800,
"vencimento_timestamp": 1528858800,
"fieldIndex": 1,
"lang": "pt_BR",
"avVersion": "killbill"
},
{
"docId": "186",
"path": "/content/Menu/Contratos/",
"folderTables": "700002",
"fixedOrder": "3",
"numero": "003",
"inicio": "2017-06-06 00:00:00",
"vencimento": "2017-06-23 00:00:00",
"classIdText": "Fornecedores",
"classId": "1",
"typeIdText": "Bens",
"typeId": "2",
"descr": "Teste de bens",
"valor": "1000.00",
"maxMatches": "0",
"sumMatches": "0",
"inicio_timestamp": 1496718000,
"vencimento_timestamp": 1498186800,
"fieldIndex": 2,
"lang": "pt_BR",
"avVersion": "killbill"
}
]
}
UI
A última etapa deste tutorial consiste em preparar a interface do front-end para receber o novo tipo de documento. Conforme foi especificado no banco de dados na tabela LIB_FolderType no campo views, o mainView da aplicação será dado pelo template contratos.html. Neste arquivo definimos um botão para adicionar novos contratos, e um campo de busca para exibir contratos já cadastrados.
<div ng-controller="ContratosCtrl">
<button class="btn btn-default" ng-click="open()">Adiciona</button>
<h2>Contratos</h2>
<form name="searchContrato" class="input-group">
<input type="text" ng-model="search.filter" class="form-control">
<div class="input-group-btn">
<button type="submit" class="btn btn-primary" ng-click="doSearch()"><i class="mdi mdi-magnify"></i></button>
</div><!-- /btn-group -->
</form><!-- /input-group -->
<div ng-show="docList.children.length>0">
<br>
<p class="pull-right">Exibindo {{search.offset+1}}-{{(search.offset+search.limit) < docList.totalResults ? search.offset+search.limit : docList.totalResults}} de {{docList.totalResults}}</p>
<h2>Resultados da busca:</h2>
<table class="table">
<thead>
<tr>
<th>Número</th>
<th>Início</th>
<th>Vencimento</th>
<th>Classe</th>
<th>Tipo</th>
<th>Descrição</th>
<th>Valor</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in docList.children | orderBy: 'expiration'" ng-click="open(item)" class="clickable">
<td>{{::item.numero}}</td>
<td>{{::item.inicio|mysqlDatetimeToISO|date:dateFormat}}</td>
<td>{{::item.vencimento|mysqlDatetimeToISO|date:dateFormat}}</td>
<td>{{::item.classIdText}}</td>
<td>{{::item.typeIdText}}</td>
<td>{{::item.descr}}</td>
<td><av-currency-symbol></av-currency-symbol>{{::item.valor|number:2}}</td>
</tr>
</tbody>
</table>
<av-pagination data-source="docList" data-limit="search.limit" data-offset="search.offset"></av-pagination>
</div>
</div>
O template utiliza um controlador (ContratosCtrl) que deve:
- definir a função open que utiliza o serviço NodeModal do avalanche para abrir um modal com o formulário para cadastro dos contratos
- definir a estrutura search que armazena os parâmetros de busca
- definir a função loadSearch que utiliza o serviço Search do avalanche para fazer uma pesquisa dos contratos realizados
- definir um listener para o evento av.pagination.offset para tratar a navegação do usuário em páginas de resultado de busca, que utiliza também a função loadSearch
- definir a função doSearch que trata o evento de clique no botão relacionado, e que também utiliza a função loadSearch
app.controller('ContratosCtrl',function($scope, $state, $alert, Search, NodeModal, Loading, SelectField, avalancheConfig){
$scope.open = function(item) {
var params = {
title: 'Contrato',
path: avalancheConfig.path.contratos,
buttons: { history: false, remove: true },
excludeFields: avalancheConfig.excludeFields.contratos,
onBeforeDelete: function() {
var items = $scope.docList.children;
return items.splice(items.indexOf(item),1);
},
onUndoDelete: function(undoData) {
items.push(undoData[0]);
}
}
if(item)
params.docId = item.docId;
NodeModal(params);
}
$scope.search = {
limit: avalancheConfig.search.limit,
offset: 0
};
var loadSearch = function() {
Loading.beginTasks();
var load = Loading.pushTask();
Loading.watchTasks();
$scope.search.path = avalancheConfig.path.contratos;
$scope.search.searchCriteria = 'numero,descr';
var data = Search.search($scope.search,function() {
$scope.docList = data;
if(data.children.length==0) {
var error = $alert({
animation: 'am-slide-top',
placement: 'top-right',
content: 'Nenhum contrato encontrado',
template: 'avAlert.html',
type: 'warning',
show: true,
container: 'body',
duration: 5,
dismissable: true
});
}
load.resolve();
}, function() {
load.reject();
});
}
var paginationListnerUnreg = $scope.$on('av.pagination.offset', function(event,offset){
$scope.search.offset=offset;
$scope.docList = undefined;
loadSearch();
});
$scope.$on('$destroy', function() {
paginationListnerUnreg();
});
$scope.doSearch = function() {
$scope.docList = undefined;
$scope.search.offset = 0;
loadSearch();
}
})
Comentários
Documentos Relacionados
-
João Borsoi junho 2017
-
João Borsoi junho 2017