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.
241 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|