feat(CC-087): add Trust Credit Readiness (financial data completeness, MANDATORY disclaimers)
This commit is contained in:
parent
18cf60f3d3
commit
46d3836567
1 changed files with 130 additions and 0 deletions
130
src/app/dashboard/trust/credit-readiness/page.tsx
Normal file
130
src/app/dashboard/trust/credit-readiness/page.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '../../../../lib/api';
|
||||
import { useSession } from '../../../../components/session-provider';
|
||||
|
||||
interface Transaction { id: string; type: string; amount: number; currency: string | null; date: string; description: string | null; }
|
||||
interface Goal { id: string; status: string; tags: string[]; }
|
||||
interface Observation { id: string; metric: string; value: string; confidence: number | null; }
|
||||
|
||||
const INCOME_TYPES = ['income', 'venit', 'revenue', 'incasare'];
|
||||
const EXPENSE_TYPES = ['expense', 'cheltuiala', 'cheltuială', 'cost'];
|
||||
|
||||
export default function TrustCreditReadinessPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
|
||||
const { data: transactions = [] } = useQuery({ queryKey: ['cr-txn', tenantId], queryFn: () => apiFetch<Transaction[]>('/v1/transactions?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
|
||||
const { data: goals = [] } = useQuery({ queryKey: ['cr-goals', tenantId], queryFn: () => apiFetch<Goal[]>('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
|
||||
const { data: observations = [] } = useQuery({ queryKey: ['cr-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
|
||||
|
||||
const analysis = useMemo(() => {
|
||||
const income = transactions.filter((t) => INCOME_TYPES.includes(t.type.toLowerCase()));
|
||||
const expense = transactions.filter((t) => EXPENSE_TYPES.includes(t.type.toLowerCase()));
|
||||
const totalIncome = income.reduce((s, t) => s + t.amount, 0);
|
||||
const totalExpense = expense.reduce((s, t) => s + Math.abs(t.amount), 0);
|
||||
const cashflow = totalIncome - totalExpense;
|
||||
const savingsRate = totalIncome > 0 ? Math.round((cashflow / totalIncome) * 100) : 0;
|
||||
|
||||
const months = new Set(transactions.map((t) => t.date.slice(0, 7))).size;
|
||||
|
||||
const finGoals = goals.filter((g) => g.tags.some((tag) => ['financial','financiar','investitie','investment','economii','savings'].includes(tag.toLowerCase())));
|
||||
const credObs = observations.filter((o) => o.metric.toLowerCase().includes('certif') || o.metric.toLowerCase().includes('diploma'));
|
||||
const avgConf = observations.length > 0 ? observations.reduce((s, o) => s + (o.confidence ?? 1), 0) / observations.length : 0;
|
||||
|
||||
const checks = [
|
||||
{ label: 'Venituri logate', done: income.length > 0, detail: `${income.length} tranzacții`, impact: 25 },
|
||||
{ label: 'Flux de numerar pozitiv', done: cashflow > 0, detail: cashflow > 0 ? `+${cashflow.toFixed(0)}` : `${cashflow.toFixed(0)}`, impact: 20 },
|
||||
{ label: 'Rată economii > 10%', done: savingsRate >= 10, detail: `${savingsRate}%`, impact: 20 },
|
||||
{ label: 'Consistență date (3+ luni)', done: months >= 3, detail: `${months} luni`, impact: 15 },
|
||||
{ label: 'Obiective financiare', done: finGoals.length > 0, detail: `${finGoals.length} obiective`, impact: 10 },
|
||||
{ label: 'Credențiale verificabile', done: credObs.length > 0, detail: `${credObs.length} credențiale`, impact: 10 },
|
||||
];
|
||||
|
||||
const score = checks.filter((c) => c.done).reduce((s, c) => s + c.impact, 0);
|
||||
return { checks, score, totalIncome, totalExpense, cashflow, savingsRate, months };
|
||||
}, [transactions, goals, observations]);
|
||||
|
||||
function scoreCls(s: number) { return s >= 70 ? 'text-signal-ok' : s >= 40 ? 'text-warn' : 'text-signal-danger'; }
|
||||
function barCls(s: number) { return s >= 70 ? 'bg-signal-ok' : s >= 40 ? 'bg-warn' : 'bg-signal-danger'; }
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6 p-6">
|
||||
<div>
|
||||
<nav className="text-xs text-ink-faint mb-1">
|
||||
<Link href="/dashboard/trust" className="hover:underline">Trust Dashboard</Link> / Credit Readiness
|
||||
</nav>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Credit Readiness</h1>
|
||||
<p className="text-sm text-ink-faint mt-1">Pregătire pentru finanțare — analiză internă din datele CEO OS.</p>
|
||||
</div>
|
||||
|
||||
<div className="card p-4 bg-signal-danger/5 border-signal-danger/30 space-y-1">
|
||||
<p className="text-xs font-semibold text-signal-danger">Nu este un scor de credit</p>
|
||||
<p className="text-[10px] text-ink-faint">
|
||||
Acest indicator reflectă <strong>disponibilitatea datelor financiare auto-raportate</strong> din CEO OS.
|
||||
Nu este un scor de credit bancar, nu reflectă istoricul de plăți și nu poate fi folosit ca garanție sau evaluare externă.
|
||||
Pentru finanțare reală, consultă o instituție financiară autorizată.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Score */}
|
||||
<div className="card p-5 flex items-center gap-6">
|
||||
<div className="text-center w-24 shrink-0">
|
||||
<p className={`font-display text-5xl font-bold ${scoreCls(analysis.score)}`}>{analysis.score}</p>
|
||||
<p className="text-[10px] text-ink-faint mt-1">pregătire date</p>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-3 rounded-full bg-muted overflow-hidden">
|
||||
<div className={`h-full rounded-full ${barCls(analysis.score)}`} style={{ width: `${analysis.score}%` }} />
|
||||
</div>
|
||||
<p className="text-xs text-ink-faint">
|
||||
{analysis.score >= 70 ? '✅ Date financiare bine documentate' : analysis.score >= 40 ? '⚠ Lipsesc câteva categorii cheie' : '❌ Date financiare insuficiente în CEO OS'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Financial snapshot */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="card p-3 text-center">
|
||||
<p className="text-lg font-bold text-signal-ok">{analysis.totalIncome.toLocaleString()}</p>
|
||||
<p className="text-[10px] text-ink-faint">venituri totale</p>
|
||||
</div>
|
||||
<div className="card p-3 text-center">
|
||||
<p className="text-lg font-bold text-signal-danger">{analysis.totalExpense.toLocaleString()}</p>
|
||||
<p className="text-[10px] text-ink-faint">cheltuieli totale</p>
|
||||
</div>
|
||||
<div className="card p-3 text-center">
|
||||
<p className={`text-lg font-bold ${analysis.cashflow >= 0 ? 'text-signal-ok' : 'text-signal-danger'}`}>
|
||||
{analysis.cashflow >= 0 ? '+' : ''}{analysis.cashflow.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-[10px] text-ink-faint">cash flow net</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checklist */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Criterii de pregătire date</p>
|
||||
{analysis.checks.map((c) => (
|
||||
<div key={c.label} className="flex items-center gap-3">
|
||||
<span className={`text-lg shrink-0 ${c.done ? 'text-signal-ok' : 'text-ink-faint'}`}>
|
||||
{c.done ? '✓' : '○'}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-ink">{c.label}</p>
|
||||
<p className="text-[10px] text-ink-faint">{c.detail}</p>
|
||||
</div>
|
||||
<span className="text-[10px] text-ink-faint shrink-0">+{c.impact}pt</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 text-xs">
|
||||
<Link href="/dashboard/finance" className="text-primary hover:underline">Finance Dashboard →</Link>
|
||||
<Link href="/dashboard/trust/credibility" className="text-primary hover:underline">Profil Credibilitate →</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue