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
 
