testes/Modulos Angular/projects/idt_app/docs/general/CURSOR.md

28 KiB

IDT App - Documentação

Visão Geral

O IDT App é uma aplicação Angular para gerenciamento de veículos, desenvolvida como parte do sistema Prafrota. A aplicação permite o gerenciamento completo de frota de veículos, incluindo cadastro, edição, localização e simulação de financiamento.

Estrutura do Projeto

Componentes Principais

  • VehiclesComponent: Componente principal para gerenciamento de veículos
  • VehicleMapComponent: Componente para visualização de veículos em mapa
  • DataTableComponent: Componente reutilizável para exibição de dados em tabela

Funcionalidades Principais

  1. Gerenciamento de Veículos

    • Cadastro de novos veículos
    • Edição de veículos existentes
    • Visualização em tabela com filtros e ordenação
    • Paginação de resultados
  2. Localização

    • Visualização de veículos em mapa
    • Localização específica por placa
  3. Financiamento

    • Simulação de financiamento para veículos
    • Cálculo de valores e parcelas

Campos do Veículo

  • Placa
  • Chassi (VIN)
  • Tipo de Carroceria
  • Marca
  • Cor
  • Descrição
  • Combustível
  • Grupo
  • Ano de Fabricação
  • Marca Registrada
  • Número de Portas
  • RENAVAM
  • Número de Assentos
  • Status
  • Transmissão
  • Tipo de Veículo

Recursos Técnicos

  • Angular Material para componentes UI
  • Componentes standalone
  • Gerenciamento de estado
  • Serviços para comunicação com backend
  • Sistema de formulários dinâmicos
  • Upload de imagens
  • Integração com mapas

API Backend - PraFrota

Endpoint Base

  • URL: https://prafrota-be-bff-tenant-api.grupopra.tech
  • Versão: v1
  • Protocolo: HTTPS
  • Formato: JSON

Autenticação

  • Tipo: Bearer Token (JWT)
  • Header: Authorization: Bearer <token>
  • Renovação: Automática via refresh token
  • Expiração: Configurável por tenant

Estrutura de Rotas da API

Veículos

  • GET /api/v1/vehicles - Listar veículos
  • POST /api/v1/vehicles - Criar veículo
  • PUT /api/v1/vehicles/{id} - Atualizar veículo
  • DELETE /api/v1/vehicles/{id} - Excluir veículo
  • GET /api/v1/vehicles/{id} - Obter veículo específico

Motoristas

  • GET /api/v1/drivers - Listar motoristas
  • POST /api/v1/drivers - Criar motorista
  • PUT /api/v1/drivers/{id} - Atualizar motorista
  • DELETE /api/v1/drivers/{id} - Excluir motorista

Finanças

  • GET /api/v1/finances/accounts-payable - Contas a pagar
  • POST /api/v1/finances/accounts-payable - Criar conta a pagar
  • PUT /api/v1/finances/accounts-payable/{id} - Atualizar conta
  • DELETE /api/v1/finances/accounts-payable/{id} - Excluir conta

Rotas

  • GET /api/v1/routes/mercado-live - Rotas Mercado Live
  • GET /api/v1/routes/shopee - Rotas Shopee
  • POST /api/v1/routes/sync - Sincronizar rotas
  • POST /api/v1/routes/optimize - Otimizar rotas

Padrões de Resposta

Sucesso (200/201)

{
  "success": true,
  "data": [...],
  "totalCount": 100,
  "pageCount": 10,
  "currentPage": 1,
  "timestamp": "2024-12-13T10:00:00Z"
}

Erro (400/401/500)

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Dados inválidos",
    "details": ["Campo obrigatório: placa"]
  },
  "timestamp": "2024-12-13T10:00:00Z"
}

Paginação

  • Parâmetros: page, limit, sort, order
  • Limite máximo: 100 itens por página
  • Padrão: 10 itens por página

Filtros

  • Formato: Query parameters
  • Exemplo: ?status=active&brand=toyota&year=2023
  • Operadores: eq, like, gt, lt, between

Headers Obrigatórios

Content-Type: application/json
Authorization: Bearer <token>
X-Tenant-ID: <tenant-id>
X-Client-Version: 1.0.0

Códigos de Status

  • 200 - Sucesso
  • 201 - Criado com sucesso
  • 400 - Dados inválidos
  • 401 - Não autorizado

Pré-requisitos

  • Node.js
  • Angular CLI
  • Git

Instalação

  1. Clone o repositório
  2. Instale as dependências:
    npm install
    
  3. Execute o projeto:
    ng serve
    

Convenções de Código

  • Uso de TypeScript
  • Componentes standalone
  • Estilo de código seguindo as diretrizes do Angular
  • Documentação de código em inglês

🎯 PADRÃO OBRIGATÓRIO - Template HTML para Componentes de Domínio

Template Padrão BaseDomainComponent

TODOS os componentes que estendem BaseDomainComponent DEVEM usar exatamente este template HTML:

<div class="domain-container">
  <div class="main-content">
    <app-tab-system
      #tabSystem
      [config]="tabConfig"
      [events]="tabEvents"
      [showDebugInfo]="false"
      (tabSelected)="onTabSelected($event)"
      (tabClosed)="onTabClosed($event)"
      (tabAdded)="onTabAdded($event)"
      (tableEvent)="onTableEvent($event)">
    </app-tab-system>
  </div>
</div>

NUNCA FAZER:

  • <app-tab-system></app-tab-system> (sem bindings)
  • Templates customizados para domínios ERP
  • Estruturas HTML diferentes

SEMPRE FAZER:

  • Usar exatamente o template acima
  • Incluir todos os bindings de eventos
  • Manter a estrutura domain-container > main-content
  • Referenciar #tabSystem para controle programático

Componentes que DEVEM seguir este padrão:

  • VehiclesComponent
  • DriversComponent
  • RoutesComponent
  • FinancialCategoriesComponent
  • AccountPayableComponent
  • Qualquer novo componente de domínio ERP

🚫 PADRÃO OBRIGATÓRIO - Services com ApiClientService

NUNCA FAZER:

// ❌ ERRADO - Usar HttpClient diretamente
import { HttpClient } from '@angular/common/http';

constructor(private http: HttpClient) {}

this.http.get('api/endpoint')  // NUNCA FAZER ISSO!

SEMPRE FAZER:

// ✅ CORRETO - Usar ApiClientService
import { ApiClientService } from '../../shared/services/api/api-client.service';

constructor(private apiClient: ApiClientService) {}

this.apiClient.get('endpoint')  // SEMPRE ASSIM!

Template Completo de Service:

import { Injectable } from '@angular/core';
import { Observable, map } from 'rxjs';
import { ApiClientService } from '../../shared/services/api/api-client.service';
import { DomainService } from '../../shared/components/base-domain/base-domain.component';
import { PaginatedResponse } from '../../shared/interfaces/paginate.interface';

@Injectable({
  providedIn: 'root'
})
export class ExampleService implements DomainService<Entity> {
  
  constructor(
    private apiClient: ApiClientService
  ) {}

  getEntities(page: number, pageSize: number, filters: any): Observable<{
    data: Entity[];
    totalCount: number;
    pageCount: number;
    currentPage: number;
  }> {
    return this.getExamples(page, pageSize, filters).pipe(
      map(response => ({
        data: response.data,
        totalCount: response.totalCount,
        pageCount: response.pageCount,
        currentPage: response.currentPage
      }))
    );
  }

  create(data: any): Observable<Entity> {
    return this.apiClient.post<Entity>('examples', data);
  }

  update(id: any, data: any): Observable<Entity> {
    return this.apiClient.patch<Entity>(`examples/${id}`, data);
  }

  getExamples(page = 1, limit = 10, filters?: any): Observable<PaginatedResponse<Entity>> {
    let url = `examples?page=${page}&limit=${limit}`;
    
    if (filters) {
      const params = new URLSearchParams();
      for (const [key, value] of Object.entries(filters)) {
        if (value) {
          params.append(key, value.toString());
        }
      }
      if (params.toString()) {
        url += `&${params.toString()}`;
      }
    }

    return this.apiClient.get<PaginatedResponse<Entity>>(url);
  }

  getById(id: string): Observable<Entity> {
    return this.apiClient.get<Entity>(`examples/${id}`);
  }

  delete(id: string): Observable<void> {
    return this.apiClient.delete<void>(`examples/${id}`);
  }
}

🚫 PADRÕES DE NOMENCLATURA CRÍTICOS - Services

⚠️ REGRAS OBRIGATÓRIAS DE NOMENCLATURA

NUNCA QUEBRAR ESTES PADRÕES! A inconsistência na nomenclatura quebra toda a arquitetura do projeto.

MÉTODOS OBRIGATÓRIOS (SEMPRE seguir):

// Interface DomainService - OBRIGATÓRIOS
getEntities(page: number, pageSize: number, filters: any): Observable<any>
create(data: any): Observable<Entity>
update(id: any, data: any): Observable<Entity>

// Métodos específicos - PADRÃO ESTABELECIDO
getById(id: string): Observable<Entity>
delete(id: string): Observable<void>
get[Domain]s(page: number, limit: number, filters?: any): Observable<PaginatedResponse<Entity>>

MÉTODOS PROIBIDOS (NUNCA usar):

// 🚫 SUFIXOS ESPECÍFICOS - PROIBIDO
createRoute(), createVehicle(), createDriver()     // ❌ Usar: create()
updateRoute(), updateVehicle(), updateDriver()     // ❌ Usar: update()
deleteRoute(), deleteVehicle(), deleteDriver()     // ❌ Usar: delete()
getRoute(), getVehicle(), getDriver()              // ❌ Usar: getById()

// 🚫 NOMENCLATURA ALTERNATIVA - PROIBIDO
addEntity(), editEntity(), removeEntity()          // ❌ Usar: create(), update(), delete()
findById(), searchById(), retrieveById()           // ❌ Usar: getById()
saveEntity(), persistEntity()                      // ❌ Usar: create() ou update()

📋 AUDITORIA DE CONFORMIDADE:

Service create update delete getById get[Domain]s
VehiclesService getVehicles
DriversService getDrivers
RoutesService getRoutes
FinancialCategoriesService getCategories
AccountPayableService getAccounts

🎯 MOTIVOS TÉCNICOS:

  1. BaseDomainComponent espera métodos create, update, delete
  2. DomainService interface define contratos específicos
  3. Consistency com todo o ecosistema Angular do projeto
  4. Maintainability - padrões claros facilitam manutenção
  5. Team Standards - evita confusão entre desenvolvedores

Contribuição

  1. Crie uma branch para sua feature
  2. Faça commit das alterações
  3. Envie um pull request

Padrão de Branches

Estrutura de Branches

  • main: Branch principal de produção
  • develop: Branch de desenvolvimento
  • release/*: Branches para preparação de releases
  • feature/*: Branches para novas funcionalidades
  • bugfix/*: Branches para correções de bugs
  • hotfix/*: Branches para correções urgentes em produção

Convenções de Nomenclatura

  1. Feature Branches

    • Formato: feature/nome-da-feature
    • Exemplo: feature/vehicle-location-map
  2. Bugfix Branches

    • Formato: bugfix/descricao-do-bug
    • Exemplo: bugfix/fix-vehicle-form-validation
  3. Hotfix Branches

    • Formato: hotfix/descricao-do-problema
    • Exemplo: hotfix/fix-critical-security-issue
  4. Release Branches

    • Formato: release/versao
    • Exemplo: release/v1.2.0

Fluxo de Trabalho

  1. Desenvolvimento de Features

    • Criar branch a partir de develop
    • Desenvolver feature
    • Criar PR para develop
    • Após aprovação, merge em develop
  2. Correção de Bugs

    • Criar branch a partir de develop
    • Corrigir bug
    • Criar PR para develop
    • Após aprovação, merge em develop
  3. Preparação de Release

    • Criar branch a partir de develop
    • Realizar testes e ajustes finais
    • Criar PR para main e develop
    • Após aprovação, merge em ambas as branches
  4. Hotfixes

    • Criar branch a partir de main
    • Corrigir problema
    • Criar PR para main e develop
    • Após aprovação, merge em ambas as branches

Regras Importantes

  • Manter branches atualizadas com a branch base
  • Realizar rebase antes de criar PR
  • Seguir padrão de commits convencionais
  • Manter histórico de commits limpo e organizado
  • Deletar branches após merge bem-sucedido

📊 Padrões de Paginação e Listagem

Problema Comum: Paginação com Múltiplas APIs

Quando há necessidade de consultar múltiplas APIs e consolidar os dados, a paginação deve ser implementada localmente.

Estrutura Recomendada

loadData(currentPage = this.currentPage, itemsPerPage = this.itemsPerPage) {
  this.isLoading = true;
  this.data = [];
  this.totalItems = 0;
  
  const sources = ['source1', 'source2', 'source3'];
  let completedRequests = 0;
  let allData: any[] = [];

  // Buscar todos os dados primeiro
  for (const source of sources) {
    this.service.getData(1, 1000, source, this.currentFilters)
      .subscribe({
        next: (response) => {
          if (response.data[0]?.data_string) {
            const datapack = JSON.parse(response.data[0].data_string);
            allData = [...allData, ...datapack];
          }
          completedRequests++;
          
          // Aplicar paginação local quando todas as requisições terminarem
          if (completedRequests === sources.length) {
            // Ordenação opcional
            allData.sort((a, b) => /* critério de ordenação */);
            
            // Aplicar paginação
            this.totalItems = allData.length;
            this.totalPages = Math.ceil(this.totalItems / itemsPerPage);
            
            const startIndex = (currentPage - 1) * itemsPerPage;
            const endIndex = startIndex + itemsPerPage;
            this.data = allData.slice(startIndex, endIndex);
            
            this.isLoading = false;
          }
        },
        error: (error) => {
          completedRequests++;
          if (completedRequests === sources.length) {
            this.isLoading = false;
          }
        }
      });
  }
}

Controle de Mudança de Página

onPageChange(event: { page: number; pageSize: number }) {
  const maxPage = Math.ceil(this.totalItems / event.pageSize);
  const validPage = Math.min(event.page, maxPage);

  if (this.currentPage !== validPage || this.itemsPerPage !== event.pageSize) {
    this.currentPage = validPage;
    this.itemsPerPage = event.pageSize;
    this.loadData(this.currentPage, this.itemsPerPage);
  }
}

DataTable Component Integration

  • Componente: DataTableComponent localizado em shared/components/data-table/
  • Configuração: Usar TableConfig para definir colunas, ações e comportamentos
  • Paginação: Sempre implementar totalDataItems, currentPage e pageChange

Exemplo de Configuração de Tabela

tableConfig: TableConfig = {
  columns: [
    {
      field: "campo",
      header: "Cabeçalho",
      sortable: true,
      filterable: true,
      label: (data: any) => {
        // Transformação de dados para exibição
        return data;
      }
    }
  ],
  pageSize: this.itemsPerPage,
  pageSizeOptions: [5, 10, 25, 50],
  showFirstLastButtons: true,
  actions: [
    {
      icon: "fas fa-edit",
      label: "Editar",
      action: "edit",
    }
  ]
};

📝 Logger Service

Implementação Baseada no Backend

O Logger service foi criado seguindo o mesmo padrão da interface do backend, mantendo compatibilidade de API.

Estrutura do Logger

  • Interface: LoggerService com métodos log, error, warn, debug, verbose, fatal
  • Tipos: LogLevel com todos os níveis de log suportados
  • Classe: Logger que implementa LoggerService

Uso Básico

import { Logger } from '../../../../shared/services/logger';

// Método estático (uso global)
Logger.log('Mensagem de log');
Logger.error('Erro ocorreu', 'MeuComponente');
Logger.warn('Aviso importante');
Logger.debug('Info de debug');
Logger.verbose('Log detalhado');
Logger.fatal('Erro crítico');

// Instância com contexto (recomendado para componentes)
export class MeuComponent {
  private logger = new Logger('MeuComponent');
  
  ngOnInit() {
    this.logger.log('Componente inicializado');
    this.logger.debug('Dados carregados:', dados);
  }
  
  onError(error: any) {
    this.logger.error('Erro no processamento:', error);
  }
}

Funcionalidades Avançadas

// Configuração personalizada
const logger = new Logger('DataService', {
  timestamp: true,
  environment: 'production',
  enableConsole: true,
  enableRemoteLogging: false
});

// Controle de níveis de log
Logger.overrideLogger(['error', 'warn']); // Só mostra erros e avisos
Logger.overrideLogger(false); // Desabilita todos os logs
Logger.overrideLogger(true);  // Habilita todos os logs

// Buffer para logs de inicialização
Logger.attachBuffer();
Logger.log('Log armazenado no buffer');
Logger.detachBuffer(); // Flush todos os logs

// Verificação condicional
if (Logger.isLevelEnabled('debug')) {
  const expensiveData = calculateDebugData();
  Logger.debug('Debug data:', expensiveData);
}

Cores e Formatação

O Logger inclui formatação colorida no console:

  • LOG: Azul (#2196F3)
  • ERROR/FATAL: Vermelho (#f44336/#d32f2f)
  • WARN: Laranja (#ff9800)
  • DEBUG: Verde (#4caf50)
  • VERBOSE: Roxo (#9c27b0)

Localização

  • Arquivos: shared/services/logger/
  • Interface: logger.interface.ts
  • Implementação: logger.service.ts
  • Exemplos: logger.example.ts
  • Export: index.ts

🔄 Padrões de Mapeamento de Dados

Mapeamento de APIs Heterogêneas para Interface Unificada

Quando trabalhando com múltiplas APIs que retornam estruturas diferentes, use o padrão de mapeamento específico por tipo.

Estrutura de Mapeamento

// Método principal de mapeamento
private mapRoutesToInterface(datapack: any[], type: string): InterfacePadrao[] {
  return datapack.map(item => {
    switch (type) {
      case 'tipo1':
        return this.mapTipo1(item);
      case 'tipo2':
        return this.mapTipo2(item);
      default:
        return this.mapGenerico(item, type);
    }
  });
}

// Mapeamentos específicos por tipo
private mapTipo1(data: any): InterfacePadrao {
  return {
    id: data.id?.toString() || '',
    campo1: data.campo_api || 'valor_padrao',
    status: this.mapStatus(data.status),
    // ... outros campos
  };
}

Exemplo: Rotas Mercado Livre

// Aplicação no recebimento de dados
const datapack = JSON.parse(response.data[0].data_string);
const mappedRoutes = this.mapRoutesToInterface(datapack, type);
allRoutes = [...allRoutes, ...mappedRoutes];

// Mapeamento específico por tipo de rota
private mapFirstMileRoute(route: any): MercadoLiveRoute {
  return {
    id: route.id?.toString() || '',
    customerName: route.carrierName || 'N/A',
    estimatedPackages: route.estimatedPackages || 0,
    priority: route.warnings?.length > 0 ? 'high' : 'medium'
  };
}

Padrões de Status Normalization

private mapStatus(status: string): StatusPadrao {
  const statusMap: { [key: string]: StatusPadrao } = {
    'active': 'in_transit',
    'pending': 'pending',
    'finished': 'delivered',
    'cancelled': 'cancelled'
  };
  
  return statusMap[status?.toLowerCase()] || 'pending';
}

Tratamento de Dados Opcionais

// Safe navigation e fallbacks
campo: data.nivel1?.nivel2?.campo || 'valor_padrao',
data: data.timestamp ? new Date(data.timestamp * 1000) : new Date(),
numero: data.valor || 0,
array: data.lista?.length ? data.lista : []

Interfaces Específicas por Tipo

Criar interfaces específicas para cada estrutura de dados diferente:

// Interfaces específicas por tipo de fonte
export interface FirstMileRoute {
  id: number;
  routeType: string;
  facilityName: string;
  estimatedPackages: number;
  vehicleName: string;
  driverName: string;
  warnings: FirstMileWarning[];
}

export interface LineHaulRoute {
  carrier_id: number;
  carrier: string;
  drivers: LineHaulDriver[];
  vehicles: LineHaulVehicle[];
  steps: LineHaulStep[];
  stops: LineHaulStop[];
}

export interface LastMileRoute {
  id: string;
  cluster: string;
  driver: LastMileDriver;
  counters: LastMileCounters;
  timingData: LastMileTimingData;
}

// Union type para tipagem de entrada
export type MercadoLiveRouteRaw = FirstMileRoute | LineHaulRoute | LastMileRoute;

Mapeamento com Type Safety

private mapRoutesToInterface(datapack: MercadoLiveRouteRaw[], type: string): MercadoLiveRoute[] {
  return datapack.map(route => {
    switch (type) {
      case 'first_mile':
        return this.mapFirstMileRoute(route as FirstMileRoute);
      case 'line_haul':
        return this.mapLineHaulRoute(route as LineHaulRoute);
      case 'last_mile':
        return this.mapLastMileRoute(route as LastMileRoute);
    }
  });
}

private mapFirstMileRoute(route: FirstMileRoute): MercadoLiveRoute {
  return {
    id: route.id.toString(),
    customerName: route.carrierName,
    estimatedPackages: route.estimatedPackages,
    vehicleType: route.vehicleType, // FirstMileRoute.vehicleType
    locationName: route.facilityName, // FirstMileRoute.facilityName
    driverName: route.driverName, // FirstMileRoute.driverName
    DepartureDate: new Date(route.initDate * 1000), // timestamp → Date
    priority: route.warnings.length > 0 ? 'high' : 'medium'
  };
}

private mapLineHaulRoute(route: LineHaulRoute): MercadoLiveRoute {
  return {
    // ...
    vehicleType: route.vehicle_type, // LineHaulRoute.vehicle_type
    locationName: route.site_id, // LineHaulRoute.site_id
    driverName: route.drivers[0]?.name, // LineHaulRoute.drivers[0].name
    DepartureDate: new Date(route.departure_date), // datetime → Date
    // ...
  };
}

private mapLastMileRoute(route: LastMileRoute): MercadoLiveRoute {
  return {
    // ...
    vehicleType: route.vehicle.description, // LastMileRoute.vehicle.description
    locationName: route.facilityId, // LastMileRoute.facilityId
    driverName: route.driver.driverName, // LastMileRoute.driver.driverName
    DepartureDate: new Date(route.initDate * 1000), // timestamp → Date
    // ...
  };
}

Conversão de Formatos de Data

Quando diferentes APIs retornam datas em formatos distintos, implemente conversões específicas:

// Função concisa para conversão automática de datas
private convertToDate(dateValue: any): Date {
  if (!dateValue) return new Date();
  
  if (typeof dateValue === 'number') {
    // Se menor que 10^12, está em segundos; senão, em milissegundos
    return new Date(dateValue < 1000000000000 ? dateValue * 1000 : dateValue);
  }
  
  if (typeof dateValue === 'string') {
    return new Date(dateValue);
  }
  
  return new Date();
}

// Exemplos de conversão por tipo usando a função auxiliar
DepartureDate: this.convertToDate(route.initDate),        // auto-detecta formato
DepartureDate: this.convertToDate(route.departure_date),  // auto-detecta formato
estimatedDelivery: this.convertToDate(route.finalDate),   // auto-detecta formato

// Exemplos de dados reais:
// "initDate": 1748351864 (timestamp segundos) → 25/01/2025
// "departure_date": "2025-05-25T16:48:47Z" (ISO string) → 25/05/2025

Vantagens do Padrão

Consistência: Dados uniformizados independente da origem
Manutenibilidade: Fácil adição de novos tipos
Legibilidade: Mapeamentos específicos e organizados
Robustez: Tratamento de dados faltantes
Type Safety: Garantia de tipos através de interfaces específicas
IntelliSense: Autocompletar e validação em tempo de desenvolvimento
Detecção de Erros: Erros de tipagem detectados em build time
Conversão de Formatos: Normalização automática de timestamps e datetime strings

🎨 Componentes de Interface Avançados

Color Input Component

Componente especializado para seleção de cores com interface visual intuitiva.

Funcionalidades

  • Dropdown Visual: Grid de círculos coloridos com nomes
  • Preview Seleção: Mostra cor selecionada no botão principal
  • Botão Limpar: Opção para remover seleção
  • Overlay Inteligente: Fecha ao clicar fora
  • Responsive: Layout adaptado para mobile
  • Tema Escuro: Suporte completo a temas

Implementação

{
  key: 'color',
  label: 'Cor',
  type: 'color-input',
  required: false,
  options: [
    { value: { name: 'Branco', code: '#ffffff' }, label: 'Branco' },
    { value: { name: 'Preto', code: '#000000' }, label: 'Preto' },
    // ... outras cores
  ]
}

Integração com Data Table

  • Renderização HTML: Círculos de cor nas células da tabela
  • DomSanitizer: HTML seguro com bypassSecurityTrustHtml()
  • Fallback Inteligente: Mapa de cores para objetos sem código hex
  • Configuração: allowHtml: true na configuração da coluna

Indicadores de Campos Obrigatórios

Sistema unificado de sinalização visual para campos obrigatórios em formulários.

Componentes Atualizados

  • custom-input: Asterisco vermelho nos labels
  • color-input: Suporte nativo no template inline
  • kilometer-input: Asterisco + interface TypeScript atualizada
  • generic-tab-form: Labels dos selects nativos com asterisco
  • remote-select: Sistema required-asterisk já implementado
  • multi-select: Sistema required-asterisk já implementado

CSS Unificado

.required-indicator {
  color: var(--idt-danger, #dc3545);
  margin-left: 4px;
  font-weight: 700;
  font-size: 16px;
  line-height: 1;
}

Vantagens

  • UX Melhorada: Usuários sabem quais campos são obrigatórios
  • Consistência Visual: Mesmo padrão em todos os componentes
  • Acessibilidade: Indicação clara de campos obrigatórios
  • Prevenção de Erros: Reduz tentativas de submit com dados incompletos

Data Table HTML Rendering

Sistema seguro para renderização de HTML personalizado em células de tabela.

Configuração de Coluna

{
  field: "color",
  header: "Cor",
  allowHtml: true,
  label: (value: any) => {
    const colorCode = value.code || '#999999';
    return `<span style="display: inline-flex; align-items: center; gap: 6px;">
              <span style="width: 12px; height: 12px; border-radius: 50%; 
                           background-color: ${colorCode}; border: 1px solid #ddd;"></span>
              <span>${value.name}</span>
            </span>`;
  }
}

Segurança

  • DomSanitizer: Uso de bypassSecurityTrustHtml() para HTML seguro
  • Validação: Verificação de allowHtml antes da renderização
  • Fallback: Renderização de texto simples quando HTML não é permitido

🔧 Padrões de Validação e Formulários

Validação Condicional

Sistema inteligente que aplica validação apenas quando necessário.

Implementação

// Validação aplicada apenas para campos required: true
createOptionValidator(field: TabFormField): ValidatorFn | null {
  if (!field.required) return null;
  
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) return { required: true };
    
    if (field.returnObjectSelected) {
      return this.isValidObjectSelection(control.value, field) ? null : { invalidOption: true };
    }
    
    return this.isValidPrimitiveSelection(control.value, field) ? null : { invalidOption: true };
  };
}

Vantagens

  • Performance: Validação apenas quando necessário
  • Flexibilidade: Campos opcionais não geram erros
  • Robustez: Suporte a objetos complexos e valores primitivos

Serialização de Objetos em Formulários

Tratamento correto de campos que retornam objetos complexos.

Problema Comum

<!-- ❌ INCORRETO: Serializa como "[object Object]" -->
<option [value]="option.value">{{ option.label }}</option>

<!-- ✅ CORRETO: Preserva objeto completo -->
<option [ngValue]="option.value">{{ option.label }}</option>

Processamento no Submit

onSubmit(): void {
  const formData = { ...this.form.value };
  
  // Processar campos com returnObjectSelected
  this.config.fields
    .filter(field => field.returnObjectSelected)
    .forEach(field => {
      if (formData[field.key] && typeof formData[field.key] === 'object') {
        // Objeto já está correto, não precisa processar
        console.log(`✅ Campo ${field.key} já é objeto:`, formData[field.key]);
      }
    });
  
  this.submitData.emit(formData);
}

Suporte

Para suporte ou dúvidas, entre em contato com a equipe de desenvolvimento.