# 🌳 Guia de Implementação de Dados Hierárquicos ## 📋 Resumo Documentação para implementação de telas com apresentação hierárquica (árvore) no framework PraFrota, utilizando registros com `parent_id` para criar estruturas de categorias, departamentos, planos de contas e outras hierarquias do ERP. ## 🎯 Casos de Uso no ERP ### **Módulos que Necessitam Hierarquia:** - 📦 **Categoria de Produtos** - Categoria > Subcategoria > Produto - 💰 **Plano de Contas** - Receitas/Despesas > Grupos > Subgrupos > Contas - 👥 **Departamentos** - Empresa > Diretoria > Gerência > Setor - 📊 **DRE (Demonstração de Resultado)** - Receita Bruta > Deduções > Receita Líquida - 💸 **Fluxo de Caixa** - Operacional > Investimento > Financiamento - 🏢 **Estrutura Organizacional** - Holding > Empresas > Filiais > Setores - 📋 **Centro de Custos** - Principal > Secundário > Auxiliar ## 🏗️ Arquitetura Proposta ### **1. Interface Base para Dados Hierárquicos** ```typescript // shared/interfaces/hierarchical-entity.interface.ts export interface HierarchicalEntity { id: string | number; name: string; parent_id: string | number | null; level?: number; // Nível na hierarquia (0 = raiz) path?: string; // Caminho completo (ex: "1/2/3") children?: HierarchicalEntity[]; expanded?: boolean; // Para controle de expansão na UI order?: number; // Ordem de exibição // Campos específicos do domínio [key: string]: any; } // Exemplo específico para Categoria de Produtos export interface ProductCategory extends HierarchicalEntity { code?: string; description?: string; active: boolean; created_at: string; updated_at: string; } ``` ### **2. Service Base para Hierarquia** ```typescript // shared/services/hierarchical-base.service.ts import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { HierarchicalEntity } from '../interfaces/hierarchical-entity.interface'; @Injectable() export abstract class HierarchicalBaseService { /** * Converte lista plana em estrutura hierárquica */ buildHierarchy(flatData: T[], rootParentId: any = null): T[] { const itemMap = new Map(); const rootItems: T[] = []; // Primeiro, criar mapa de todos os itens flatData.forEach(item => { item.children = []; itemMap.set(item.id, item); }); // Depois, construir a hierarquia flatData.forEach(item => { if (item.parent_id === rootParentId || item.parent_id === null) { rootItems.push(item); } else { const parent = itemMap.get(item.parent_id); if (parent) { parent.children = parent.children || []; parent.children.push(item); } } }); return this.sortHierarchy(rootItems); } /** * Ordena hierarquia por ordem ou nome */ private sortHierarchy(items: T[]): T[] { return items.sort((a, b) => { const orderA = a.order || 0; const orderB = b.order || 0; if (orderA !== orderB) { return orderA - orderB; } return a.name.localeCompare(b.name); }).map(item => { if (item.children && item.children.length > 0) { item.children = this.sortHierarchy(item.children); } return item; }); } /** * Converte hierarquia em lista plana */ flattenHierarchy(hierarchicalData: T[]): T[] { const result: T[] = []; const flatten = (items: T[], level: number = 0) => { items.forEach(item => { const flatItem = { ...item }; flatItem.level = level; delete flatItem.children; // Remove children para evitar referência circular result.push(flatItem); if (item.children && item.children.length > 0) { flatten(item.children, level + 1); } }); }; flatten(hierarchicalData); return result; } /** * Encontra todos os filhos de um item */ findAllChildren(items: T[], parentId: any): T[] { const children: T[] = []; const findChildren = (currentItems: T[]) => { currentItems.forEach(item => { if (item.parent_id === parentId) { children.push(item); if (item.children) { findChildren(item.children); } } else if (item.children) { findChildren(item.children); } }); }; findChildren(items); return children; } /** * Calcula o caminho completo do item */ buildPath(items: T[], targetId: any): string { const path: string[] = []; const findPath = (currentItems: T[], searchId: any): boolean => { for (const item of currentItems) { path.push(item.id.toString()); if (item.id === searchId) { return true; } if (item.children && findPath(item.children, searchId)) { return true; } path.pop(); } return false; }; findPath(items, targetId); return path.join('/'); } /** * Valida se um item pode ser movido para um novo pai */ canMoveTo(items: T[], itemId: any, newParentId: any): boolean { // Não pode mover para si mesmo if (itemId === newParentId) { return false; } // Não pode mover para um de seus filhos (evitar loop) const children = this.findAllChildren(items, itemId); return !children.some(child => child.id === newParentId); } } ``` ### **3. Component Base para Hierarquia** ```typescript // shared/components/hierarchical-base/hierarchical-base.component.ts import { Component, Input, Output, EventEmitter } from '@angular/core'; import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; import { HierarchicalEntity } from '../../interfaces/hierarchical-entity.interface'; import { HierarchicalBaseService } from '../../services/hierarchical-base.service'; @Component({ selector: 'app-hierarchical-base', template: `

{{ selectedItem.name }}

`, styleUrls: ['./hierarchical-base.component.scss'] }) export class HierarchicalBaseComponent { @Input() hierarchicalData: T[] = []; @Input() allowDrag: boolean = true; @Input() allowEdit: boolean = true; @Input() allowDelete: boolean = true; @Input() allowAdd: boolean = true; @Output() itemSelected = new EventEmitter(); @Output() itemAdded = new EventEmitter(); // null = adicionar raiz @Output() itemEdited = new EventEmitter(); @Output() itemDeleted = new EventEmitter(); @Output() itemMoved = new EventEmitter<{item: T, newParent: T | null}>(); selectedItem: T | null = null; searchTerm: string = ''; filteredData: T[] = []; constructor(protected hierarchicalService: HierarchicalBaseService) {} ngOnInit() { this.filteredData = [...this.hierarchicalData]; } ngOnChanges() { this.filteredData = [...this.hierarchicalData]; } onDrop(event: CdkDragDrop) { const movedItem = event.previousContainer.data[event.previousIndex]; const newParent = this.findDropTarget(event); if (this.hierarchicalService.canMoveTo(this.hierarchicalData, movedItem.id, newParent?.id)) { if (event.previousContainer === event.container) { moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); } else { transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex ); } movedItem.parent_id = newParent?.id ?? null; this.itemMoved.emit({ item: movedItem, newParent }); } } onNodeExpanded(item: T) { item.expanded = !item.expanded; } onNodeSelected(item: T) { this.selectedItem = item; this.itemSelected.emit(item); } onAdd(parent: T | null) { this.itemAdded.emit(parent); } onEdit(item: T) { this.itemEdited.emit(item); } onDelete(item: T) { this.itemDeleted.emit(item); } onSearch() { if (!this.searchTerm.trim()) { this.filteredData = [...this.hierarchicalData]; return; } const searchLower = this.searchTerm.toLowerCase(); this.filteredData = this.filterHierarchy(this.hierarchicalData, searchLower); } private filterHierarchy(items: T[], searchTerm: string): T[] { return items.filter(item => { const matchesSearch = item.name.toLowerCase().includes(searchTerm); const hasMatchingChildren = item.children && this.filterHierarchy(item.children, searchTerm).length > 0; if (matchesSearch || hasMatchingChildren) { if (hasMatchingChildren) { item.children = this.filterHierarchy(item.children!, searchTerm); } return true; } return false; }); } expandAll() { this.setExpandedState(this.hierarchicalData, true); } collapseAll() { this.setExpandedState(this.hierarchicalData, false); } private setExpandedState(items: T[], expanded: boolean) { items.forEach(item => { item.expanded = expanded; if (item.children) { this.setExpandedState(item.children, expanded); } }); } private findDropTarget(event: CdkDragDrop): T | null { // Lógica para determinar o novo pai baseado na posição do drop // Implementação específica dependendo do layout return null; } } ``` ## 🎨 Estilos CSS ```scss // hierarchical-base.component.scss .hierarchical-container { display: flex; flex-direction: column; height: 100%; .hierarchy-toolbar { display: flex; gap: 1rem; padding: 1rem; border-bottom: 1px solid var(--divider); align-items: center; .search-box { margin-left: auto; width: 300px; } } .hierarchy-tree { flex: 1; overflow-y: auto; padding: 1rem; } .details-panel { width: 300px; border-left: 1px solid var(--divider); padding: 1rem; } } // tree-node.component.scss .tree-node { display: flex; align-items: center; padding: 0.5rem; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; &:hover { background-color: var(--surface-hover); } &.selected { background-color: var(--primary-light); color: var(--primary); } .expand-btn { width: 20px; height: 20px; border: none; background: transparent; cursor: pointer; margin-right: 0.5rem; } .item-icon { margin-right: 0.5rem; color: var(--text-secondary); } .item-name { flex: 1; font-weight: 500; } .item-actions { display: flex; gap: 0.25rem; opacity: 0; transition: opacity 0.2s; .btn-action { width: 24px; height: 24px; border: none; background: transparent; border-radius: 2px; cursor: pointer; &:hover { background-color: var(--surface-hover); } &.btn-danger:hover { background-color: var(--error); color: white; } } } &:hover .item-actions { opacity: 1; } } .children-container { border-left: 1px dashed var(--divider); margin-left: 10px; } ``` ## 🚀 Implementação Específica por Domínio ### **Exemplo: Categoria de Produtos** ```typescript // domain/product-categories/product-categories.component.ts import { Component } from '@angular/core'; import { HierarchicalBaseComponent } from '../../shared/components/hierarchical-base/hierarchical-base.component'; import { ProductCategory } from './product-category.interface'; import { ProductCategoriesService } from './product-categories.service'; @Component({ selector: 'app-product-categories', template: `
{{ selectedCategory.code }}

{{ selectedCategory.description }}

{{ selectedCategory.active ? 'Ativo' : 'Inativo' }}
` }) export class ProductCategoriesComponent extends HierarchicalBaseComponent { categories: ProductCategory[] = []; selectedCategory: ProductCategory | null = null; constructor( private categoriesService: ProductCategoriesService, hierarchicalService: HierarchicalBaseService ) { super(hierarchicalService); } ngOnInit() { this.loadCategories(); } loadCategories() { this.categoriesService.getAll().subscribe(flatData => { this.categories = this.hierarchicalService.buildHierarchy(flatData); this.hierarchicalData = this.categories; }); } onCategorySelected(category: ProductCategory) { this.selectedCategory = category; } onCategoryAdd(parent: ProductCategory | null) { // Abrir modal de criação const newCategory: Partial = { name: '', parent_id: parent?.id || null, active: true }; // Implementar modal... } onCategoryEdit(category: ProductCategory) { // Abrir modal de edição // Implementar modal... } onCategoryDelete(category: ProductCategory) { this.categoriesService.delete(category.id).subscribe(() => { this.loadCategories(); }); } onCategoryMoved(event: {item: ProductCategory, newParent: ProductCategory | null}) { this.categoriesService.updateParent(event.item.id, event.newParent?.id || null) .subscribe(() => { this.loadCategories(); }); } } ``` ## 📊 Casos de Uso Específicos ### **1. Plano de Contas** ```typescript export interface ChartOfAccounts extends HierarchicalEntity { code: string; // 1.1.01.001 account_type: 'ASSET' | 'LIABILITY' | 'EQUITY' | 'REVENUE' | 'EXPENSE'; balance_type: 'DEBIT' | 'CREDIT'; accepts_entries: boolean; // Se aceita lançamentos diretos current_balance: number; } ``` #### **Implementação Completa: Plano de Contas** ```typescript // domain/chart-of-accounts/chart-of-accounts.interface.ts export interface ChartOfAccounts extends HierarchicalEntity { code: string; // Código da conta (ex: 1.1.01.001) account_type: AccountType; // Tipo da conta balance_type: BalanceType; // Natureza do saldo accepts_entries: boolean; // Se aceita lançamentos diretos current_balance: number; // Saldo atual description?: string; // Descrição detalhada is_synthetic: boolean; // Se é conta sintética (apenas agrupamento) is_active: boolean; // Se está ativa created_at: string; updated_at: string; } export enum AccountType { ASSET = 'ASSET', // 1 - Ativo LIABILITY = 'LIABILITY', // 2 - Passivo EQUITY = 'EQUITY', // 3 - Patrimônio Líquido REVENUE = 'REVENUE', // 4 - Receitas EXPENSE = 'EXPENSE' // 5 - Despesas } export enum BalanceType { DEBIT = 'DEBIT', // Natureza devedora CREDIT = 'CREDIT' // Natureza credora } // domain/chart-of-accounts/chart-of-accounts.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { HierarchicalBaseService } from '../../shared/services/hierarchical-base.service'; import { ChartOfAccounts, AccountType } from './chart-of-accounts.interface'; @Injectable() export class ChartOfAccountsService extends HierarchicalBaseService { private readonly apiUrl = 'https://prafrota-be-bff-tenant-api.grupopra.tech/api'; constructor(private http: HttpClient) { super(); } getAll(): Observable { return this.http.get(`${this.apiUrl}/chart-of-accounts`); } getByType(accountType: AccountType): Observable { return this.http.get(`${this.apiUrl}/chart-of-accounts`, { params: { account_type: accountType } }); } create(account: Partial): Observable { return this.http.post(`${this.apiUrl}/chart-of-accounts`, account); } update(id: any, account: Partial): Observable { return this.http.put(`${this.apiUrl}/chart-of-accounts/${id}`, account); } delete(id: any): Observable { return this.http.delete(`${this.apiUrl}/chart-of-accounts/${id}`); } updateParent(id: any, newParentId: any): Observable { return this.http.patch(`${this.apiUrl}/chart-of-accounts/${id}`, { parent_id: newParentId }); } /** * Gera próximo código disponível baseado no pai */ generateNextCode(parentId: any): Observable { return this.http.get<{next_code: string}>(`${this.apiUrl}/chart-of-accounts/next-code`, { params: { parent_id: parentId || '' } }).pipe(map(response => response.next_code)); } /** * Valida se o código já existe */ validateCode(code: string, excludeId?: any): Observable { const params: any = { code }; if (excludeId) { params.exclude_id = excludeId; } return this.http.get<{available: boolean}>(`${this.apiUrl}/chart-of-accounts/validate-code`, { params }).pipe(map(response => response.available)); } /** * Busca contas que aceitam lançamentos */ getAnalyticalAccounts(): Observable { return this.http.get(`${this.apiUrl}/chart-of-accounts/analytical`); } } // domain/chart-of-accounts/chart-of-accounts.component.ts import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HierarchicalBaseComponent } from '../../shared/components/hierarchical-base/hierarchical-base.component'; import { ChartOfAccounts, AccountType, BalanceType } from './chart-of-accounts.interface'; import { ChartOfAccountsService } from './chart-of-accounts.service'; import { HierarchicalBaseService } from '../../shared/services/hierarchical-base.service'; @Component({ selector: 'app-chart-of-accounts', standalone: true, imports: [CommonModule, FormsModule], template: `
`, styleUrls: ['./chart-of-accounts.component.scss'] }) export class ChartOfAccountsComponent extends HierarchicalBaseComponent implements OnInit { accounts: ChartOfAccounts[] = []; filteredAccounts: ChartOfAccounts[] = []; selectedAccount: ChartOfAccounts | null = null; selectedAccountType: AccountType | null = null; accountTypes = [ { value: AccountType.ASSET, label: 'Ativo', icon: 'fas fa-coins' }, { value: AccountType.LIABILITY, label: 'Passivo', icon: 'fas fa-credit-card' }, { value: AccountType.EQUITY, label: 'Patrimônio', icon: 'fas fa-building' }, { value: AccountType.REVENUE, label: 'Receitas', icon: 'fas fa-arrow-up' }, { value: AccountType.EXPENSE, label: 'Despesas', icon: 'fas fa-arrow-down' } ]; constructor( private accountsService: ChartOfAccountsService, hierarchicalService: HierarchicalBaseService ) { super(hierarchicalService); } ngOnInit() { this.loadAccounts(); } loadAccounts() { this.accountsService.getAll().subscribe(flatData => { this.accounts = this.hierarchicalService.buildHierarchy(flatData); this.applyFilters(); }); } filterByAccountType(accountType: AccountType | null) { this.selectedAccountType = accountType; this.applyFilters(); } private applyFilters() { if (this.selectedAccountType) { this.filteredAccounts = this.filterAccountsByType(this.accounts, this.selectedAccountType); } else { this.filteredAccounts = [...this.accounts]; } this.hierarchicalData = this.filteredAccounts; } private filterAccountsByType(accounts: ChartOfAccounts[], accountType: AccountType): ChartOfAccounts[] { return accounts.filter(account => { const matchesType = account.account_type === accountType; const hasMatchingChildren = account.children && this.filterAccountsByType(account.children, accountType).length > 0; if (matchesType || hasMatchingChildren) { if (hasMatchingChildren) { account.children = this.filterAccountsByType(account.children!, accountType); } return true; } return false; }); } onAccountSelected(account: ChartOfAccounts) { this.selectedAccount = account; } onAccountAdd(parent: ChartOfAccounts | null) { // Gerar próximo código this.accountsService.generateNextCode(parent?.id || null).subscribe(nextCode => { const newAccount: Partial = { name: '', code: nextCode, parent_id: parent?.id || null, account_type: parent?.account_type || AccountType.ASSET, balance_type: this.getDefaultBalanceType(parent?.account_type || AccountType.ASSET), accepts_entries: true, is_synthetic: false, is_active: true, current_balance: 0 }; // Abrir modal de criação com dados pré-preenchidos this.openAccountModal(newAccount); }); } onAccountEdit(account: ChartOfAccounts) { this.openAccountModal(account); } onAccountDelete(account: ChartOfAccounts) { if (account.children && account.children.length > 0) { alert('Não é possível excluir uma conta que possui subcontas.'); return; } if (account.current_balance !== 0) { alert('Não é possível excluir uma conta com saldo.'); return; } this.accountsService.delete(account.id).subscribe(() => { this.loadAccounts(); }); } onAccountMoved(event: {item: ChartOfAccounts, newParent: ChartOfAccounts | null}) { // Validar se o movimento é permitido if (!this.canMoveAccount(event.item, event.newParent)) { alert('Movimento não permitido. Verifique os tipos de conta.'); return; } this.accountsService.updateParent(event.item.id, event.newParent?.id || null) .subscribe(() => { this.loadAccounts(); }); } private canMoveAccount(account: ChartOfAccounts, newParent: ChartOfAccounts | null): boolean { // Não pode mover para um tipo de conta diferente if (newParent && account.account_type !== newParent.account_type) { return false; } // Outras validações específicas do plano de contas return true; } getAccountTypeLabel(accountType: AccountType): string { const type = this.accountTypes.find(t => t.value === accountType); return type?.label || accountType; } private getDefaultBalanceType(accountType: AccountType): BalanceType { switch (accountType) { case AccountType.ASSET: case AccountType.EXPENSE: return BalanceType.DEBIT; case AccountType.LIABILITY: case AccountType.EQUITY: case AccountType.REVENUE: return BalanceType.CREDIT; default: return BalanceType.DEBIT; } } private openAccountModal(account: Partial) { // Implementar modal de criação/edição console.log('Abrir modal para:', account); } viewTransactions(account: ChartOfAccounts) { // Navegar para tela de lançamentos da conta console.log('Ver lançamentos da conta:', account.code); } viewBalance(account: ChartOfAccounts) { // Abrir relatório de balancete da conta console.log('Ver balancete da conta:', account.code); } } ``` #### **Estilos Específicos para Plano de Contas** ```scss // chart-of-accounts.component.scss .chart-of-accounts-container { height: 100%; display: flex; flex-direction: column; } .account-type-filters { display: flex; gap: 0.5rem; padding: 1rem; border-bottom: 1px solid var(--divider); flex-wrap: wrap; .filter-btn { padding: 0.5rem 1rem; border: 1px solid var(--divider); background: var(--surface); color: var(--text-primary); border-radius: 6px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 0.5rem; &:hover { background: var(--surface-hover); } &.active { background: var(--primary); color: white; border-color: var(--primary); } i { font-size: 0.875rem; } } } .account-details { .detail-row { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 1px solid var(--divider-light); label { font-weight: 600; color: var(--text-secondary); font-size: 0.875rem; } .account-code { font-family: 'Courier New', monospace; font-weight: bold; color: var(--primary); } .balance { font-weight: bold; color: var(--success); &.negative { color: var(--error); } } } .quick-actions { margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap; } } // Badges específicos para tipos de conta .badge-asset { background: #3b82f6; color: white; } .badge-liability { background: #ef4444; color: white; } .badge-equity { background: #8b5cf6; color: white; } .badge-revenue { background: #10b981; color: white; } .badge-expense { background: #f59e0b; color: white; } .badge-debit { background: #6b7280; color: white; } .badge-credit { background: #374151; color: white; } ``` #### **Estrutura Típica de Plano de Contas** ```typescript // Exemplo de estrutura hierárquica padrão const sampleChartOfAccounts: ChartOfAccounts[] = [ { id: '1', name: 'ATIVO', code: '1', parent_id: null, account_type: AccountType.ASSET, balance_type: BalanceType.DEBIT, accepts_entries: false, is_synthetic: true, children: [ { id: '1.1', name: 'ATIVO CIRCULANTE', code: '1.1', parent_id: '1', account_type: AccountType.ASSET, balance_type: BalanceType.DEBIT, accepts_entries: false, is_synthetic: true, children: [ { id: '1.1.01', name: 'DISPONÍVEL', code: '1.1.01', parent_id: '1.1', account_type: AccountType.ASSET, balance_type: BalanceType.DEBIT, accepts_entries: false, is_synthetic: true, children: [ { id: '1.1.01.001', name: 'Caixa', code: '1.1.01.001', parent_id: '1.1.01', account_type: AccountType.ASSET, balance_type: BalanceType.DEBIT, accepts_entries: true, is_synthetic: false, current_balance: 5000.00 }, { id: '1.1.01.002', name: 'Banco Conta Movimento', code: '1.1.01.002', parent_id: '1.1.01', account_type: AccountType.ASSET, balance_type: BalanceType.DEBIT, accepts_entries: true, is_synthetic: false, current_balance: 25000.00 } ] } ] } ] }, { id: '2', name: 'PASSIVO', code: '2', parent_id: null, account_type: AccountType.LIABILITY, balance_type: BalanceType.CREDIT, accepts_entries: false, is_synthetic: true, children: [ // ... estrutura do passivo ] } // ... outras contas principais ]; ``` ### **2. Estrutura Organizacional** ```typescript export interface Department extends HierarchicalEntity { code: string; manager_id?: string; cost_center: string; budget: number; employee_count: number; department_type: 'OPERATIONAL' | 'ADMINISTRATIVE' | 'STRATEGIC'; } ``` ### **3. Categoria de Produtos** ```typescript export interface ProductCategory extends HierarchicalEntity { code: string; description?: string; active: boolean; tax_rate?: number; commission_rate?: number; product_count: number; } ``` ## 🎯 Funcionalidades Avançadas ### **1. Drag & Drop com Validação** ```typescript // Validações específicas por domínio canMoveTo(item: T, newParent: T | null): boolean { // Regras específicas do negócio if (item.account_type === 'REVENUE' && newParent?.account_type === 'EXPENSE') { return false; // Não pode mover receita para despesa } return super.canMoveTo(this.hierarchicalData, item.id, newParent?.id); } ``` ### **2. Busca Inteligente** ```typescript // Busca por código, nome ou caminho completo searchItems(term: string): T[] { return this.flattenHierarchy(this.hierarchicalData).filter(item => item.name.toLowerCase().includes(term.toLowerCase()) || item.code?.toLowerCase().includes(term.toLowerCase()) || this.buildPath(this.hierarchicalData, item.id).includes(term) ); } ``` ## 🚀 Próximos Passos ### **Fase 1: Implementação Base** 1. ✅ Criar interfaces e services base 2. ✅ Implementar componentes de árvore 3. ✅ Adicionar drag & drop básico 4. ✅ Integrar com sistema de formulários ### **Fase 2: Funcionalidades Avançadas** 1. 🔄 Busca e filtros avançados 2. 🔄 Validações de negócio 3. 🔄 Importação/exportação 4. 🔄 Histórico de mudanças ### **Fase 3: Otimizações** 1. ⏳ Lazy loading para hierarquias grandes 2. ⏳ Virtualização de lista 3. ⏳ Cache inteligente 4. ⏳ Performance para milhares de itens --- ## 🔗 Links Relacionados - **Base Domain Component**: [base-domain.component.ts](../src/app/shared/components/base-domain/base-domain.component.ts) - **Tab System**: [tab-system/README.md](../src/app/shared/components/tab-system/README.md) - **API Integration**: [API_INTEGRATION_GUIDE.md](./API_INTEGRATION_GUIDE.md) --- **💡 Esta documentação serve como base para implementação de todas as telas hierárquicas do ERP PraFrota, mantendo consistência e reutilização de código.** 🌳