testes/Modulos Angular/projects/idt_app/docs/HIERARCHICAL_DATA_GUIDE.md

36 KiB

🌳 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

// 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

// 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<T extends HierarchicalEntity> {
  
  /**
   * Converte lista plana em estrutura hierárquica
   */
  buildHierarchy(flatData: T[], rootParentId: any = null): T[] {
    const itemMap = new Map<any, T>();
    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

// 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: `
    <div class="hierarchical-container">
      <!-- Toolbar -->
      <div class="hierarchy-toolbar">
        <button class="btn btn-primary" (click)="onAdd(null)">
          <i class="fas fa-plus"></i> Adicionar Raiz
        </button>
        <button class="btn btn-secondary" (click)="expandAll()">
          <i class="fas fa-expand-arrows-alt"></i> Expandir Todos
        </button>
        <button class="btn btn-secondary" (click)="collapseAll()">
          <i class="fas fa-compress-arrows-alt"></i> Recolher Todos
        </button>
        <div class="search-box">
          <input 
            type="text" 
            [(ngModel)]="searchTerm" 
            (input)="onSearch()"
            placeholder="Buscar..."
            class="form-control">
        </div>
      </div>

      <!-- Árvore Hierárquica -->
      <div class="hierarchy-tree" 
           cdkDropList 
           [cdkDropListData]="hierarchicalData"
           (cdkDropListDropped)="onDrop($event)">
        
        <div *ngFor="let item of filteredData" class="tree-node-container">
          <app-tree-node 
            [item]="item"
            [level]="0"
            [allowDrag]="allowDrag"
            [allowEdit]="allowEdit"
            [allowDelete]="allowDelete"
            (nodeExpanded)="onNodeExpanded($event)"
            (nodeSelected)="onNodeSelected($event)"
            (nodeEdit)="onEdit($event)"
            (nodeDelete)="onDelete($event)"
            (nodeAdd)="onAdd($event)">
          </app-tree-node>
        </div>
      </div>

      <!-- Painel de Detalhes -->
      <div class="details-panel" *ngIf="selectedItem">
        <h3>{{ selectedItem.name }}</h3>
        <div class="details-content">
          <ng-content select="[slot=details]"></ng-content>
        </div>
      </div>
    </div>
  `,
  styleUrls: ['./hierarchical-base.component.scss']
})
export class HierarchicalBaseComponent<T extends HierarchicalEntity> {
  @Input() hierarchicalData: T[] = [];
  @Input() allowDrag: boolean = true;
  @Input() allowEdit: boolean = true;
  @Input() allowDelete: boolean = true;
  @Input() allowAdd: boolean = true;

  @Output() itemSelected = new EventEmitter<T>();
  @Output() itemAdded = new EventEmitter<T | null>(); // null = adicionar raiz
  @Output() itemEdited = new EventEmitter<T>();
  @Output() itemDeleted = new EventEmitter<T>();
  @Output() itemMoved = new EventEmitter<{item: T, newParent: T | null}>();

  selectedItem: T | null = null;
  searchTerm: string = '';
  filteredData: T[] = [];

  constructor(protected hierarchicalService: HierarchicalBaseService<T>) {}

  ngOnInit() {
    this.filteredData = [...this.hierarchicalData];
  }

  ngOnChanges() {
    this.filteredData = [...this.hierarchicalData];
  }

  onDrop(event: CdkDragDrop<T[]>) {
    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[]>): 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

// 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

// 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: `
    <app-hierarchical-base
      [hierarchicalData]="categories"
      [allowDrag]="true"
      [allowEdit]="true"
      [allowDelete]="true"
      (itemSelected)="onCategorySelected($event)"
      (itemAdded)="onCategoryAdd($event)"
      (itemEdited)="onCategoryEdit($event)"
      (itemDeleted)="onCategoryDelete($event)"
      (itemMoved)="onCategoryMoved($event)">
      
      <!-- Painel de Detalhes Customizado -->
      <div slot="details" *ngIf="selectedCategory">
        <div class="form-group">
          <label>Código:</label>
          <span>{{ selectedCategory.code }}</span>
        </div>
        <div class="form-group">
          <label>Descrição:</label>
          <p>{{ selectedCategory.description }}</p>
        </div>
        <div class="form-group">
          <label>Status:</label>
          <span class="badge" [class.badge-success]="selectedCategory.active">
            {{ selectedCategory.active ? 'Ativo' : 'Inativo' }}
          </span>
        </div>
      </div>
    </app-hierarchical-base>
  `
})
export class ProductCategoriesComponent extends HierarchicalBaseComponent<ProductCategory> {
  categories: ProductCategory[] = [];
  selectedCategory: ProductCategory | null = null;

  constructor(
    private categoriesService: ProductCategoriesService,
    hierarchicalService: HierarchicalBaseService<ProductCategory>
  ) {
    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<ProductCategory> = {
      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

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

// 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<ChartOfAccounts> {
  private readonly apiUrl = 'https://prafrota-be-bff-tenant-api.grupopra.tech/api';

  constructor(private http: HttpClient) {
    super();
  }

  getAll(): Observable<ChartOfAccounts[]> {
    return this.http.get<ChartOfAccounts[]>(`${this.apiUrl}/chart-of-accounts`);
  }

  getByType(accountType: AccountType): Observable<ChartOfAccounts[]> {
    return this.http.get<ChartOfAccounts[]>(`${this.apiUrl}/chart-of-accounts`, {
      params: { account_type: accountType }
    });
  }

  create(account: Partial<ChartOfAccounts>): Observable<ChartOfAccounts> {
    return this.http.post<ChartOfAccounts>(`${this.apiUrl}/chart-of-accounts`, account);
  }

  update(id: any, account: Partial<ChartOfAccounts>): Observable<ChartOfAccounts> {
    return this.http.put<ChartOfAccounts>(`${this.apiUrl}/chart-of-accounts/${id}`, account);
  }

  delete(id: any): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/chart-of-accounts/${id}`);
  }

  updateParent(id: any, newParentId: any): Observable<ChartOfAccounts> {
    return this.http.patch<ChartOfAccounts>(`${this.apiUrl}/chart-of-accounts/${id}`, {
      parent_id: newParentId
    });
  }

  /**
   * Gera próximo código disponível baseado no pai
   */
  generateNextCode(parentId: any): Observable<string> {
    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<boolean> {
    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<ChartOfAccounts[]> {
    return this.http.get<ChartOfAccounts[]>(`${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: `
    <div class="chart-of-accounts-container">
      <!-- Filtros por Tipo de Conta -->
      <div class="account-type-filters">
        <button 
          *ngFor="let type of accountTypes" 
          class="filter-btn"
          [class.active]="selectedAccountType === type.value"
          (click)="filterByAccountType(type.value)">
          <i [class]="type.icon"></i>
          {{ type.label }}
        </button>
        <button 
          class="filter-btn"
          [class.active]="selectedAccountType === null"
          (click)="filterByAccountType(null)">
          <i class="fas fa-list"></i>
          Todas
        </button>
      </div>

      <!-- Componente Hierárquico -->
      <app-hierarchical-base
        [hierarchicalData]="filteredAccounts"
        [allowDrag]="true"
        [allowEdit]="true"
        [allowDelete]="true"
        (itemSelected)="onAccountSelected($event)"
        (itemAdded)="onAccountAdd($event)"
        (itemEdited)="onAccountEdit($event)"
        (itemDeleted)="onAccountDelete($event)"
        (itemMoved)="onAccountMoved($event)">
        
        <!-- Painel de Detalhes Customizado -->
        <div slot="details" *ngIf="selectedAccount">
          <div class="account-details">
            <h4>{{ selectedAccount.name }}</h4>
            
            <div class="detail-row">
              <label>Código:</label>
              <span class="account-code">{{ selectedAccount.code }}</span>
            </div>
            
            <div class="detail-row">
              <label>Tipo:</label>
              <span class="badge" [class]="'badge-' + selectedAccount.account_type.toLowerCase()">
                {{ getAccountTypeLabel(selectedAccount.account_type) }}
              </span>
            </div>
            
            <div class="detail-row">
              <label>Natureza:</label>
              <span class="badge" [class]="'badge-' + selectedAccount.balance_type.toLowerCase()">
                {{ selectedAccount.balance_type === 'DEBIT' ? 'Devedora' : 'Credora' }}
              </span>
            </div>
            
            <div class="detail-row">
              <label>Saldo Atual:</label>
              <span class="balance" [class.negative]="selectedAccount.current_balance < 0">
                {{ selectedAccount.current_balance | currency:'BRL':'symbol':'1.2-2' }}
              </span>
            </div>
            
            <div class="detail-row">
              <label>Aceita Lançamentos:</label>
              <span class="badge" [class.badge-success]="selectedAccount.accepts_entries">
                {{ selectedAccount.accepts_entries ? 'Sim' : 'Não' }}
              </span>
            </div>
            
            <div class="detail-row" *ngIf="selectedAccount.description">
              <label>Descrição:</label>
              <p>{{ selectedAccount.description }}</p>
            </div>
            
            <div class="detail-row">
              <label>Status:</label>
              <span class="badge" [class.badge-success]="selectedAccount.is_active">
                {{ selectedAccount.is_active ? 'Ativa' : 'Inativa' }}
              </span>
            </div>
            
            <!-- Ações Rápidas -->
            <div class="quick-actions">
              <button class="btn btn-primary btn-sm" (click)="viewTransactions(selectedAccount)">
                <i class="fas fa-list"></i> Ver Lançamentos
              </button>
              <button class="btn btn-secondary btn-sm" (click)="viewBalance(selectedAccount)">
                <i class="fas fa-chart-line"></i> Balancete
              </button>
            </div>
          </div>
        </div>
      </app-hierarchical-base>
    </div>
  `,
  styleUrls: ['./chart-of-accounts.component.scss']
})
export class ChartOfAccountsComponent extends HierarchicalBaseComponent<ChartOfAccounts> 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<ChartOfAccounts>
  ) {
    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<ChartOfAccounts> = {
        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<ChartOfAccounts>) {
    // 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

// 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

// 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

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

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

// 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

// 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


💡 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. 🌳