vendaweb-api/docs/components.md

15 KiB

Componentes - DRE Gerencial

Visão Geral

O sistema DRE Gerencial é construído com componentes React funcionais em TypeScript, seguindo padrões modernos de desenvolvimento frontend.

Estrutura de Componentes

1. Componente Principal (src/app/DRE/teste.tsx)

Responsabilidades

  • Orquestração da interface DRE hierárquica
  • Gerenciamento de estado de expansão/colapso
  • Controle de ordenação e filtros
  • Integração com componente analítico

Estados Principais

const [data, setData] = useState<DREItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const [expandedSubgrupos, setExpandedSubgrupos] = useState<Set<string>>(new Set());
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(new Set());
const [sortConfig, setSortConfig] = useState<SortConfig>({
  field: 'descricao',
  direction: 'asc',
});
const [analiticoFiltros, setAnaliticoFiltros] = useState({
  dataInicio: '',
  dataFim: '',
  centroCusto: '',
  codigoGrupo: '',
  codigoSubgrupo: '',
  codigoConta: '',
});

Funções Principais

fetchData()
const fetchData = async () => {
  try {
    setLoading(true);
    setError(null);
    const response = await fetch('/api/dre');
    
    if (!response.ok) {
      throw new Error(`Erro ao carregar dados: ${response.status}`);
    }
    
    const result = await response.json();
    setData(result);
    
    // Extrair meses únicos dos dados
    const meses = [...new Set(
      result.map((item: DREItem) => {
        const dataCompetencia = new Date(item.data_competencia);
        return `${dataCompetencia.getFullYear()}-${String(
          dataCompetencia.getMonth() + 1
        ).padStart(2, '0')}`;
      })
    )].sort() as string[];
    
    setMesesDisponiveis(meses);
  } catch (err) {
    setError(err instanceof Error ? err.message : 'Erro desconhecido');
  } finally {
    setLoading(false);
  }
};
buildHierarchicalData()
const buildHierarchicalData = (): HierarchicalRow[] => {
  const rows: HierarchicalRow[] = [];
  
  // Agrupar por grupo, tratando grupo 05 como subgrupo do grupo 04
  const grupos = data.reduce((acc, item) => {
    if (item.grupo.includes('05')) {
      // Lógica especial para grupo 05
      const grupo04Key = Object.keys(acc).find((key) => key.includes('04'));
      if (grupo04Key) {
        acc[grupo04Key].push(item);
      } else {
        const grupo04Nome = '04 - GRUPO 04';
        if (!acc[grupo04Nome]) {
          acc[grupo04Nome] = [];
        }
        acc[grupo04Nome].push(item);
      }
    } else {
      if (!acc[item.grupo]) {
        acc[item.grupo] = [];
      }
      acc[item.grupo].push(item);
    }
    return acc;
  }, {} as Record<string, DREItem[]>);
  
  // Construir hierarquia completa
  // ... lógica de construção hierárquica
  
  return rows;
};
handleRowClick()
const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => {
  if (!data.length) return;
  
  // Calcular período baseado nos dados
  const datas = data.map((item) => item.data_competencia);
  const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
  const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
  
  const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7);
  const dataFimStr = new Date(dataFim).toISOString().substring(0, 7);
  
  const { codigoGrupo, codigoSubgrupo } = extractCodes(
    row.grupo || '',
    row.subgrupo
  );
  
  // Criar identificador único para a linha
  const linhaId = `${row.type}-${row.grupo || ''}-${row.subgrupo || ''}-${
    row.centro_custo || ''
  }-${row.codigo_conta || ''}`;
  setLinhaSelecionada(linhaId);
  
  // Configurar filtros para análise analítica
  const dataInicioFiltro = mesSelecionado || dataInicioStr;
  const dataFimFiltro = mesSelecionado || dataFimStr;
  
  setAnaliticoFiltros({
    dataInicio: dataInicioFiltro,
    dataFim: dataFimFiltro,
    centroCusto: row.centro_custo || '',
    codigoGrupo,
    codigoSubgrupo,
    codigoConta: row.codigo_conta?.toString() || '',
  });
};

Renderização

return (
  <div className="w-full flex flex-col items-center gap-2">
    <div className="mb-1">
      <h1 className="text-lg font-bold">DRE Gerencial</h1>
    </div>
    
    {/* Tabela hierárquica */}
    <div className="w-[95%] max-h-[400px] overflow-y-auto border rounded-md relative">
      {/* Header fixo */}
      <div className="sticky top-0 z-30 border-b shadow-sm">
        {/* ... header content */}
      </div>
      
      {/* Dados hierárquicos */}
      <div className="flex flex-col">
        {hierarchicalData.map((row, index) => (
          <div key={index} className={`flex ${getRowStyle(row)}`}>
            {/* ... row content */}
          </div>
        ))}
      </div>
    </div>
    
    {/* Componente Analítico */}
    {!loading && data.length > 0 && (
      <AnaliticoComponent filtros={analiticoFiltros} />
    )}
  </div>
);

2. Componente Analítico (src/app/DRE/analitico.tsx)

Responsabilidades

  • Visualização detalhada de transações
  • Ordenação de dados analíticos
  • Exportação para Excel
  • Aplicação de filtros dinâmicos

Props

interface AnaliticoProps {
  filtros: {
    dataInicio: string;
    dataFim: string;
    centroCusto?: string;
    codigoGrupo?: string;
    codigoSubgrupo?: string;
    codigoConta?: string;
  };
}

Estados

const [data, setData] = useState<AnaliticoItem[]>([]);
const [loading, setLoading] = useState(false);
const [sortConfig, setSortConfig] = useState<SortConfig>({
  field: 'data_competencia',
  direction: 'desc',
});

Funções Principais

fetchData()
const fetchData = useCallback(async () => {
  if (!filtros.dataInicio || !filtros.dataFim) {
    setData([]);
    return;
  }
  
  setLoading(true);
  try {
    const params = new URLSearchParams({
      dataInicio: filtros.dataInicio,
      dataFim: filtros.dataFim,
      ...(filtros.centroCusto && { centroCusto: filtros.centroCusto }),
      ...(filtros.codigoGrupo && { codigoGrupo: filtros.codigoGrupo }),
      ...(filtros.codigoSubgrupo && { codigoSubgrupo: filtros.codigoSubgrupo }),
      ...(filtros.codigoConta && { codigoConta: filtros.codigoConta }),
    });
    
    const response = await fetch(`/api/analitico?${params}`);
    if (response.ok) {
      const result = await response.json();
      setData(result as AnaliticoItem[]);
    } else {
      console.error('Erro ao buscar dados:', await response.text());
    }
  } catch (error) {
    console.error('Erro ao buscar dados:', error);
  } finally {
    setLoading(false);
  }
}, [filtros]);
exportToExcel()
const exportToExcel = () => {
  if (data.length === 0) return;
  
  // Preparar dados para exportação
  const exportData = data.map((item) => ({
    'Data Competência': new Date(item.data_competencia).toLocaleDateString('pt-BR'),
    'Data Vencimento': new Date(item.data_vencimento).toLocaleDateString('pt-BR'),
    'Data Caixa': new Date(item.data_caixa).toLocaleDateString('pt-BR'),
    'Código Fornecedor': item.codigo_fornecedor,
    Fornecedor: item.nome_fornecedor,
    'Código Centro Custo': item.codigo_centrocusto,
    'Centro Custo': item.codigo_centrocusto,
    'Código Conta': item.codigo_conta,
    Conta: item.conta,
    Valor: typeof item.valor === 'string' ? parseFloat(item.valor) : item.valor,
    Histórico: item.historico,
    'Histórico 2': item.historico2,
    Recnum: item.recnum,
  }));
  
  // Criar workbook
  const wb = XLSX.utils.book_new();
  const ws = XLSX.utils.json_to_sheet(exportData);
  
  // Adicionar resumo na segunda aba
  const resumoData = [
    { Métrica: 'Total de Registros', Valor: data.length },
    { Métrica: 'Valor Total', Valor: totalValor },
  ];
  const wsResumo = XLSX.utils.json_to_sheet(resumoData);
  
  // Adicionar abas ao workbook
  XLSX.utils.book_append_sheet(wb, ws, 'Dados Analíticos');
  XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
  
  // Gerar nome do arquivo com timestamp
  const now = new Date();
  const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
  const fileName = `analitico_${timestamp}.xlsx`;
  
  // Fazer download
  XLSX.writeFile(wb, fileName);
};

3. Componentes UI (src/components/ui/)

Button Component

// src/components/ui/button.tsx
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : 'button';
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);
Button.displayName = 'Button';

export { Button, buttonVariants };

Padrões de Design

1. Composition Pattern

  • Componentes pequenos e focados
  • Props tipadas com TypeScript
  • Reutilização através de composition

2. State Management

  • Estados locais com useState
  • Callbacks com useCallback para performance
  • Effects com useEffect para side effects

3. Styling

  • Tailwind CSS para styling
  • Class variance authority para variantes
  • Responsive design mobile-first

4. Type Safety

  • Interfaces TypeScript para props
  • Tipos específicos para dados
  • Validação de tipos em runtime

Utilitários

1. Formatação

const formatCurrency = (value: number) => {
  return new Intl.NumberFormat('pt-BR', {
    style: 'currency',
    currency: 'BRL',
  }).format(value);
};

const formatCurrencyWithColor = (value: number) => {
  const formatted = formatCurrency(value);
  const isNegative = value < 0;
  return { formatted, isNegative };
};

const formatDate = (dateString: string) => {
  return new Date(dateString).toLocaleDateString('pt-BR');
};

2. Extração de Códigos

const extractCodes = (grupo: string, subgrupo?: string) => {
  const grupoMatch = grupo.match(/^(\d+)/);
  const codigoGrupo = grupoMatch ? grupoMatch[1] : '';
  
  let codigoSubgrupo = '';
  if (subgrupo) {
    const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
    if (subgrupoMatch) {
      codigoSubgrupo = subgrupoMatch[1];
    } else {
      codigoSubgrupo = subgrupo;
    }
  }
  
  return { codigoGrupo, codigoSubgrupo };
};

3. Cálculos

const calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
  const valoresPorMes: Record<string, number> = {};
  
  items.forEach((item) => {
    const dataCompetencia = new Date(item.data_competencia);
    const anoMes = `${dataCompetencia.getFullYear()}-${String(
      dataCompetencia.getMonth() + 1
    ).padStart(2, '0')}`;
    
    if (!valoresPorMes[anoMes]) {
      valoresPorMes[anoMes] = 0;
    }
    valoresPorMes[anoMes] += parseFloat(item.valor);
  });
  
  return valoresPorMes;
};

const calcularPercentuaisPorMes = (
  valoresPorMes: Record<string, number>,
  grupo: string
): Record<string, number> => {
  const percentuais: Record<string, number> = {};
  
  // Se for o grupo 03, retorna 100% para todos os meses
  if (grupo.includes('03')) {
    Object.keys(valoresPorMes).forEach((mes) => {
      percentuais[mes] = 100;
    });
    return percentuais;
  }
  
  // Para outros grupos, calcular percentual baseado no grupo 03
  Object.keys(valoresPorMes).forEach((mes) => {
    const valorAtual = valoresPorMes[mes];
    
    // Encontrar o valor do grupo 03 para o mesmo mês
    const grupo03Items = data.filter((item) => {
      const dataCompetencia = new Date(item.data_competencia);
      const anoMes = `${dataCompetencia.getFullYear()}-${String(
        dataCompetencia.getMonth() + 1
      ).padStart(2, '0')}`;
      return anoMes === mes && item.grupo.includes('03');
    });
    
    const valorGrupo03 = grupo03Items.reduce(
      (sum, item) => sum + parseFloat(item.valor),
      0
    );
    
    if (valorGrupo03 !== 0) {
      percentuais[mes] = (valorAtual / valorGrupo03) * 100;
    } else {
      percentuais[mes] = 0;
    }
  });
  
  return percentuais;
};

Performance

1. Otimizações Implementadas

  • useCallback para funções de fetch
  • useMemo para cálculos pesados (potencial)
  • Renderização condicional

2. Estratégias de Renderização

  • Lazy loading de componentes
  • Virtualização para listas grandes (potencial)
  • Debounce para filtros (potencial)

Testes

1. Testes Unitários

// Exemplo de teste para componente
import { render, screen } from '@testing-library/react';
import Teste from './teste';

describe('Teste Component', () => {
  it('renders DRE title', () => {
    render(<Teste />);
    expect(screen.getByText('DRE Gerencial')).toBeInTheDocument();
  });
});

2. Testes de Integração

// Teste de interação com API
describe('DRE Integration', () => {
  it('loads data from API', async () => {
    render(<Teste />);
    
    await waitFor(() => {
      expect(screen.getByText('Carregando dados...')).toBeInTheDocument();
    });
    
    // Verificar se dados foram carregados
  });
});

Próximos Passos

  1. Implementar Context API para estado global
  2. Adicionar React Query para cache de dados
  3. Implementar Error Boundaries robustos
  4. Adicionar testes unitários e de integração
  5. Implementar lazy loading de componentes
  6. Adicionar acessibilidade (ARIA labels)
  7. Implementar temas dark/light
  8. Adicionar animações e transições