Busca

Acompanhe

RSS

Wiki UI CMS #256

Novos tipos de documento

João Borsoi, #256, maio 2017

Palavras-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();
    }
})

Imprimir

Comentários

Adicionar Comentário

Documentos Relacionados

  1. João Borsoi junho 2017

    LIB_FolderTables

  2. João Borsoi junho 2017

    LIB_Folder