# 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 ` - **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) ```json { "success": true, "data": [...], "totalCount": 100, "pageCount": 10, "currentPage": 1, "timestamp": "2024-12-13T10:00:00Z" } ``` #### Erro (400/401/500) ```json { "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 ```http Content-Type: application/json Authorization: Bearer X-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: ```bash npm install ``` 3. Execute o projeto: ```bash 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: ```html
``` ### ❌ NUNCA FAZER: - `` (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: ```typescript // ❌ ERRADO - Usar HttpClient diretamente import { HttpClient } from '@angular/common/http'; constructor(private http: HttpClient) {} this.http.get('api/endpoint') // NUNCA FAZER ISSO! ``` ### ✅ SEMPRE FAZER: ```typescript // ✅ 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: ```typescript 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 { 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 { return this.apiClient.post('examples', data); } update(id: any, data: any): Observable { return this.apiClient.patch(`examples/${id}`, data); } getExamples(page = 1, limit = 10, filters?: any): Observable> { 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>(url); } getById(id: string): Observable { return this.apiClient.get(`examples/${id}`); } delete(id: string): Observable { return this.apiClient.delete(`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): ```typescript // Interface DomainService - OBRIGATÓRIOS getEntities(page: number, pageSize: number, filters: any): Observable create(data: any): Observable update(id: any, data: any): Observable // Métodos específicos - PADRÃO ESTABELECIDO getById(id: string): Observable delete(id: string): Observable get[Domain]s(page: number, limit: number, filters?: any): Observable> ``` #### ❌ MÉTODOS PROIBIDOS (NUNCA usar): ```typescript // 🚫 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript 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 ```typescript // 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: ```typescript // 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 ```typescript 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: ```typescript // 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 ```typescript { 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 ```scss .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 ```typescript { field: "color", header: "Cor", allowHtml: true, label: (value: any) => { const colorCode = value.code || '#999999'; return ` ${value.name} `; } } ``` #### 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 ```typescript // 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 ```html ``` #### Processamento no Submit ```typescript 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.