Files
CurrenciCombo/src/pages/ReportingPage.tsx
Nakamoto, S 7253ad1974 feat(portal): wire Accounts/Treasury/Reporting/Compliance/Settlements/TransactionBuilder to live Chain-138 + SolaceScan
Extends the POC from #2 beyond the Dashboard so every portal page that
can benefit from on-chain signal now pulls from live backends while
preserving its existing UX. Pages without an on-chain analogue (the
IFRS/GAAP/IPSAS report rows, the dbis_core compliance alerts) stay on
sample data with an explicit 'mocked' note.

New shared primitives
---------------------
src/hooks/useLatestTransactions.ts   — polls SolaceScan /transactions every 15s
src/hooks/useAddressTransactions.ts  — per-address tx feed, 60s polling
src/components/portal/LiveTransactionsPanel.tsx  — reusable live-tx card
src/components/portal/LiveChainBanner.tsx        — slim status banner
src/components/portal/OnChainBalanceTag.tsx      — shared live/off-chain pill

Per-page wiring
---------------
AccountsPage          — on-chain pill + META balance + SolaceScan link on
                        each account row that carries a walletAddress;
                        overlay renders only on wallet rows (negative check).
SettlementsPage       — replaces the static 'Settlement Rate' tile with a
                        live Chain-138 block + tx-today tile; adds a
                        LiveTransactionsPanel above the CSD queue so the
                        page no longer renders identical output when RPC
                        is dead.
ReportingPage         — new On-Chain Reporting Snapshot row (Blockscout
                        /stats: block depth, total tx, total addrs,
                        utilisation, avg block time). Clear note that
                        the IFRS/GAAP/IPSAS rows come from dbis_core and
                        are still mocked.
TreasuryPage          — two new summary tiles: live Chain-138 gas +
                        aggregated on-chain custody (META) from sample
                        wallet addresses. Uses the same
                        useOnChainBalances hook as Accounts.
CompliancePage        — AML monitor strip with wallet selector; dedicated
                        'On-Chain Tx Feed' card shows IN/OUT per tracked
                        wallet via SolaceScan. dbis_core alerts still
                        mocked (no public deploy).
TransactionBuilder    — LiveChainBanner inserted above the composer so
                        users know chain health + gas + latency before
                        composing; transaction-builder-module made a
                        flex column so the banner doesn't cover the
                        canvas.

Assertions baked into every live widget
---------------------------------------
- RPC failure flips colour + text to 'degraded'/'—' (no silent freeze).
- Loading state is distinct from both live and degraded.
- Each overlay is only rendered where real data differs from sample data
  (walletAddress rows for balances, tracked custody for AML, etc.) so a
  page without live overlays is proof-of-scope, not proof-of-brokenness.

Verified locally
----------------
- tsc --noEmit: clean
- npm run build: clean (2066 modules, 565 ms)

Still intentionally mocked
--------------------------
- proxmox.ts — CF-Access protected; a BFF route is now open in
  orchestrator PR (see companion PR for /api/proxmox/*).
- dbisCore.ts — no public deployment exists yet.
2026-04-19 08:31:04 +00:00

241 lines
12 KiB
TypeScript

import { useState } from 'react';
import { FileText, Download, Filter, Plus, Eye, Clock, CheckCircle2, AlertTriangle, Send, Database } from 'lucide-react';
import { reportConfigs } from '../data/portalData';
import type { ReportingStandard } from '../types/portal';
import { useLiveChain } from '../hooks/useLiveChain';
import { endpoints } from '../config/endpoints';
const standardColors: Record<ReportingStandard, string> = {
IPSAS: '#a855f7',
US_GAAP: '#3b82f6',
IFRS: '#22c55e',
};
const statusIcons: Record<string, typeof Clock> = {
draft: Clock,
generated: AlertTriangle,
reviewed: Eye,
published: CheckCircle2,
};
const statusColors: Record<string, string> = {
draft: '#6b7280',
generated: '#eab308',
reviewed: '#3b82f6',
published: '#22c55e',
};
export default function ReportingPage() {
const [standardFilter, setStandardFilter] = useState<string>('all');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [activeStandard, setActiveStandard] = useState<ReportingStandard>('IFRS');
const { health, stats, error: liveErr, lastUpdated: liveUpdatedAt } = useLiveChain();
const filtered = reportConfigs.filter(r => {
const matchStandard = standardFilter === 'all' || r.standard === standardFilter;
const matchType = typeFilter === 'all' || r.type === typeFilter;
return matchStandard && matchType;
});
const standardDetails: Record<ReportingStandard, { full: string; description: string; keyStatements: string[]; jurisdiction: string }> = {
IPSAS: {
full: 'International Public Sector Accounting Standards',
description: 'Accrual-based accounting standards for public sector entities, issued by the IPSASB. Ensures transparency and accountability in government financial reporting.',
keyStatements: ['Statement of Financial Position', 'Statement of Financial Performance', 'Statement of Changes in Net Assets', 'Cash Flow Statement', 'Budget Comparison Statement'],
jurisdiction: 'International (Public Sector)',
},
US_GAAP: {
full: 'United States Generally Accepted Accounting Principles',
description: 'Comprehensive accounting framework issued by FASB, mandatory for US public companies and widely adopted by financial institutions.',
keyStatements: ['Balance Sheet', 'Income Statement', 'Statement of Cash Flows', 'Statement of Stockholders\' Equity', 'Notes to Financial Statements'],
jurisdiction: 'United States',
},
IFRS: {
full: 'International Financial Reporting Standards',
description: 'Global accounting standards issued by the IASB, adopted by 140+ jurisdictions. Principle-based framework for transparent financial reporting.',
keyStatements: ['Statement of Financial Position', 'Statement of Profit or Loss', 'Statement of Comprehensive Income', 'Statement of Cash Flows', 'Statement of Changes in Equity'],
jurisdiction: 'International (140+ jurisdictions)',
},
};
const detail = standardDetails[activeStandard];
return (
<div className="reporting-page">
<div className="page-header">
<div>
<h1><FileText size={24} /> Financial Reporting</h1>
<p className="page-subtitle">IPSAS, US GAAP, and IFRS compliant reporting frameworks</p>
</div>
<div className="page-header-actions">
<button className="btn-secondary"><Download size={14} /> Export All</button>
<button className="btn-primary"><Plus size={14} /> Generate Report</button>
</div>
</div>
{/* On-Chain Reporting Snapshot — live data from Chain-138 + SolaceScan */}
<div
className="onchain-report-snapshot"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
gap: 12,
marginBottom: 16,
padding: 12,
border: '1px solid rgba(255,255,255,0.06)',
borderRadius: 8,
background: 'rgba(255,255,255,0.02)',
}}
>
<div style={{ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#cbd5e1' }}>
<Database size={14} /> On-Chain Reporting Snapshot Chain {endpoints.chain138.chainId}
</span>
<span className="small" style={{ color: liveErr ? '#ef4444' : '#6b7280' }}>
{liveErr ? `RPC degraded · ${liveErr}` : liveUpdatedAt ? `updated ${liveUpdatedAt.toLocaleTimeString()}` : 'polling…'}
</span>
</div>
<div>
<span className="summary-label" style={{ fontSize: 10 }}>Latest Block</span>
<div className="mono" style={{ fontSize: 18 }}>{liveErr ? '—' : (health?.blockNumber?.toLocaleString() ?? '…')}</div>
</div>
<div>
<span className="summary-label" style={{ fontSize: 10 }}>Total Blocks (ledger depth)</span>
<div className="mono" style={{ fontSize: 18 }}>{liveErr ? '—' : (stats?.total_blocks?.toLocaleString() ?? '…')}</div>
</div>
<div>
<span className="summary-label" style={{ fontSize: 10 }}>Total Transactions</span>
<div className="mono" style={{ fontSize: 18 }}>{liveErr ? '—' : (stats?.total_transactions?.toLocaleString() ?? '…')}</div>
</div>
<div>
<span className="summary-label" style={{ fontSize: 10 }}>Total Addresses</span>
<div className="mono" style={{ fontSize: 18 }}>{liveErr ? '—' : (stats?.total_addresses?.toLocaleString() ?? '…')}</div>
</div>
<div>
<span className="summary-label" style={{ fontSize: 10 }}>Network Utilisation</span>
<div className="mono" style={{ fontSize: 18 }}>
{liveErr ? '—' : (stats ? `${stats.network_utilization_percentage.toFixed(1)}%` : '…')}
</div>
</div>
<div>
<span className="summary-label" style={{ fontSize: 10 }}>Avg Block Time</span>
<div className="mono" style={{ fontSize: 18 }}>
{liveErr ? '—' : (stats ? `${stats.average_block_time.toFixed(1)}s` : '…')}
</div>
</div>
<div style={{ gridColumn: '1 / -1', fontSize: 10, color: '#6b7280' }}>
Sources: <a href={endpoints.chain138.rpcUrl} target="_blank" rel="noreferrer" style={{ color: '#60a5fa' }}>{endpoints.chain138.rpcUrl}</a>
{' · '}<a href={`${endpoints.explorer.apiBaseUrl}/api/v2/stats`} target="_blank" rel="noreferrer" style={{ color: '#60a5fa' }}>{endpoints.explorer.apiBaseUrl}/api/v2/stats</a>
{' · the IFRS / US GAAP / IPSAS reports below are generated by dbis_core (currently mocked — no public deployment).'}
</div>
</div>
{/* Standards Overview */}
<div className="standards-tabs">
{(['IPSAS', 'US_GAAP', 'IFRS'] as ReportingStandard[]).map(std => (
<button
key={std}
className={`standard-tab ${activeStandard === std ? 'active' : ''}`}
onClick={() => setActiveStandard(std)}
style={activeStandard === std ? { borderColor: standardColors[std], color: standardColors[std] } : {}}
>
<span className="standard-dot" style={{ background: standardColors[std] }} />
{std.replace('_', ' ')}
<span className="standard-count">{reportConfigs.filter(r => r.standard === std).length}</span>
</button>
))}
</div>
<div className="standard-detail-card" style={{ borderColor: standardColors[activeStandard] + '40' }}>
<div className="standard-detail-header">
<div>
<h3 style={{ color: standardColors[activeStandard] }}>{activeStandard.replace('_', ' ')}</h3>
<p className="standard-full-name">{detail.full}</p>
</div>
<span className="jurisdiction-badge">{detail.jurisdiction}</span>
</div>
<p className="standard-description">{detail.description}</p>
<div className="key-statements">
<span className="key-statements-label">Key Financial Statements:</span>
<div className="statements-list">
{detail.keyStatements.map(stmt => (
<span key={stmt} className="statement-badge">{stmt}</span>
))}
</div>
</div>
</div>
{/* Reports Table */}
<div className="dashboard-card reports-card">
<div className="card-header">
<h3>Generated Reports</h3>
<div className="card-header-actions">
<div className="filter-group">
<Filter size={12} />
<select value={standardFilter} onChange={e => setStandardFilter(e.target.value)}>
<option value="all">All Standards</option>
<option value="IPSAS">IPSAS</option>
<option value="US_GAAP">US GAAP</option>
<option value="IFRS">IFRS</option>
</select>
</div>
<div className="filter-group">
<select value={typeFilter} onChange={e => setTypeFilter(e.target.value)}>
<option value="all">All Types</option>
<option value="balance_sheet">Balance Sheet</option>
<option value="income_statement">Income Statement</option>
<option value="cash_flow">Cash Flow</option>
<option value="trial_balance">Trial Balance</option>
<option value="regulatory">Regulatory</option>
<option value="position_summary">Position Summary</option>
<option value="risk_exposure">Risk Exposure</option>
<option value="compliance_summary">Compliance Summary</option>
</select>
</div>
</div>
</div>
<div className="reports-table">
<div className="reports-table-header">
<span>Report Name</span>
<span>Standard</span>
<span>Type</span>
<span>Period</span>
<span>Status</span>
<span>Generated</span>
<span>By</span>
<span>Actions</span>
</div>
{filtered.map(report => {
const StatusIcon = statusIcons[report.status] || Clock;
return (
<div key={report.id} className="reports-table-row">
<span className="report-name">{report.name}</span>
<span>
<span className="standard-badge" style={{ color: standardColors[report.standard], borderColor: standardColors[report.standard] + '40' }}>
{report.standard.replace('_', ' ')}
</span>
</span>
<span className="report-type">{report.type.replace(/_/g, ' ')}</span>
<span className="report-period">{report.period}</span>
<span>
<span className="report-status" style={{ color: statusColors[report.status] }}>
<StatusIcon size={12} />
{report.status}
</span>
</span>
<span className="mono small">{report.generatedAt ? report.generatedAt.toLocaleDateString() : '—'}</span>
<span className="small">{report.generatedBy || '—'}</span>
<span className="report-actions">
<button className="row-action-btn" title="View"><Eye size={12} /></button>
<button className="row-action-btn" title="Download"><Download size={12} /></button>
<button className="row-action-btn" title="Submit"><Send size={12} /></button>
</span>
</div>
);
})}
</div>
</div>
</div>
);
}