feat(CC-083): add Digital Twin page (decision style profile from behavioral data + disclaimers)
This commit is contained in:
parent
de0a635069
commit
abe64cb1f9
1 changed files with 116 additions and 0 deletions
116
src/app/dashboard/digital-twin/page.tsx
Normal file
116
src/app/dashboard/digital-twin/page.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
'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 Decision { id: string; title: string; impact: string | null; status: string; rationale: string | null; tags: string[]; createdAt: string; }
|
||||
interface Observation { id: string; metric: string; value: string; subjectType: string; createdAt: string; observedAt: string | null; }
|
||||
interface Task { id: string; status: string; priority: string | null; tags: string[]; createdAt: string; }
|
||||
|
||||
const STYLE_INDICATORS = [
|
||||
{ id: 'decisiveness', label: 'Decisivitate', desc: 'Cât de rapid și ferm iei decizii' },
|
||||
{ id: 'risk', label: 'Toleranță la risc', desc: 'Preferința pentru soluții conservatoare vs. îndrăznețe' },
|
||||
{ id: 'planning', label: 'Orientare spre planificare', desc: 'Task-uri cu due date vs. spontane' },
|
||||
{ id: 'collaboration', label: 'Orientare colaborativă', desc: 'Decizii cu stakeholders vs. autonome' },
|
||||
{ id: 'documentation', label: 'Documentare', desc: 'Cât de detaliat documentezi deciziile și observațiile' },
|
||||
{ id: 'consistency', label: 'Consistență', desc: 'Regularitatea logării datelor personale' },
|
||||
];
|
||||
|
||||
export default function DigitalTwinPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
|
||||
const { data: decisions = [] } = useQuery({ queryKey: ['dt-dec', tenantId], queryFn: () => apiFetch<Decision[]>('/v1/decisions?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
|
||||
const { data: observations = [] } = useQuery({ queryKey: ['dt-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
|
||||
const { data: tasks = [] } = useQuery({ queryKey: ['dt-tasks', tenantId], queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
|
||||
|
||||
const profile = useMemo(() => {
|
||||
const totalDecisions = decisions.length;
|
||||
const documentedDecisions = decisions.filter((d) => d.rationale && d.rationale.length > 20).length;
|
||||
const highImpact = decisions.filter((d) => d.impact === 'high' || d.impact === 'critical').length;
|
||||
const tasksWithDue = tasks.filter((t) => t.dueDate !== undefined).length;
|
||||
const urgentTasks = tasks.filter((t) => t.priority === 'urgent').length;
|
||||
const obsLast30 = observations.filter((o) => (Date.now() - new Date(o.observedAt ?? o.createdAt).getTime()) < 30 * 86400_000).length;
|
||||
|
||||
const scores: Record<string, number> = {
|
||||
decisiveness: Math.min(100, totalDecisions * 5),
|
||||
risk: highImpact > 0 && totalDecisions > 0 ? Math.min(100, Math.round((highImpact / totalDecisions) * 200)) : 30,
|
||||
planning: tasks.length > 0 ? Math.min(100, Math.round((tasksWithDue / tasks.length) * 100)) : 50,
|
||||
collaboration: totalDecisions > 0 ? Math.min(100, Math.round((documentedDecisions / totalDecisions) * 100)) : 0,
|
||||
documentation: totalDecisions > 0 ? Math.min(100, Math.round((documentedDecisions / totalDecisions) * 100)) : 0,
|
||||
consistency: Math.min(100, obsLast30 * 5),
|
||||
};
|
||||
|
||||
return scores;
|
||||
}, [decisions, observations, tasks]);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6 p-6">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Digital Twin</h1>
|
||||
<p className="text-sm text-ink-faint mt-1">Model de preferințe și stil decizional — derivat din comportamentul tău în CEO OS.</p>
|
||||
</div>
|
||||
|
||||
<div className="card p-4 bg-warn/5 border-warn/30">
|
||||
<p className="text-xs text-ink-faint">
|
||||
<strong>Limitări obligatorii:</strong> Modelul este derivat exclusiv din datele auto-raportate în CEO OS.
|
||||
Nu predice comportamentul viitor. Nu actionează autonom. Explică probabilități observate, nu certitudini.
|
||||
Scorurile sunt orientative — nu se folosesc pentru evaluarea altor persoane.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Style profile */}
|
||||
<div className="card p-5 space-y-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Profil stil decizional</p>
|
||||
{STYLE_INDICATORS.map((ind) => {
|
||||
const score = profile[ind.id] ?? 0;
|
||||
return (
|
||||
<div key={ind.id} className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-ink">{ind.label}</p>
|
||||
<p className="text-[10px] text-ink-faint">{ind.desc}</p>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-ink ml-4 shrink-0">{score}%</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-muted overflow-hidden">
|
||||
<div className="h-full rounded-full bg-primary/60" style={{ width: `${score}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Context */}
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="card p-4 text-center">
|
||||
<p className="text-2xl font-bold text-ink">{decisions.length}</p>
|
||||
<p className="text-[10px] text-ink-faint">decizii logate</p>
|
||||
</div>
|
||||
<div className="card p-4 text-center">
|
||||
<p className="text-2xl font-bold text-ink">{observations.length}</p>
|
||||
<p className="text-[10px] text-ink-faint">observații personale</p>
|
||||
</div>
|
||||
<div className="card p-4 text-center">
|
||||
<p className="text-2xl font-bold text-ink">{tasks.length}</p>
|
||||
<p className="text-[10px] text-ink-faint">taskuri în sistem</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-4 bg-primary/5 border-primary/20 space-y-2">
|
||||
<p className="text-xs font-semibold text-ink">Îmbunătățește modelul</p>
|
||||
<div className="text-[10px] text-ink-faint space-y-0.5">
|
||||
<p>Cu cât loghezi mai multe decizii cu rațional și observații personale, cu atât modelul devine mai precis.</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
<Link href="/dashboard/decisions" className="text-[10px] text-primary hover:underline">Adaugă decizii →</Link>
|
||||
<Link href="/dashboard/personal-kpi" className="text-[10px] text-primary hover:underline">Loghează observații →</Link>
|
||||
<Link href="/dashboard/coach" className="text-[10px] text-primary hover:underline">Performance Coach →</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue