940 lines
28 KiB
Markdown
940 lines
28 KiB
Markdown
# 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)
|
|
```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 <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:
|
|
```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
|
|
<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:
|
|
```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<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):
|
|
```typescript
|
|
// 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):
|
|
```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 `<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
|
|
```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
|
|
<!-- ❌ 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
|
|
```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. |