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ículosVehicleMapComponent: Componente para visualização de veículos em mapaDataTableComponent: Componente reutilizável para exibição de dados em tabela
Funcionalidades Principais
-
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
-
Localização
- Visualização de veículos em mapa
- Localização específica por placa
-
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ículosPOST /api/v1/vehicles- Criar veículoPUT /api/v1/vehicles/{id}- Atualizar veículoDELETE /api/v1/vehicles/{id}- Excluir veículoGET /api/v1/vehicles/{id}- Obter veículo específico
Motoristas
GET /api/v1/drivers- Listar motoristasPOST /api/v1/drivers- Criar motoristaPUT /api/v1/drivers/{id}- Atualizar motoristaDELETE /api/v1/drivers/{id}- Excluir motorista
Finanças
GET /api/v1/finances/accounts-payable- Contas a pagarPOST /api/v1/finances/accounts-payable- Criar conta a pagarPUT /api/v1/finances/accounts-payable/{id}- Atualizar contaDELETE /api/v1/finances/accounts-payable/{id}- Excluir conta
Rotas
GET /api/v1/routes/mercado-live- Rotas Mercado LiveGET /api/v1/routes/shopee- Rotas ShopeePOST /api/v1/routes/sync- Sincronizar rotasPOST /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- Sucesso201- Criado com sucesso400- Dados inválidos401- Não autorizado
Pré-requisitos
- Node.js
- Angular CLI
- Git
Instalação
- Clone o repositório
- Instale as dependências:
npm install - 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:
- BaseDomainComponent espera métodos
create,update,delete - DomainService interface define contratos específicos
- Consistency com todo o ecosistema Angular do projeto
- Maintainability - padrões claros facilitam manutenção
- Team Standards - evita confusão entre desenvolvedores
Contribuição
- Crie uma branch para sua feature
- Faça commit das alterações
- Envie um pull request
Padrão de Branches
Estrutura de Branches
main: Branch principal de produçãodevelop: Branch de desenvolvimentorelease/*: Branches para preparação de releasesfeature/*: Branches para novas funcionalidadesbugfix/*: Branches para correções de bugshotfix/*: Branches para correções urgentes em produção
Convenções de Nomenclatura
-
Feature Branches
- Formato:
feature/nome-da-feature - Exemplo:
feature/vehicle-location-map
- Formato:
-
Bugfix Branches
- Formato:
bugfix/descricao-do-bug - Exemplo:
bugfix/fix-vehicle-form-validation
- Formato:
-
Hotfix Branches
- Formato:
hotfix/descricao-do-problema - Exemplo:
hotfix/fix-critical-security-issue
- Formato:
-
Release Branches
- Formato:
release/versao - Exemplo:
release/v1.2.0
- Formato:
Fluxo de Trabalho
-
Desenvolvimento de Features
- Criar branch a partir de
develop - Desenvolver feature
- Criar PR para
develop - Após aprovação, merge em
develop
- Criar branch a partir de
-
Correção de Bugs
- Criar branch a partir de
develop - Corrigir bug
- Criar PR para
develop - Após aprovação, merge em
develop
- Criar branch a partir de
-
Preparação de Release
- Criar branch a partir de
develop - Realizar testes e ajustes finais
- Criar PR para
mainedevelop - Após aprovação, merge em ambas as branches
- Criar branch a partir de
-
Hotfixes
- Criar branch a partir de
main - Corrigir problema
- Criar PR para
mainedevelop - Após aprovação, merge em ambas as branches
- Criar branch a partir de
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:
DataTableComponentlocalizado emshared/components/data-table/ - Configuração: Usar
TableConfigpara definir colunas, ações e comportamentos - Paginação: Sempre implementar
totalDataItems,currentPageepageChange
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:
LoggerServicecom métodoslog,error,warn,debug,verbose,fatal - Tipos:
LogLevelcom todos os níveis de log suportados - Classe:
Loggerque implementaLoggerService
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: truena 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-asteriskjá implementado - ✅ multi-select: Sistema
required-asteriskjá 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
allowHtmlantes 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.