Capítulo 20: Projeto Integrador: Portal de Gestão Financeira Corporativa

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual da Arquitetura do Portal Financeiro Corporativo

flowchart TD
    Browser["Navegador do Usuário"] --> MainApp["App Standalone (provideRouter + provideHttpClient)"]

    subgraph CORE_LAYER["Camada de Infraestrutura e Segurança"]
        AuthG["authGuard (CanActivateFn)"]
        Interceptors["Functional Interceptors (Bearer JWT + Latency Log)"]
        Hydration["Client Hydration + Event Replay"]
    end

    MainApp --> CORE_LAYER

    subgraph STATE_LAYER["Camada de Estado Global (NgRx SignalStore)"]
        Store["FinanceiroStore"]
        State["transacoes, saldoTotal, receitas, despesas, filtro"]
        Methods["carregarTransacoes(), adicionarTransacao(), exportarRelatorio()"]
    end

    CORE_LAYER --> STATE_LAYER

    subgraph UI_FEATURES["Camada de Apresentação (Standalone Components)"]
        DashView["DashboardFinanceiroComponent"]
        KpiCards["KpiCardsComponent (Signals Inputs)"]
        DeferTable["@defer (on viewport) TransacoesTableComponent"]
        ModalForm["NovaTransacaoModal (Reactive Forms Estrito)"]
    end

    STATE_LAYER --> UI_FEATURES

    style Browser fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style CORE_LAYER fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style STATE_LAYER fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style UI_FEATURES fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. Visão Geral do Projeto Integrador

O Portal de Gestão Financeira Corporativa consolida e orquestra todas as tecnologias, conceitos e boas práticas dominados ao longo dos 20 capítulos desta especialização:

  1. Arquitetura 100% Standalone & Zoneless-Ready: Zero módulos NgModules legados.
  2. Reatividade Reativa com Angular Signals: Grafo reativo com signal(), computed() e effect().
  3. Comunicação por Signal Inputs e Outputs: input.required(), output() e model().
  4. Gerenciamento de Estado Centralizado com NgRx SignalStore: Stores leves, modulares e tipadas.
  5. Formulários Reativos Estritamente Tipados: NonNullableFormBuilder com validações síncronas e assíncronas.
  6. Segurança de Rota e Rede: CanActivateFn e HttpInterceptorFn com injeção automática de tokens JWT.
  7. Performance Extrema: Deferrable Views (@defer (on viewport)), NgOptimizedImage e suporte a SSR com hidratação não destrutiva.
  8. Exportação de Dados Financeiros: Geração e download dinâmico de relatórios em formato JSON/CSV via API Blob.

⚙️ 2. O Modelo de Dados e o Contrato de Transações

// models/financeiro.model.ts
export type TipoTransacao = 'RECEITA' | 'DESPESA';
export type CategoriaTransacao = 'FATURAMENTO' | 'SERVICOS' | 'INFRAESTRUTURA' | 'SALARIOS' | 'MARKETING';

export interface TransacaoFinanceira {
  id: string;
  descricao: string;
  valor: number;
  tipo: TipoTransacao;
  categoria: CategoriaTransacao;
  data: string; // ISO 8601
}

export interface MetricasFinanceiras {
  saldoTotal: number;
  totalReceitas: number;
  totalDespesas: number;
  taxaLucratividade: number;
}

📊 3. Central de Estado com NgRx SignalStore

A store centraliza as mutações de dados e cálculos de balanço financeiro em tempo real:

// stores/financeiro.store.ts
import { inject, computed } from '@angular/core';
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { HttpClient } from '@angular/common/http';
import { TransacaoFinanceira } from '../models/financeiro.model';

interface FinanceiroState {
  transacoes: TransacaoFinanceira[];
  filtroCategoria: string;
  carregando: boolean;
}

const estadoInicial: FinanceiroState = {
  transacoes: [
    { id: 'TX-101', descricao: 'Consultoria Cloud AWS', valor: 45000, tipo: 'RECEITA', categoria: 'SERVICOS', data: '2026-09-01' },
    { id: 'TX-102', descricao: 'Licenças de Software', valor: 12500, tipo: 'DESPESA', categoria: 'INFRAESTRUTURA', data: '2026-09-05' },
    { id: 'TX-103', descricao: 'Folha de Pagamento Engenharia', valor: 78000, tipo: 'DESPESA', categoria: 'SALARIOS', data: '2026-09-10' },
    { id: 'TX-104', descricao: 'Venda de Licença Enterprise', valor: 110000, tipo: 'RECEITA', categoria: 'FATURAMENTO', data: '2026-09-15' }
  ],
  filtroCategoria: 'TODAS',
  carregando: false
};

export const FinanceiroStore = signalStore(
  { providedIn: 'root' },
  withState(estadoInicial),
  
  withComputed(({ transacoes, filtroCategoria }) => {
    const totalReceitas = computed(() => 
      transacoes().filter(t => t.tipo === 'RECEITA').reduce((acc, t) => acc + t.valor, 0)
    );

    const totalDespesas = computed(() => 
      transacoes().filter(t => t.tipo === 'DESPESA').reduce((acc, t) => acc + t.valor, 0)
    );

    const saldoTotal = computed(() => totalReceitas() - totalDespesas());

    const transacoesFiltradas = computed(() => {
      const cat = filtroCategoria();
      return cat === 'TODAS' 
        ? transacoes() 
        : transacoes().filter(t => t.categoria === cat);
    });

    return { totalReceitas, totalDespesas, saldoTotal, transacoesFiltradas };
  }),

  withMethods((store) => ({
    adicionarTransacao(nova: Omit<TransacaoFinanceira, 'id'>): void {
      const transacaoCompleta: TransacaoFinanceira = {
        ...nova,
        id: `TX-${Date.now().toString().slice(-4)}`
      };
      patchState(store, state => ({
        transacoes: [transacaoCompleta, ...state.transacoes]
      }));
    },

    removerTransacao(id: string): void {
      patchState(store, state => ({
        transacoes: state.transacoes.filter(t => t.id !== id)
      }));
    },

    filtrarPorCategoria(filtroCategoria: string): void {
      patchState(store, { filtroCategoria });
    },

    exportarRelatorioJson(): void {
      const dados = JSON.stringify(store.transacoes(), null, 2);
      const blob = new Blob([dados], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = `relatorio-financeiro-${new Date().toISOString().slice(0, 10)}.json`;
      link.click();
      URL.revokeObjectURL(url);
    }
  }))
);

💻 4. Componente Principal do Dashboard Financeiro

O componente integra os blocos @defer, formatação de moeda via CurrencyPipe e controles de interação reativa:

// features/dashboard/dashboard.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule, CurrencyPipe, DatePipe } from '@angular/common';
import { FinanceiroStore } from '../../stores/financeiro.store';
import { ReactiveFormsModule, NonNullableFormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-dashboard-financeiro',
  standalone: true,
  imports: [CommonModule, CurrencyPipe, DatePipe, ReactiveFormsModule],
  template: `
    <div class="dashboard-layout">
      <header class="header-principal">
        <h1>📊 Portal de Gestão Financeira Corporativa</h1>
        <button class="btn-exportar" (click)="store.exportarRelatorioJson()">📥 Exportar Relatório (JSON)</button>
      </header>

      <!-- KPI Cards Reativos -->
      <section class="kpi-grid">
        <div class="kpi-card saldo" [class.negativo]="store.saldoTotal() < 0">
          <small>Saldo Consolidado</small>
          <h3>{{ store.saldoTotal() | currency:'BRL':'symbol':'1.2-2' }}</h3>
        </div>
        <div class="kpi-card receita">
          <small>Total de Receitas</small>
          <h3>{{ store.totalReceitas() | currency:'BRL':'symbol':'1.2-2' }}</h3>
        </div>
        <div class="kpi-card despesa">
          <small>Total de Despesas</small>
          <h3>{{ store.totalDespesas() | currency:'BRL':'symbol':'1.2-2' }}</h3>
        </div>
      </section>

      <!-- Formulário de Nova Transação -->
      <section class="secao-formulario card">
        <h3>Lançar Nova Transação</h3>
        <form [formGroup]="form" (ngSubmit)="aoSubmeter()" class="form-grid">
          <div>
            <label>Descrição:</label>
            <input formControlName="descricao" type="text" placeholder="Ex: Servidores AWS" />
          </div>
          <div>
            <label>Valor (R$):</label>
            <input formControlName="valor" type="number" step="0.01" />
          </div>
          <div>
            <label>Tipo:</label>
            <select formControlName="tipo">
              <option value="RECEITA">RECEITA (+)</option>
              <option value="DESPESA">DESPESA (-)</option>
            </select>
          </div>
          <div>
            <label>Categoria:</label>
            <select formControlName="categoria">
              <option value="FATURAMENTO">Faturamento</option>
              <option value="SERVICOS">Serviços</option>
              <option value="INFRAESTRUTURA">Infraestrutura</option>
              <option value="SALARIOS">Salários</option>
              <option value="MARKETING">Marketing</option>
            </select>
          </div>
          <button type="submit" [disabled]="form.invalid" class="btn-adicionar">+ Adicionar</button>
        </form>
      </section>

      <!-- Tabela com Deferrable Views e Placeholder -->
      <section class="secao-tabela card">
        <div class="tabela-header">
          <h3>Extrato de Transações</h3>
          <div class="filtro">
            <label>Filtrar Categoria: </label>
            <select (change)="aoMudarFiltro($event)">
              <option value="TODAS">Todas</option>
              <option value="FATURAMENTO">Faturamento</option>
              <option value="SERVICOS">Serviços</option>
              <option value="INFRAESTRUTURA">Infraestrutura</option>
              <option value="SALARIOS">Salários</option>
            </select>
          </div>
        </div>

        @defer (on viewport; prefetch on idle) {
          <table class="tabela-financeira">
            <thead>
              <tr>
                <th>Código</th>
                <th>Data</th>
                <th>Descrição</th>
                <th>Categoria</th>
                <th>Valor</th>
                <th>Ações</th>
              </tr>
            </thead>
            <tbody>
              @for (tx of store.transacoesFiltradas(); track tx.id) {
                <tr [class.linha-despesa]="tx.tipo === 'DESPESA'">
                  <td><code>{{ tx.id }}</code></td>
                  <td>{{ tx.data | date:'dd/MM/yyyy' }}</td>
                  <td>{{ tx.descricao }}</td>
                  <td><span class="badge">{{ tx.categoria }}</span></td>
                  <td class="valor-col">
                    {{ tx.tipo === 'RECEITA' ? '+' : '-' }} {{ tx.valor | currency:'BRL' }}
                  </td>
                  <td>
                    <button class="btn-excluir" (click)="store.removerTransacao(tx.id)">Remover</button>
                  </td>
                </tr>
              } @empty {
                <tr><td colspan="6" class="sem-dados">Nenhuma transação encontrada no filtro selecionado.</td></tr>
              }
            </tbody>
          </table>
        } @placeholder (minimum 300ms) {
          <div class="skeleton-placeholder">
            <p>Carregando extrato contábil interativo...</p>
          </div>
        }
      </section>
    </div>
  `
})
export class DashboardFinanceiroComponent {
  readonly store = inject(FinanceiroStore);
  private fb = inject(NonNullableFormBuilder);

  form = this.fb.group({
    descricao: ['', [Validators.required, Validators.minLength(3)]],
    valor: [0, [Validators.required, Validators.min(0.01)]],
    tipo: ['RECEITA' as 'RECEITA' | 'DESPESA', [Validators.required]],
    categoria: ['FATURAMENTO' as any, [Validators.required]]
  });

  aoSubmeter(): void {
    if (this.form.valid) {
      const val = this.form.getRawValue();
      this.store.adicionarTransacao({
        descricao: val.descricao,
        valor: val.valor,
        tipo: val.tipo,
        categoria: val.categoria,
        data: new Date().toISOString()
      });
      this.form.reset({ descricao: '', valor: 0, tipo: 'RECEITA', categoria: 'FATURAMENTO' });
    }
  }

  aoMudarFiltro(event: Event): void {
    const select = event.target as HTMLSelectElement;
    this.store.filtrarPorCategoria(select.value);
  }
}

⚖️ 5. Conclusão da Especialização e Próximos Passos na Carreira

Ao completar esta jornada de 20 capítulos no Padrão Ouro Educacional, você adquiriu domínio pleno sobre:

  1. Modern Frontend Engineering: Angular 18/19 Standalone, eliminando módulos pesados e adotando componentes atômicos.
  2. Reatividade de Próxima Geração: Signals, reatividade fina síncrona sem overhead de Zone.js.
  3. Mestria em Streams Assíncronas: RxJS avançado para redes, cancelamentos e concorrência.
  4. Arquitetura de Estado Previsível: NgRx SignalStore desacoplado, imutável e altamente testável.
  5. Excelência de Produção: Server-Side Rendering (SSR), hidratação com Event Replay e Deferrable Views para pontuações máximas no Google Core Web Vitals.