💻 Exemplos: Capítulo 20: Projeto Corporativo: Portal de Gestão Financeira com Auditoria JSON
Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 20 — Projeto Corporativo Integrado: Portal Financeiro com Auditoria e Exportação JSON
Este exemplo consolida toda a arquitetura desenvolvida ao longo da especialização: Standalone Components no Angular 18/19, tipagem estrita com TypeScript, reatividade nativa com Angular Signals, cálculo de KPIs em tempo real com computed, filtragem dinâmica com @if / @for e exportação em memória de relatórios de auditoria no formato JSON via Blob API.
// src/app/pages/financial-portal.component.ts
import { Component, signal, computed } from '@angular/core';
import { CommonModule, CurrencyPipe, DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
// 1. CONTRATOS DE DADOS CORPORATIVOS
export interface FinancialTransaction {
id: string;
payee: string;
amount: number;
category: 'CLOUD_INFRA' | 'DEV_TOOLS' | 'CONSULTING' | 'LICENSES';
status: 'APPROVED' | 'PENDING' | 'REJECTED';
timestamp: string;
}
export interface FinancialKPIs {
totalApprovedAmount: number;
approvalRate: number;
activeCount: number;
}
@Component({
selector: 'app-financial-portal',
standalone: true,
imports: [CommonModule, FormsModule, CurrencyPipe, DatePipe],
template: `
<div class="max-w-5xl mx-auto p-6 space-y-6 font-sans">
<!-- Cabeçalho do Portal Executivo -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white border border-slate-200 p-6 rounded-2xl shadow-sm">
<div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-red-600 animate-pulse"></span>
<h1 class="text-2xl font-black text-slate-900 tracking-tight">
Portal Financeiro & Auditoria de Custos
</h1>
</div>
<p class="text-xs text-slate-500 mt-1">
Angular 18 Enterprise • Standalone Architecture, Signals & Auditoria JSON
</p>
</div>
<div class="flex items-center gap-3">
<button
type="button"
(click)="simulateNewTransaction()"
class="px-3.5 py-2 text-xs font-semibold bg-red-50 hover:bg-red-100 text-red-700 rounded-xl transition-colors"
>
+ Simular Lançamento
</button>
<button
type="button"
[disabled]="isExporting()"
(click)="exportAuditJson()"
class="px-4 py-2 text-xs font-bold bg-red-600 hover:bg-red-700 text-white rounded-xl transition-colors shadow-sm disabled:opacity-50 flex items-center gap-2"
>
<span>📥</span>
<span>{{ isExporting() ? 'Gerando Arquivo...' : 'Exportar Auditoria JSON' }}</span>
</button>
</div>
</div>
<!-- Grade de Indicadores Executivos (KPIs Computados) -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white border border-slate-200 p-5 rounded-2xl shadow-sm">
<span class="text-xs font-bold text-slate-400 uppercase tracking-wider">Volume Aprovado</span>
<div class="text-2xl font-black text-slate-900 mt-1">
{{ kpis().totalApprovedAmount | currency:'BRL':'symbol':'1.2-2' }}
</div>
<span class="text-xs text-emerald-600 font-semibold mt-1 inline-block">Liquidação em D+0</span>
</div>
<div class="bg-white border border-slate-200 p-5 rounded-2xl shadow-sm">
<span class="text-xs font-bold text-slate-400 uppercase tracking-wider">Taxa de Conformidade</span>
<div class="text-2xl font-black text-emerald-600 mt-1">{{ kpis().approvalRate }}%</div>
<span class="text-xs text-slate-400 mt-1 inline-block">Meta mínima exigida: 80.0%</span>
</div>
<div class="bg-white border border-slate-200 p-5 rounded-2xl shadow-sm">
<span class="text-xs font-bold text-slate-400 uppercase tracking-wider">Total de Lançamentos</span>
<div class="text-2xl font-black text-indigo-600 mt-1">{{ kpis().activeCount }}</div>
<span class="text-xs text-slate-400 mt-1 inline-block">Eventos no período fiscal</span>
</div>
</div>
<!-- Tabela de Auditoria com Filtros Dinâmicos -->
<div class="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden space-y-4 p-5">
<div class="flex flex-col sm:flex-row gap-3 justify-between items-center">
<input
type="text"
[(ngModel)]="searchFilter"
placeholder="Filtrar por favorecido ou ID..."
class="w-full sm:w-72 border px-3 py-2 text-xs rounded-xl outline-none focus:ring-2 focus:ring-red-500"
/>
<div class="flex items-center gap-2">
<span class="text-xs text-slate-500 font-medium">Status:</span>
<select
[(ngModel)]="statusFilter"
class="border px-3 py-1.5 text-xs rounded-xl outline-none bg-white font-medium text-slate-700"
>
<option value="ALL">Todos os Status</option>
<option value="APPROVED">Apenas Aprovados</option>
<option value="PENDING">Apenas Pendentes</option>
<option value="REJECTED">Apenas Rejeitados</option>
</select>
</div>
</div>
<!-- Grade de Registros -->
<div class="border rounded-xl overflow-hidden">
<table class="w-full text-left text-xs">
<thead class="bg-slate-50 text-slate-600 font-semibold border-b">
<tr>
<th class="px-4 py-3">Código</th>
<th class="px-4 py-3">Favorecido / Fornecedor</th>
<th class="px-4 py-3">Categoria</th>
<th class="px-4 py-3 text-right">Valor Líquido</th>
<th class="px-4 py-3 text-center">Status</th>
<th class="px-4 py-3 text-right">Data/Hora</th>
</tr>
</thead>
<tbody class="divide-y text-slate-700">
@for (tx of filteredTransactions(); track tx.id) {
<tr class="hover:bg-slate-50 transition-colors">
<td class="px-4 py-3 font-mono font-bold text-red-600">{{ tx.id }}</td>
<td class="px-4 py-3 font-medium text-slate-800">{{ tx.payee }}</td>
<td class="px-4 py-3 font-mono text-[10px] text-slate-500 uppercase">{{ tx.category }}</td>
<td class="px-4 py-3 font-bold text-right font-mono">
{{ tx.amount | currency:'BRL':'symbol':'1.2-2' }}
</td>
<td class="px-4 py-3 text-center">
<span
class="px-2.5 py-0.5 rounded-full font-bold text-[10px]"
[ngClass]="{
'bg-emerald-100 text-emerald-800': tx.status === 'APPROVED',
'bg-amber-100 text-amber-800': tx.status === 'PENDING',
'bg-red-100 text-red-800': tx.status === 'REJECTED'
}"
>
{{ tx.status }}
</span>
</td>
<td class="px-4 py-3 text-right font-mono text-slate-400 text-[11px]">
{{ tx.timestamp | date:'dd/MM/yyyy HH:mm' }}
</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="p-8 text-center text-slate-400 text-xs">
Nenhuma transação atende aos critérios de pesquisa selecionados.
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
`
})
export class FinancialPortalComponent {
// 1. Estado Reativo Gerenciado com Signals
transactions = signal<FinancialTransaction[]>([
{ id: 'tx-801', payee: 'Amazon Web Services Inc.', amount: 28400.0, category: 'CLOUD_INFRA', status: 'APPROVED', timestamp: '2026-09-28T09:10:00Z' },
{ id: 'tx-802', payee: 'GitHub Enterprise Suite', amount: 4800.0, category: 'DEV_TOOLS', status: 'APPROVED', timestamp: '2026-09-28T09:25:30Z' },
{ id: 'tx-803', payee: 'Consultoria Segura InfoSec', amount: 15000.0, category: 'CONSULTING', status: 'PENDING', timestamp: '2026-09-28T09:30:15Z' },
{ id: 'tx-804', payee: 'Datadog Cloud Monitoring', amount: 8200.0, category: 'CLOUD_INFRA', status: 'APPROVED', timestamp: '2026-09-28T09:42:00Z' },
{ id: 'tx-805', payee: 'Licenças JetBrains IDEs', amount: 2150.0, category: 'LICENSES', status: 'REJECTED', timestamp: '2026-09-28T10:00:00Z' }
]);
searchFilter = '';
statusFilter: 'ALL' | 'APPROVED' | 'PENDING' | 'REJECTED' = 'ALL';
isExporting = signal<boolean>(false);
// 2. Computed Signals para Indicadores Analíticos (KPIs)
kpis = computed<FinancialKPIs>(() => {
const list = this.transactions();
const approved = list.filter((t) => t.status === 'APPROVED');
const totalApproved = approved.reduce((acc, t) => acc + t.amount, 0);
const rate = list.length > 0 ? (approved.length / list.length) * 100 : 0;
return {
totalApprovedAmount: totalApproved,
approvalRate: Number(rate.toFixed(1)),
activeCount: list.length
};
});
// 3. Filtragem Reativa Multicritério
filteredTransactions = computed(() => {
const term = this.searchFilter.toLowerCase().trim();
const status = this.statusFilter;
return this.transactions().filter((tx) => {
const matchStatus = status === 'ALL' || tx.status === status;
const matchSearch = tx.payee.toLowerCase().includes(term) || tx.id.toLowerCase().includes(term);
return matchStatus && matchSearch;
});
});
simulateNewTransaction(): void {
const payees = ['Cloudflare Zero Trust', 'Confluent Cloud Kafka', 'Docker Hub Enterprise'];
const payee = payees[Math.floor(Math.random() * payees.length)];
const amount = Math.floor(Math.random() * 12000) + 1500;
const newTx: FinancialTransaction = {
id: `tx-${Math.floor(Math.random() * 900) + 810}`,
payee,
amount,
category: 'CLOUD_INFRA',
status: Math.random() > 0.25 ? 'APPROVED' : 'PENDING',
timestamp: new Date().toISOString()
};
this.transactions.update((curr) => [newTx, ...curr]);
}
// 4. Exportação de Snapshot de Auditoria em Formato JSON Padronizado
exportAuditJson(): void {
this.isExporting.set(true);
const auditPayload = {
complianceStandard: 'SOX-404-FINANCIAL-AUDIT',
generatedAt: new Date().toISOString(),
angularVersion: '18.2.0',
kpisSnapshot: this.kpis(),
recordsCount: this.filteredTransactions().length,
auditTrail: this.filteredTransactions()
};
const blob = new Blob([JSON.stringify(auditPayload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `financial_audit_snapshot_${Date.now()}.json`;
link.click();
URL.revokeObjectURL(url);
setTimeout(() => {
this.isExporting.set(false);
}, 500);
}
}
🖥️ Saída / Comportamento Esperado
Renderização Inicial:
Volume Aprovado: R$ 41.400,00 (Soma de AWS, GitHub e Datadog)
Taxa de Conformidade: 60.0% (3 aprovadas de 5 totais)
Total de Lançamentos: 5 transações
Ao Clicar em "+ Simular Lançamento":
transactions.update() insere nova transação no topo da tabela.
Volume Aprovado e Taxa de Conformidade recalculam reativamente em tempo real.
Ao Filtrar por "Apenas Pendentes":
A tabela exibe apenas o lançamento da Consultoria Segura InfoSec.
Total de linhas exibidas: 1.
Ao Clicar em "Exportar Auditoria JSON":
O navegador realiza download do arquivo 'financial_audit_snapshot_[timestamp].json'.
O payload contém cabeçalho normativo, KPIs consolidados e trilha de auditoria para fins fiscais.
🔍 Destaques da Implementação
- Padrão Ouro Standalone no Angular 18: Componente isolado, autônomo, tipado e sem necessidade de módulos externos.
- Reatividade Instantânea com Signals: Modificações nos filtros disparam reconciliação cirúrgica sem a pesadez de ChangeDetection global.
- Download Nativo em Memória (
Blob): Geração client-side que desonera a infraestrutura de backend de relatórios.