Árvore de páginas

Versões comparadas

Chave

  • Esta linha foi adicionada.
  • Esta linha foi removida.
  • A formatação mudou.


Painel
borderColorlightgrey
bgColor#F0F0F0
borderWidth2
borderStyledashed

Índice
maxLevel3


01. INTRODUÇÃO/OBJETIVO

Temos como objetivo implementar técnicas para facilitar a personalização de telas TOTVS - Linha Datasul de forma lowcode, apenas com cadastro de campos por parte do cliente.



02. VISÃO GERAL

A partir da release 12.1.31 são disponibilizados as técnicas e cadastros para implementar a personalização em telas HTML da linha Datasul.

Nesta técnica de personalização, o desenvolvedor deverá realizar o cadastro dos campos a serem personalizados e criar alguns componentes em PO-UI que utilizem os componentes: PO-DYNAMIC-FORM, PO-DYNAMIC-VIEW e PO-PAGE-DYNAMIC-TABLE (este último somente se for necessário). Deve-se também implementar endpoint em Progress, o qual será utilizado como fonte de dados para os campos personalizados.



03. PRÉ-REQUISITOS

Para utilização desta técnica será necessário possuir conhecimento de desenvolvimento com: APIs REST em Progress, Angular, TypeScript e PO-UI.



04. TÉCNICAS

A Técnica de personalização de telas HTML com PO-UI contempla os seguintes objetos:


Endpoint Progress do Framework


Retorna a lista de campos personalizados que devem ser previamente cadastrados na tela de Personalização em HTML. 

Neste item, deverá ser utilizado o endpoint Progress /api/btb/v1/personalizationView/metadata/ + código_do_programa ,onde deve ser passado o Código do Programa Datasul que conterá a lista de campos personalizados.

Exemplo: Implementação de personalização para o programa pedido-execucao-monitor, deve-se utilizar o endpoint "/api/btb/v1/personalizationView/metadata/pedido-execucao-monitor":

Painel
borderColor#808080
bgColor#FAEDD8
borderWidth2
titleBGColor#FAD8A5
borderStyledashed
titleJSon de retorno do Endpoint do Framework
Expandir
titleClique aqui para ver o JSON...
Bloco de código
languagejs
themeEclipse
firstline1
titleJSon de retorno do Endpoint do Framework
linenumberstrue
{
    "fields": [
        {
            "visible": true,
            "gridColumns": 6,
            "disable": false,
            "property": "cod_idioma",
            "label": "Idioma",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": false,
            "property": "cod_idiom_padr",
            "label": "Idioma Padrão",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": false,
            "property": "des_idioma",
            "label": "Descrição",
            "type": "string"
        },
        {
            "visible": false,
            "property": "id",
            "type": "number",
            "key": true
        }
    ]
}
Nota
titleNota!

Atributo KEY: pode-se marcar como "Key":true o que faz parte da chave/identificador do registro.
Assim é possível ter vários campos marcados como Key, independente do nome da propriedade.

Exemplo:

Caso dois campos (empresa=10 e filial=100) sejam marcados com "key":true e concatenar os valores.

Ao buscar o registro completo, se tem a url: <URL>/customers/10|100


Endpoint Progress que devem ser implementados na "área de negócio"

Deve retornar os dados que serão apresentados nos campos personalizados;

Componente PO-UIEndpointTipo RequisiçãoDescrição

po-dynamic-view

po-dynamic-form

/byid/nome_do_programa/idGET

Retorna um registro único.

Recebe no PathParms o "nome do programa" e o "id".

po-page-dynamic-table/nome_do_programaGET

Retorna uma lista de registros.

Recebe no PathParams o "nome do programa".

po-dynamic-form/validateForm/nome_do_programaPOST

Valida o formulário.

Recebe no PathParams o "nome do programa".

po-dynamic-form/nome_do_programaPOST

Efetua a criação de um novo registro. 

Recebe no PathParams o "nome do programa".

po-dynamic-form/nome_do_programa/idPUT

Efetua a alteração do registro.

Recebe no PathParams o "nome do programa" e o "id".

po-dynamic-form/nome_do_programa/idDELETE

Efetua a eliminação do registro.

Recebe no PathParams o "nome do programa" e o "id".

Aviso
titleAviso

Em todas os componentes dinâmicos da tabela apresentada, efetuará uma requisição para obter a lista de campos personalizados na API REST do Framework, com a utilização da requisição:

 "/api/btb/v1/personalizationView/metadata/" + código_do_programa


Utilitário facilitador no Progress

Foi implementado no Progress o utilitário  - btb/personalizationUtil.p - com seu include btb/personalizationUtil.i, que deve ser utilizado para retornar à área de negócio a lista de campos personalizáveis de um determinado programa, cujo o intuito é facilitar a implementação para que seja enviado somente os valores dos campos personalizáveis.

Devido a característica do PO-UI dinâmico, caso seja enviado os dados de campos que não estão na lista de campos, o PO-UI irá apresentar o valor do campo com uma label com o mesmo nome do campo. Com a obtenção da lista de campos pode-se evitar o envio de informações que estão fora da lista de campos personalizados.




- Include btb/personalizationUtil.i


Bloco de código
languageruby
themeConfluence
firstline1
titleInclude btb/personalizationUtil.i
linenumberstrue
DEFINE TEMP-TABLE ttPersonalization NO-UNDO 
    FIELD codProgDtsul AS CHARACTER
    FIELD codField     AS CHARACTER
    FIELD codType      AS CHARACTER
    FIELD codLabel     AS CHARACTER
    FIELD codValid     AS CHARACTER
    FIELD logReadOnly  AS LOGICAL INITIAL FALSE
    FIELD logEnable    AS LOGICAL INITIAL TRUE
    INDEX codigo IS PRIMARY codProgDtsul codField.

- Procedures disponíveis no programa btb/personalizationUtil.p


ProcedureParâmetrosDescrição/Exemplo
piGetTTPersonalization

INPUT cProg AS CHARACTER

OUTPUT TABLE ttPersonalization

Retorna a temp-table ttPersonalization com a lista de campos personalizáveis de um determinado programa.

Exemplo:

Bloco de código
languageruby
firstline1
titleExemplo de chamada do utilitário btb/personalizationUtil.p
linenumberstrue
collapsetrue
{ btb/personalizationUtil.i }

DEFINE VARIABLE hPers AS HANDLE NO-UNDO.

RUN btb/personalizatinUtil.p PERSISTENT SET hPers.
RUN piGetTTPersonalization IN hPers ("codigo_do_programa", OUTPUT TABLE ttPersonalization).
DELETE PROCEDURE hPers.

FOR EACH ttPersonalization:
    DISPLAY ttPersonalization.
END.
piGetFieldList

INPUT cProg AS CHARACTER

OUTPUT cList AS CHARACTER

OUTPUT cTypeList AS CHARACTER

Retorna duas listas CHARACTER, uma contendo a lista de campos e uma lista dos seus respectivos tipos, de um determinado programa.

Observação: As listas usam como separador a vírgula ",".

Exemplo:

Bloco de código
firstline1
titleExemplode chamada de procedure
linenumberstrue
collapsetrue
DEFINE VARIABLE hPers     AS HANDLE    NO-UNDO.
DEFINE VARIABLE cList     AS CHARACTER NO-UNDO.
DEFINE VARIABLE cTypeList AS CHARACTER NO-UNDO.

RUN btb/personalizatinUtil.p PERSISTENT SET hPers.
RUN piGetFieldList IN hPers ("codigo_do_programa", OUTPUT cList, OUTPUT cTypeList).
DELETE PROCEDURE hPers.

MESSAGE cList SKIP cTypeList VIEW-AS ALERT-BOX



Implementação pela área de negócio a interface em PO-DYNAMIC-FORM e PO-DYNAMIC-VIEW.

Todos os componentes dinâmicos do PO-UI realizam, no mínimo, duas requisições REST, uma para obter a lista de campos personalizáveis e outra para obter os dados a serem apresentados nesses campos.


Abaixo temos o papel de cada componente dinâmico que podemos utilizar:

PO-DYNAMIC-FORM: Para a edição e criação de um novo registro personalizado;

PO-DYNAMIC-VIEW: Para a visualização do registro personalizado.

Dica
titleDica

Pode-se implementar também um componente PO-PAGE-DYNAMIC-TABLE, para a navegação dos registros e permitir a visualização e edição dos mesmos.


Em nossa técnica, em todas as requisições REST, será enviado:

- Para buscar a lista de campos personalizados utilizando o endpoint Progress fornecido pelo framework, que é o /api/btb/v1/personalizationView/metadata/ + codigo_do_programa;

- O código do programa personalizado e também um id do registro corrente para obtenção dos valores, necessários para buscar os valores dos dados a serem apresentados.



05. EXEMPLO DE UTILIZAÇÃO

Cadastro de campos personalizados

A seguir são apresentados as telas necessárias para a realização do cadastro dos campos personalizados.



Ao localizar no menu o programa Campos personalizados (html.personalization-metadata), é apresentada a tela em formato de lista que conterá todos os campos (metadados) cadastrados no produto Datasul. Para cadastrar um campo que será utilizado na personalização, basta clicar no botão +Adicionar.



A tela a seguir apresenta o cadastro do metadado relacionado a um campo que pode ser apresentado no programa como personalizado.







Para alguns tipos de campo, é possível informar uma lista de opções apresentadas em tela, para isso basta informar os dados (chave, valor).


CampoDescriçãoObrigatório
Código Programa Datasul

Código do programa base que podem ser aplicadas as técnicas de personalização

Nota
titleNota

É possível cadastrar os campos somente em programas que permitem a personalização

Sim

Identificador Campo

Identificador único do campo (por programa), necessário para a geração da tela personalizada (código do campo)Sim

Nome Campo

Nome do campo que será apresentado na tela (label do campo)

Caso o campo não seja informado, o nome do campo apresentado será o informado no identificador.

Não
Tipo Campo

Tipo do campo cadastrado

Caso o campo não seja informado, será considerado que o campo é do tipo Character

Tipos de campos permitidos:

TipoDescrição
CheckboxValores lógicos, apresentado na interface como um componente de Switch.
NúmerosValores numéricos, não sendo permitido a inserção de caracteres.
Data

Valores de datas, apresentado na interface como DatePicker.

Hora

Valor do horário, apresentado máscara 99:99 por padrão.

CharacterValores alfanuméricos.
MoedaValores monetários.
TextAreaValores alfanuméricos, apresentado na interface em um box com 3 linhas.
SenhaValores alfanuméricos, apresentados na interface como Password.
Radio GroupLista de seleção apresentadas no formato de radio group, permitido no máximo 3 opções.
Checkbox GroupLista de seleção múltipla apresentadas no formato de checkbox group, permitido no máximo 3 opções.
SelectLista de seleção apresentadas no formato de select, é obrigatório cadastrar ao menos 4 opções.
MultiselectLista de seleção múltipla apresentadas no formato de select, é obrigatório cadastrar ao menos 4 opções.
ComboboxLista de seleção apresentadas no formato de combobox, é obrigatório informar o atributo optionsService para resgatar os dados da lista.
Não
Lista Opções

Informa a lista de opções a serem apresentadas em conjunto com o tipo do componente especificado.

Informações
titleInformação

Esta opção está disponível somente para os tipos Radio Group, Checkbox Group, Select e Multiselect


Somente LeituraOpção para que o campo seja apresentado como somente leitura (torna o campo readOnly)Sim
Habilita personalizaçãoOpção para habilitar ou desabilitar a apresentação da personalização por campo (não envia o campo para ser renderizado no fron-end).Sim



Após cadastrar o campo, o mesmo é apresentado na tela inicial onde é possível realizar filtros sobre seus resultados, bem como efetuar ações de edição de campos, inclusão de atributos, habilitar ou desabilitar todos os campos por programa e exclusão do campo.



Ao clicar na opção de editar, não será possível modificar o código do programa Datasul vinculado e também seu identificador. Os demais campos estão habilitados para edição.




Atributos de campos personalizados

Com a configuração de atributos dos campos personalizados, é possível adicionar outras características tais como:


- Alteração de cor;

- Apresentação da ordem do campo no qual será visualizado em tela;

- Validações de interface;

- Máscara de apresentação do campo, entre muitos outros.

Ao clicar no botão Atributos apresentado na grid principal (Opções), uma tela de parametrizações é apresentada para que seja possível adicionar as novas características.

Ao renderizar os campos personalizados em tela, esses atributos serão inclusos no campo personalizado e enviados para tela.

Aviso
titleAtributos do PO-UI

Os nomes dos atributos devem ser os mesmos que estão documentados nas propriedades do componente no PO-UI.

Exemplo: Para personalizar um campo no formato CPF, criamos o campo COD_CPF e adicionamos um atributo do tipo mask que conterá o formato 999.999.999-99.

O atributo mask corresponde a uma propriedade do componente PO-INPUT.





Componentes em PO-UI

A seguir, são apresentados exemplos de códigos para a implementação com HTML e TypeScript:


Componente HTML (personalization-detail.component.html)

Expandir
title> > > Clique para ver o código...
Bloco de código
languagepowershell
themeConfluence
firstline1
linenumberstrue
<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>

<po-page-detail
  p-title="Detalhe do Idioma"
  [p-breadcrumb]="breadcrumb"
  (p-edit)="editClick()"
  (p-back)="goBackClick()"> 
Bloco de código
languagexml
firstline10
titleTrecho da personalização
linenumberstrue
  <!-- INICIO CODIGO PERSONALIZAVEL -->

  <po-dynamic-view
    [p-fields]="fields"
    [p-value]="record">
  </po-dynamic-view>

  <!-- FINAL CODIGO PERSONALIZAVEL -->

Bloco de código
languagexml
firstline18
linenumberstrue
</po-page-detail

Componente TypeScript (personalization-detail.component.ts)

Expandir
title > > > Clique para ver o código...
Bloco de código
languagedelphi
firstline1
linenumberstrue
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PoBreadcrumb, PoBreadcrumbItem } from '@po-ui/ng-components';

import { PersonalizationService } from '../personalization.service';

@Component({
  selector: 'app-personalization-detail',
  templateUrl: './personalization-detail.component.html',
  styleUrls: ['./personalization-detail.component.css']
})
Bloco de código
languagejs
firstline12
titleVariáveis
linenumberstrue
export class PersonalizationDetailComponent implements OnInit {
  public static cProg = 'html.aplicativos-eai';  
  
  // definicao das variaveis utilizadas
  public currentId: string;
  public fields: Array<any> = [];
  public record = {};
  public showLoading = false;

  public breadcrumb: PoBreadcrumb = { items: [] };
  public breadcrumbItem: PoBreadcrumbItem;
Bloco de código
languagejs
firstline23
titleConstrutor
linenumberstrue
  // construtor com os servicos necessarios
  constructor(
    private service: PersonalizationService,
    private activatedRoute: ActivatedRoute,
    private route: Router
  ) { }   
Bloco de código
languagejs
firstline29
titleLeitura do componente
linenumberstrue
  // load do componente
  public ngOnInit(): void {
    this.activatedRoute.params.subscribe(pars => {
      this.showLoading = true;
      this.record = {};

      // Carrega o registro pelo ID
      // tslint:disable-next-line:no-string-literal
      this.currentId = pars['id'];

      //---BUSCA OS VALORES DOS CAMPOS PERSONALIZADOS DA AREA DE NEGOCIO ----
      // busca os valores dos dados a serem apresentados
      this.service.loadValuesById(this.currentId).subscribe(resp => {
        Object.keys(resp).forEach((key) => this.record[key] = resp[key]);

        // ---BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA ----
        // carrega a lista de campos personalizados somente apos receber os dados a serem apresentados
        this.service.loadMetadata().subscribe(metadata => {
          // tslint:disable-next-line:no-string-literal
          this.fields = metadata['fields'];
          this.showLoading = false;
        });
      });
    });
    this.setBreadcrumb();
  }
  private setBreadcrumb(): void {
    this.breadcrumbItem = { label: 'Home', link: '/' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Listagem de Idiomas' , link: '/personalization' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Detalhe do Idioma' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
  } 
}
Bloco de código
languagejs
firstline64
titleAção dos botões Editar e Voltar
linenumberstrue
  // Redireciona quando clicar no botao Edit
  public editClick(): void {
    this.route.navigate(['/personalization', 'edit', this.currentId]);
  }

  // Redireciona quando clicar no botao Voltar
  public goBackClick(): void {
    this.route.navigate(['/personalization']);
  }
}

Componente HTML (personalization-edit.component.html)

Expandir
title> > > Clique para ver o código...
Bloco de código
languagepowershell
themeConfluence
firstline1
linenumberstrue
<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>

<po-page-edit
  [p-title]="cTitle"
  [p-breadcrumb]="breadcrumb"
  [p-disable-submit]="formEdit.form.invalid"
  (p-cancel)="cancelClick()"
  (p-save)="saveClick()">
Bloco de código
languagexml
firstline11
titleTrecho da personalização
linenumberstrue
  <!-- INICIO CODIGO PERSONALIZAVEL -->

  <po-dynamic-form
    #formEdit
    p-auto-focus="string"
    [p-fields]="fields"
    [p-validate]="validationUrl"
    [p-value]="record">
  </po-dynamic-form>

  <!-- FINAL CODIGO PERSONALIZAVEL -->
Bloco de código
languagexml
firstline22
linenumberstrue
</po-page-edit>

Componente TypeScript (personalization-edit.component.ts)

Expandir
title> > > Clique para ver o código...
Bloco de código
languagejs
themeConfluence
firstline1
linenumberstrue
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { PoBreadcrumb, PoBreadcrumbItem, PoDialogService, PoNotificationService } from '@po-ui/ng-components';

import { PersonalizationService } from './../personalization.service';

@Component({
  selector: 'app-personalization-edit',
  templateUrl: './personalization-edit.component.html',
  styleUrls: ['./personalization-edit.component.css']
})
Bloco de código
languagejs
firstline13
titleVariáveis
linenumberstrue
export class PersonalizationEditComponent implements OnInit {
  
  // Define as variaveis a serem utilizadas
  public cTitle: string;
  public currentId: string;
  public record = {};
  public fields: Array<any> = [];
  public isUpdate = false;
  public showLoading = false;
  public validationUrl = this.service.getUrlAreaValidation();

  public breadcrumb: PoBreadcrumb = { items: [] };
  public breadcrumbItem: PoBreadcrumbItem;

   // Obtem a referencia do componente HTML
  @ViewChild('formEdit', { static: true })
  formEdit: NgForm;  

Bloco de código
languagejs
firstline30
titleConstrutor
linenumberstrue
// Construtor da classe com os servicos necessarios
  constructor(
    private service: PersonalizationService,
    private activatedRoute: ActivatedRoute,
    private route: Router,
    private poDialog: PoDialogService,
    private poNotification: PoNotificationService
  ) { }

Bloco de código
languagejs
firstline38
titleLeitura do componente
linenumberstrue
 // Load do componente
  public ngOnInit(): void {
    this.isUpdate = false;
    this.showLoading = true;

    // Carrega o registro pelo ID
    this.activatedRoute.params.subscribe(pars => {
      // tslint:disable-next-line:no-string-literal
      this.currentId = pars['id'];

      // Se nao tiver o ID definido sera um CREATE
      if (this.currentId === undefined) {
        this.isUpdate = false;
        this.cTitle = 'Novo Idioma';
      } else {
        this.isUpdate = true;
        this.cTitle = 'Edição do Idioma';
      }

      // Atualiza o breadcrumb de acordo com o tipo de edicao
      this.setBreadcrumb();

      // Se for uma alteracao, busca o registro a ser alterado
      if (this.isUpdate) {
        // ----- BUSCA OS VALORES DOS CAMPOS PERSONALIZADOS DA AREA DE NEGOCIO -----
        this.service.loadValuesById(this.currentId).subscribe(resp => {
          Object.keys(resp).forEach((key) => this.record[key] = resp[key]);

          // ----- BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA -----
          // Em alteracao temos que receber o registro para depois buscar a lista de campos
          this.getMetadata();
        });
      } else {
        // Se for create, pega a lista de campos
        this.getMetadata();
      }
    });
  }

  private setBreadcrumb(): void {
    this.breadcrumbItem = { label: 'Home', action: this.beforeRedirect.bind(this) };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Listagem de Idiomas', action: this.beforeRedirect.bind(this) };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: this.cTitle };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
  }

  // Retorna a lista de campos
  private getMetadata() {
    this.service.loadMetadata().subscribe(metadata => {
        // tslint:disable-next-line:no-string-literal
        this.fields = metadata['fields'];
        this.showLoading = false;
    });
 }

  // Redireciona via breadcrumb
  private beforeRedirect(itemBreadcrumbLabel) {
    if (this.formEdit.valid) {
      this.route.navigate(['/']);
    } else {
      this.poDialog.confirm({
        title: 'Cancelamento de edição',
        message: 'Os dados ainda não foram gravados, confirma redirecinamento ?',
        confirm: () => this.route.navigate(['/'])
      });
    }
  }
Bloco de código
languagejs
firstline107
titleGravação do registro
linenumberstrue
  // Grava o registro quando clicado no botao Salvar
  public saveClick(): void {
    this.showLoading = true;
    if (this.isUpdate) {
      // Altera um registro ja existente
      this.service.update(this.currentId, this.record).subscribe(resp => {
        this.poNotification.success('Dados atualizados com sucesso');
        this.showLoading = false;
        this.route.navigate(['/personalization']);
      });
    } else {
      // Cria um registro novo
      this.service.create(this.currentId, this.record).subscribe(resp => {
        this.poNotification.success('Dados criados com sucesso');
        this.showLoading = false;
        this.route.navigate(['/personalization']);
      });
    }
  }

  // Cancela a edicao e redireciona ao clicar no botao Cancelar
  public cancelClick(): void {
    this.poDialog.confirm({
      title: 'Confirmar cancelamento',
      message: 'Voce deseja realmente cancelar a edição?',
      confirm: () => this.route.navigate(['/personalization'])
    });
  }
}
Bloco de código
languagejs
firstline136
titleCancelamento da edição
linenumberstrue
  // Cancela a edicao e redireciona ao clicar no botao Cancelar
  public cancelClick(): void {
    this.poDialog.confirm({
      title: 'Confirmar cancelamento',
      message: 'Voce deseja realmente cancelar a edição?',
      confirm: () => this.route.navigate(['/personalization'])
    });
  }
}

Componente HTML (personalization-list.component.html)

Expandir
title> > > Clique para ver o código...
Bloco de código
languagepowershell
themeConfluence
firstline1
linenumberstrue
<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>
Bloco de código
languagexml
firstline4
titleTrecho da personalização
linenumberstrue
<!-- INICIO CODIGO PERSONALIZAVEL -->
 
<po-page-dynamic-table
  p-auto-router
  p-title="Listagem de Idiomas"
  [p-actions]="actions"
  [p-breadcrumb]="breadcrumb"
  [p-fields]="fields"
  [p-service-api]="serviceApi">
</po-page-dynamic-table>
 
<!-- FINAL CODIGO PERSONALIZAVEL --> 

Componente TypeScript (personalization-list.component.ts)

Expandir
title> > > Clique para ver o código...
Bloco de código
languagejs
themeConfluence
firstline1
linenumberstrue
import { Component, OnInit } from '@angular/core';
import { PoBreadcrumb, PoBreadcrumbItem } from '@po-ui/ng-components';
import { PoPageDynamicTableActions } from '@po-ui/ng-templates';

import { PersonalizationService } from './../personalization.service';

@Component({
  selector: 'app-personalization-list',
  templateUrl: './personalization-list.component.html',
  styleUrls: ['./personalization-list.component.css']
})
Bloco de código
languagejs
firstline12
titleVariáveis
linenumberstrue
export class PersonalizationListComponent implements OnInit {

  // Definicao das variaveis utilizadas
  public serviceApi: string;
  public fields: Array<any> = [];
  public showLoading = false;

  public literals;

  public readonly actions: PoPageDynamicTableActions = {
    new: '/personalization/create',
    detail: '/personalization/detail/:id',
    edit: '/personalization/edit/:id',
    remove: true
  };

  public breadcrumb: PoBreadcrumb = { items: [] };
  public breadcrumbItem: PoBreadcrumbItem;
Bloco de código
languagejs
firstline30
titleConstrutor
linenumberstrue
// Construtor da classe
  constructor(
    private service: PersonalizationService
    ) { }
Bloco de código
languagejs
firstline34
titleLeitura do componente
linenumberstrue
 // Load do componente
  public ngOnInit(): void {
    this.fields = [];
    this.serviceApi = this.service.getUrlArea();
    this.showLoading = true;

    // ------- BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA --------
    this.service.loadMetadata().subscribe(metadata => {
      // tslint:disable-next-line:no-string-literal
      this.fields = metadata['fields'];
      this.showLoading = false;
    });

    this.setBreadcrumb();
  }

  private setBreadcrumb(): void {
    this.breadcrumbItem = { label: 'Home', link: '/' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Listagem de Idiomas' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
  }
}

Componente de serviço (personalization.service.ts)

Expandir
title> > > Clique para ver o código...
Bloco de código
languagejs
themeConfluence
firstline1
linenumberstrue
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { PoNotificationService } from '@po-ui/ng-components';

@Injectable({
  providedIn: 'root'
})
Bloco de código
languagejs
firstline18
titleEndpoints
linenumberstrue
export class PersonalizationService {
  public progCode = 'html.aplicativos-eai';

  // Endpoint progress do framework para obtencao da lista de campos personalizados
  private urlMetadata = '/api/btb/v1/personalizationView/metadata/';

  // Endpoint progress da area de negocio para obtencao dos valores dos campos personalizados
  private urlArea = '/api/trn/v1/idiomaValues/';
Bloco de código
languagejs
firstline26
titleConstrutor
linenumberstrue
// Construtor
constructor(
    private http: HttpClient,
    private poNotification: PoNotificationService,
  ) { }
Bloco de código
languagejs
firstline30
titleBUSCA OS CAMPOS PERSONALIZADOS
linenumberstrue
  // ------- BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA --------
  public loadMetadata() {
    return this.http.post<any[]>(this.urlMetadata + this.progCode).pipe();
  }

  // ------- BUSCA OS VALORES DOS CAMPOS PERSONALIZADOS DA AREA DE NEGOCIO --------
  public loadValuesById(cId) {
    // tslint:disable-next-line:whitespace
    return this.http.get<any[]>(this.urlArea + 'byid/' + this.progCode + '/' + cId).pipe();
  }
Bloco de código
languagejs
firstline40
linenumberstrue
  public loadAllValues() {
    return this.http.get<any[]>(this.urlArea + this.progCode + '/').pipe();
  }

  public create(cId, record) {
    return this.http.post<any[]>(this.urlArea + this.progCode + '/' + cId, record).pipe();
  }

  public update(cId, record) {
    return this.http.put<any[]>(this.urlArea + this.progCode + '/' + cId, record).pipe();
  }

  public getUrlArea(): string {
    return this.urlArea + this.progCode + '/';
  }

  public getUrlAreaValidation(): string {
    return this.urlArea + 'validateForm/' + this.progCode;
  }
} 



Endpoint em Progress para a área de negócio

A seguir, são apresentados exemplos de endpoint em Progress, que deverão ser implementados pela área de negócio para obtenção dos valores a serem apresentados nos campos personalizados.


Obtenção dos valores dos campos personalizados (idiomaValues.p)

Expandir
titleClique para ver o código...
Bloco de código
languagedelphi
themeConfluence
firstline1
linenumberstrue
{utp/ut-api.i}

{utp/ut-api-action.i pGetDataById GET /byid/~* }
{utp/ut-api-action.i pGetAll GET /~* }

{utp/ut-api-action.i pValidateForm POST /validateForm/~* }
{utp/ut-api-action.i pCreate POST /~* }

{utp/ut-api-action.i pUpdate PUT /~* }

{utp/ut-api-action.i pDelete DELETE /~* }

{utp/ut-api-notfound.i}

{btb/personalizationUtil.i}

DEFINE VARIABLE oRequest   AS JsonAPIRequestParser NO-UNDO.
DEFINE VARIABLE oResponse  AS JsonAPIResponse      NO-UNDO.
DEFINE VARIABLE oObj       AS JsonObject           NO-UNDO.
DEFINE VARIABLE oList      AS JsonArray            NO-UNDO.
DEFINE VARIABLE oBody      AS JsonObject           NO-UNDO. 

DEFINE VARIABLE cId        AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cProg      AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cList      AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cTypeList  AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cFld       AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cType      AS CHARACTER            NO-UNDO.
DEFINE VARIABLE ix         AS INTEGER              NO-UNDO.
DEFINE VARIABLE hTab       AS HANDLE               NO-UNDO.
DEFINE VARIABLE hQry       AS HANDLE               NO-UNDO.
DEFINE VARIABLE hPers      AS HANDLE               NO-UNDO.
DEFINE VARIABLE rTab       AS RECID                NO-UNDO.
Bloco de código
languagedelphi
firstline34
titleEndpoint para retorno de valores
linenumberstrue
// ** Procedure que retorna os valores **
PROCEDURE pGetDataById:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    oObj = NEW JsonObject().

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Obtem o programa e o codigo do registro corrente
    cProg = oRequest:getPathParams():getCharacter(2) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(3) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pGetDataById - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pGetDataById - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO:
        FIND idioma
            WHERE RECID(idioma) = integer(cId) 
            NO-LOCK NO-ERROR.
        IF  AVAILABLE idioma THEN DO:
            oObj:add("id", RECID(idioma)).

            // deve somente alimentar os campos que serao personalizados
            hTab = BUFFER idioma:HANDLE.
            FOR EACH ttPersonalization:    
                ASSIGN cFld = ttPersonalization.codField.
                oObj:add(cFld, hTab:BUFFER-FIELD(cFld):buffer-value()) NO-ERROR.
            END.
            hTab:BUFFER-RELEASE().
            DELETE OBJECT hTab NO-ERROR.
        END.
        LOG-MANAGER:WRITE-MESSAGE("pGetDataById - oObj = " + String(oObj:getJsonText()), ">>>>>").
    END.

    oResponse   = NEW JsonAPIResponse(oObj).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Bloco de código
languagedelphi
firstline81
titleEndpoint para retorno de todos os valores
linenumberstrue
// ** Procedure que retorna os valores **
PROCEDURE pGetAll:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    oList = NEW JsonArray().

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Obtem o programa e o codigo do registro corrente
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pGetAll - cProg = " + cProg, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO:
        FOR EACH idioma NO-LOCK:
            oObj = NEW JsonObject().
            oObj:add("id", RECID(idioma)).

            // deve somente alimentar os campos que serao personalizados    
            hTab = BUFFER idioma:HANDLE.
            FOR EACH ttPersonalization:    
                ASSIGN cFld = ttPersonalization.codField.
                oObj:add(cFld, hTab:BUFFER-FIELD(cFld):buffer-value()) NO-ERROR.
            END.
            hTab:BUFFER-RELEASE().
            DELETE OBJECT hTab NO-ERROR.

            oList:add(oObj).
        END.
    END.

    oObj = NEW JSonObject().
    oObj:add("items", oList).

    LOG-MANAGER:WRITE-MESSAGE("pGetAll - oObj = " + String(oObj:getJsonText()), ">>>>>").
    
    oResponse   = NEW JsonAPIResponse(oObj).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Bloco de código
languagedelphi
firstline130
titleCriação de novo registro na tabela
linenumberstrue
// ** Procedure que cria um novo registro na tabela **
PROCEDURE pCreate:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO. 
    
    DEFINE VARIABLE cCodIdioma     AS CHARACTER NO-UNDO.
    DEFINE VARIABLE cDesIdioma     AS CHARACTER NO-UNDO.
    DEFINE VARIABLE cCodIdiomPadr  AS CHARACTER NO-UNDO.
    DEFINE VARIABLE rIdioma        AS RECID     NO-UNDO.
    DEFINE VARIABLE lCreated       AS LOGICAL   NO-UNDO INITIAL FALSE.
 
    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().

    rTab = ?.

    // Obtem o programa e o codigo do registro corrente
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(2) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pCreate - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pCreate - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO TRANSACTION ON ERROR UNDO, LEAVE:
        CREATE idioma.
        
        // faz a gravacao dos dados onde os campos personalizados estao com o mesmo nome dos campos da tabela 
        hTab = BUFFER idioma:HANDLE.
        RUN piSetPersonalizationData (hTab, oBody).
        rTab = hTab:RECID.

        hTab:BUFFER-RELEASE() NO-ERROR.
        DELETE OBJECT hTab NO-ERROR.
    END.
    
    // Retorna o ID e se foi criado com sucesso
    oBody = NEW JsonObject().
    oBody:add('id', rTab).
    oBody:add('created', (IF lCreated THEN 'OK' ELSE 'NOK')).   

    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oBody).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Bloco de código
languagexml
firstline183
titleAtualização de conteúdo do registro pelo ID
linenumberstrue
 /** Procedure que atualiza o conteudo do registro pelo ID **/ 
PROCEDURE pUpdate:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE lUpdated       AS LOGICAL    NO-UNDO INITIAL FALSE.

    DEFINE VARIABLE oIdioma        AS JsonObject NO-UNDO.
    DEFINE VARIABLE oId            AS JsonObject NO-UNDO.

    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
    oObj     = NEW JsonObject().
   
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(2) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pUpdate - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pUpdate - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO TRANSACTION ON ERROR UNDO, LEAVE:
        FIND idioma
            WHERE RECID(idioma) = integer(cId) 
            EXCLUSIVE-LOCK NO-ERROR.
        IF  AVAILABLE idioma THEN DO:
            oObj:add("id", RECID(idioma)).
 
            // faz a gravacao dos dados onde os campos personalizados estao com o mesmo nome dos campos da tabela 
            hTab = BUFFER idioma:HANDLE.
            RUN piSetPersonalizationData (hTab, oBody).
            hTab:BUFFER-RELEASE() NO-ERROR.
            DELETE OBJECT hTab NO-ERROR.

            lUpdated = TRUE.
        END.
    END.

    // Retorna o ID e se foi atualizado com sucesso
    oBody = NEW JsonObject().
    oBody:add('id', cId).
    oBody:add('updated', (IF lUpdated THEN 'OK' ELSE 'NOK')).   

    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oBody).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Bloco de código
languagedelphi
firstline238
titleExclusão do registro pelo ID
linenumberstrue
 // ** Procedure que atualiza o conteudo do registro pelo ID **
PROCEDURE pDelete:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE lDeleted AS LOGICAL NO-UNDO INITIAL FALSE.

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
    oObj     = NEW JsonObject().
   
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(2) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pDelete - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pDelete - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // somente elimina o registro se houverem campos personalizaveis
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO TRANSACTION ON ERROR UNDO, LEAVE:
        FIND idioma
            WHERE RECID(idioma) = integer(cId) 
            EXCLUSIVE-LOCK NO-ERROR.
        IF  AVAILABLE idioma THEN DO:
            DELETE idioma.

            ASSIGN lDeleted = TRUE.
        END.
    END.
    
    // Retorna o ID e se foi criado com sucesso
    oObj = NEW JsonObject().
    oObj:add('id', cId).
    oObj:add('deleted', (IF lDeleted THEN 'OK' ELSE 'NOK')).
    
    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oObj).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Bloco de código
languagedelphi
firstline287
titleValidação do formulário
linenumberstrue
 PROCEDURE pValidateForm:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE cProp      AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oValue     AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cValue     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oNewValue  AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oNewFields AS JsonArray            NO-UNDO.
    DEFINE VARIABLE cFocus     AS CHARACTER            NO-UNDO.

    DEFINE VARIABLE oRet       AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oMessages  AS JsonArray            NO-UNDO.

    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
   
    // obtem o nome da propriedade que ocorreu o LEAVE para validacao
    cProp      = oBody:getCharacter("property")     NO-ERROR.
    oValue     = oBody:getJsonObject("value")       NO-ERROR.
    cValue     = STRING(oValue:GetCharacter(cProp)) NO-ERROR.
    cId        = oValue:getCharacter("id")          NO-ERROR.
    
    /* Recebemos do HTML o JSON abaixo
    {
        "property": "codAcoes",
        "value": {
            "codIdiomPadr": "01 Português",
            "codIdioma": "12345678",
            "desIdioma": "12345678901234567890",
            "id": 6,
        }
    }
    */

    // Novas Acoes sobre os campos da tela
    
    // oNewValue guarda os valores a serem especificados para os campos
    ASSIGN oNewValue = NEW JsonObject().
    
    // oNewFields guarda a lista de campos que serao alterados/modificados
    ASSIGN oNewFields = NEW JsonArray().
    
    // cFocus especifica em qual campo sera feito o focus
    ASSIGN cFocus = cProp.

    // oMessages guarda as mensagens de retorno formato 
    // { code: '00', message: 'texto', detailedMessage: 'detalhes da mensagem' }
    ASSIGN oMessages = NEW JsonArray().

    //
    // adicinar a logica de validacoes dos campos aqui
    //
   
    ASSIGN oRet = NEW JsonObject().
    // value -> contem todos os valores dos campos de tela
    oRet:add('value', oNewValue).
    // fields -> contem a lista de campos com suas novas propriedades
    oRet:add('fields', oNewFields).
    // focus -> especifica em qual campo o cursor vai ficar posicionado
    oRet:add('focus', cFocus).
    // _messages -> contem uma lista de mensagens que vao aparecer como notificacoes
    oRet:add('_messages', oMessages).
    
    oResponse   = NEW JsonAPIResponse(oRet).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Bloco de código
languagedelphi
firstline254
linenumberstrue
 PROCEDURE piSetPersonalizationData:
    DEFINE INPUT PARAMETER hTab  AS HANDLE     NO-UNDO.
    DEFINE INPUT PARAMETER oBody AS JsonObject NO-UNDO. 

    FOR EACH ttPersonalization:
        ASSIGN cFld  = ttPersonalization.codField
               cType = ttPersonalization.codType.
        CASE cType:
            WHEN "string"   THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getCharacter(cFld) NO-ERROR.
            WHEN "number"   THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getInteger(cFld) NO-ERROR.
            WHEN "currency" THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getDecimal(cFld) NO-ERROR.
            WHEN "boolean"  THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getLogical(cFld) NO-ERROR.
            WHEN "datetime" THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getDatetime(cFld) NO-ERROR.
            WHEN "date"     THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getDate(cFld) NO-ERROR.
        END CASE.
    END.
END PROCEDURE.
    
/* fim */
Aviso
titleEnvio de valores para PO-UI

Caso sejam enviados valores da área de negócio que não estejam cadastrados como campos personalizados, o PO-UI, o padrão adicionará essa informação extra na tela, onde será apresentado em formato String sem uma label válida.


Tela do componente HTML com o resultado da personalização



Tela de Listagem



Tela de Criação/Edição





Tela de Detalhe/Visualização



Dica
titleDica

Os fontes de exemplo para a personalização também estão disponíveis em nosso GIT (fwk-totvs-jille) em LINKS ÚTEIS.


06. LINKS ÚTEIS

Documentação API Datasul:

https://tdninterno.totvs.com/display/public/FRAMJOI/Desenvolvimento+de+APIs+para+o+produto+Datasul

PO-UI:

Migração THF PO-UI (https://po-ui.io/guides/migration-thf-to-po-ui)

Dynamic-View (https://po-ui.io/documentation/po-dynamic-view);

I18N (https://po-ui.io/documentation/po-i18n)

PO-UI (https://po-ui.io/documentation);

GIT Projeto:

fwk-tools-jille/DATASUL/personalization-poui/ ( https://github.com/totvs/fwk-tools-jille )


07. CONCLUSÃO

A ideia era apresentar uma técnica de construção para a personalização de um programa TOTVS da Liinha Datasul, de forma segura e simples.

Esta documentação trata-se de um MVP, que está sendo continuamente evoluída em nossas Sprints (SQUAD TOOLS).