Pular para o conteúdo principal

Field Layout Service

Gerencia operações relacionadas aos campos individuais de layouts, permitindo buscar configurações detalhadas de campos para renderização de formulários dinâmicos.

Visão Geral

O FieldLayoutService é responsável por:

  • Buscar campos de layout (Field Layouts) da API remota
  • Filtrar campos por ID de layout ou ID de campo
  • Fornecer configurações detalhadas de cada campo (visibilidade, obrigatoriedade, editabilidade, opções)

Os Field Layouts contêm metadados sobre cada campo de um formulário, incluindo validações, valores padrão e opções disponíveis quando aplicável.

Localização: services/field-layout/field-layout.service.ts

Endpoints Utilizados

Base URL

Todos os endpoints são relativos à base URL configurada em services/api.service.ts

Métodos

fetchRemote(params)

Busca campos de layout remotamente com opções de filtro e paginação.

Endpoint: GET /campoLayout

Parâmetros:

type FetchFieldLayoutByLayoutIdParams = {
page?: number; // Número da página (padrão: 0)
size?: number; // Quantidade de itens por página (padrão: 999)
dataHora?: string;
layoutId?: string; // Filtro por ID do layout
fieldId?: string; // Filtro por ID do campo
};

Query Params enviados:

  • page: número da página
  • size: tamanho da página
  • dataHora: timestamp (opcional)
  • layoutId: ID do layout (opcional)
  • fieldId: ID do campo (opcional)

Retorno:

Promise<FieldLayout[]>;

Exemplo de uso:

import { fieldLayoutService } from "@/services/field-layout/field-layout.instance";

// Buscar todos os campos de layout
const allFields = await fieldLayoutService.fetchRemote();

// Buscar campos de um layout específico
const layoutFields = await fieldLayoutService.fetchRemote({
layoutId: "L-001",
});

// Buscar um campo específico
const specificField = await fieldLayoutService.fetchRemote({
fieldId: "F-001",
});

// Buscar com paginação
const fieldsPage = await fieldLayoutService.fetchRemote({
page: 0,
size: 50,
layoutId: "L-002",
});

Tratamento de erros:

  • Lança Error se o status da resposta não for 2xx
  • Mensagem: FieldLayoutService.fetchRemote failed: {status}

Estruturas de Dados

Interface FieldLayout

type FieldLayout = {
fieldLayoutId: string;
requerido: "S" | "N";
editavel: "S" | "N";
visivel: "S" | "N";
valorPadrao: string;
aba: string;
field: FieldBasic;
layout: LayoutBasic;
Opcoes: Option[];
};

Type FieldBasic

type FieldBasic = {
id: string; // ID único do campo
nome: string; // Nome técnico do campo (usado como key)
descricao: string; // Descrição legível do campo (label)
entidade: LayoutEntity; // Entidade a qual o campo pertence
tipo: string; // Tipo de dados (string, integer, decimal, etc.)
};

type LayoutEntity = "Cliente" | "Produto" | "Cabeçalho" | "Contato";

Type LayoutBasic

type LayoutBasic = {
id: string; // ID do layout
idOrganization: string; // ID da organização
nome: string; // Nome do layout
entidade: LayoutEntity; // Entidade do layout
organization: string; // Nome da organização
};

Type Option

type Option = {
id: string; // ID único da opção
idCampo?: string; // ID do campo (quando opção é de campo)
idConfiguracao?: string; // ID da configuração (quando opção é de config)
campo: string | null; // Nome do campo
configuracao: string | null; // Nome da configuração
nome: string; // Nome legível da opção (label)
valor: string; // Valor da opção (value)
};

Componentes Relacionados

buildQueryParams Helper

Função utilitária que converte objeto de parâmetros em URLSearchParams.

Localização: helpers/buildQueryParams.ts

PageResponse Interface

Interface compartilhada para respostas paginadas.

Localização: services/client/client.service.ts

interface PageResponse<T> {
content: T[]; // Array de itens da página
page?: number; // Número da página atual
size?: number; // Tamanho da página
totalPages?: number; // Total de páginas
totalElements?: number; // Total de elementos
}

Fluxo de Integração

Fluxo de Renderização de Formulário

1. App obtém layoutId da entidade
2. App chama fieldLayoutService.fetchRemote({ layoutId })
3. GET /campoLayout é enviado com query params
4. API retorna campos do layout
5. Para cada campo:
a. Verificar visivel === "S" (renderizar ou ocultar)
b. Verificar requerido === "S" (adicionar validação)
c. Verificar editavel === "N" (desabilitar input)
d. Aplicar valorPadrao se definido
e. Renderizar Opcoes se disponíveis
f. Agrupar por aba
6. Formulário é renderizado dinamicamente

Fluxo de Validação de Formulário

1. Usuário preenche formulário
2. Antes de submeter:
a. Para cada campo com requerido === "S":
- Verificar se valor foi preenchido
- Adicionar erro se vazio
b. Para cada campo com tipo específico:
- Validar formato (email, número, etc.)
c. Para campos com Opcoes:
- Verificar se valor está nas opções válidas
3. Se houver erros, exibir e bloquear submissão
4. Se válido, prosseguir com submissão

Casos de Uso

Renderizar Campos de um Layout (exemplo)

// Buscar campos do layout de Cliente
const fields = await fieldLayoutService.fetchRemote({
layoutId: "L-001",
});

// Filtrar campos visíveis
const visibleFields = fields.filter((f) => f.visivel === "S");

// Agrupar por aba
const fieldsByTab = visibleFields.reduce(
(acc, field) => {
if (!acc[field.aba]) acc[field.aba] = [];
acc[field.aba].push(field);
return acc;
},
{} as Record<string, FieldLayout[]>,
);

// Renderizar cada aba
Object.entries(fieldsByTab).forEach(([tab, tabFields]) => {
console.log(`\n=== ${tab} ===`);

tabFields.forEach((field) => {
console.log(`\nCampo: ${field.field.descricao}`);
console.log(` Nome técnico: ${field.field.nome}`);
console.log(` Tipo: ${field.field.tipo}`);
console.log(` Obrigatório: ${field.requerido === "S" ? "Sim" : "Não"}`);
console.log(` Editável: ${field.editavel === "S" ? "Sim" : "Não"}`);

if (field.valorPadrao) {
console.log(` Valor padrão: ${field.valorPadrao}`);
}

if (field.Opcoes.length > 0) {
console.log(` Opções disponíveis:`);
field.Opcoes.forEach((opt) => {
console.log(` - ${opt.nome} (${opt.valor})`);
});
}
});
});

Processar Campos com Opções (Select/Radio) (exemplo)

// Buscar campos
const fields = await fieldLayoutService.fetchRemote({
layoutId: "L-002",
});

// Encontrar campos que têm opções
const fieldsWithOptions = fields.filter((f) => f.Opcoes.length > 0);

fieldsWithOptions.forEach((field) => {
console.log(`\nCampo: ${field.field.descricao}`);
console.log("Tipo de componente sugerido: Select ou Radio");
console.log("Opções:");

field.Opcoes.forEach((option) => {
console.log(` <option value="${option.valor}">${option.nome}</option>`);
});

// Valor padrão (pré-selecionado)
if (field.valorPadrao) {
const defaultOption = field.Opcoes.find(
(o) => o.valor === field.valorPadrao,
);
if (defaultOption) {
console.log(`Valor padrão: ${defaultOption.nome}`);
}
}
});

Aplicar Regras de Edição (exemplo)

// Buscar campos
const fields = await fieldLayoutService.fetchRemote({
layoutId: "L-001",
});

// Separar campos por editabilidade
const editableFields = fields.filter(
(f) => f.editavel === "S" && f.visivel === "S",
);
const readonlyFields = fields.filter(
(f) => f.editavel === "N" && f.visivel === "S",
);

console.log(
"Campos editáveis:",
editableFields.map((f) => f.field.nome),
);
console.log(
"Campos somente leitura:",
readonlyFields.map((f) => f.field.nome),
);

// Ao renderizar, aplicar atributo disabled/readonly
readonlyFields.forEach((field) => {
console.log(
`Campo ${field.field.nome} deve ser renderizado com readonly/disabled`,
);
});

Testes

Localização

services/field-layout/field-layout.service.test.ts

Executar Testes

# Teste específico do serviço
npx jest services/field-layout/field-layout.service.test.ts

# Modo watch
npx jest services/field-layout/field-layout.service.test.ts --watch

# Com coverage
npx jest services/field-layout/field-layout.service.test.ts --coverage

Casos de Teste

  • fetchRemote: busca de campos sem filtros
  • fetchRemote: busca filtrada por layoutId
  • fetchRemote: busca filtrada por fieldId
  • fetchRemote: busca com paginação
  • ✅ Tratamento de erros para status não-2xx
  • ✅ Retorno correto de estrutura FieldLayout

Mocks

Localização

__mocks__/field-layout.mock.ts

Dados Disponíveis

Array extenso de campos de layout de exemplo para diferentes tipos:

  • Campos de texto (string)
  • Campos numéricos (integer, decimal)
  • Campos com opções (select/radio)
  • Campos somente leitura
  • Campos obrigatórios e opcionais

Exemplo de campos no mock:

// Campo obrigatório de texto
{
fieldLayoutId: "FL-002-03",
requerido: "S",
editavel: "S",
visivel: "S",
valorPadrao: "",
aba: "Geral",
field: {
id: "F-012",
nome: "NomeProduto",
descricao: "Nome do produto",
entidade: "Produto",
tipo: "string"
},
Opcoes: []
}

// Campo com opções
{
fieldLayoutId: "FL-002-05",
requerido: "N",
editavel: "S",
visivel: "S",
valorPadrao: "un",
aba: "Geral",
field: {
id: "F-014",
nome: "UnidadeMedida",
descricao: "Unidade de medida do produto",
entidade: "Produto",
tipo: "string"
},
Opcoes: [
{ id: "O-011", nome: "Unidade", valor: "un" },
{ id: "O-012", nome: "Quilograma", valor: "kg" }
]
}

Exemplo de uso em testes:

import { FieldLayouts } from "@/__mocks__/field-layout.mock";

// Mock da API
const apiMock = {
get: jest.fn(async () => ({
status: 200,
data: { content: FieldLayouts },
})),
};

const service = new FieldLayoutService({ apiWithoutAccessToken: apiMock });
const result = await service.fetchRemote({ layoutId: "L-002" });

// Filtrar fields do layout específico
const layoutFields = result.filter((f) => f.layout.id === "L-002");
expect(layoutFields.length).toBeGreaterThan(0);