'use client'; import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { apiFetch } from '../../../lib/api'; import { useSession } from '../../../components/session-provider'; interface Observation { id: string; metric: string; value: string; unit: string | null; subjectType: string; confidence: number | null; createdAt: string; observedAt: string | null; } interface Task { id: string; status: string; priority: string | null; dueDate: string | null; tags: string[]; createdAt: string; } interface Goal { id: string; title: string; status: string; progress: number | null; targetDate: string | null; } const NUMERIC_OBS = ['energie', 'energy', 'somn', 'sleep', 'focus', 'stare', 'activitate', 'hidratare', 'mood', 'stress', 'productivitate']; function parseNum(v: string): number | null { const n = parseFloat(v.replace(',', '.')); return isNaN(n) ? null : n; } function movingAvg(values: number[], window: number): number[] { return values.map((_, i) => { const slice = values.slice(Math.max(0, i - window + 1), i + 1); return slice.reduce((a, b) => a + b, 0) / slice.length; }); } function trend(values: number[]): 'up' | 'down' | 'flat' { if (values.length < 3) return 'flat'; const recent = values.slice(-3).reduce((a, b) => a + b, 0) / 3; const earlier = values.slice(0, Math.max(1, values.length - 3)).reduce((a, b) => a + b, 0) / Math.max(1, values.length - 3); if (recent > earlier * 1.05) return 'up'; if (recent < earlier * 0.95) return 'down'; return 'flat'; } interface CoachInsight { type: 'strength' | 'warning' | 'suggestion'; message: string; metric?: string; } export default function PerformanceCoachPage() { const { activeTenant } = useSession(); const tenantId = activeTenant?.tenantId ?? ''; const [period, setPeriod] = useState(30); const { data: observations = [], isLoading: oL } = useQuery({ queryKey: ['coach-obs', tenantId], queryFn: () => apiFetch('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000, }); const { data: tasks = [] } = useQuery({ queryKey: ['coach-tasks', tenantId], queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000, }); const { data: goals = [] } = useQuery({ queryKey: ['coach-goals', tenantId], queryFn: () => apiFetch('/v1/goals?limit=100', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000, }); const cutoff = useMemo(() => new Date(Date.now() - period * 86400_000), [period]); const analytics = useMemo(() => { const recentObs = observations.filter((o) => new Date(o.observedAt ?? o.createdAt) >= cutoff); // Group numeric metrics const metricMap: Record = {}; for (const o of recentObs) { const val = parseNum(o.value); if (val === null) continue; const isNumericMetric = NUMERIC_OBS.some((k) => o.metric.toLowerCase().includes(k)); if (isNumericMetric || o.unit === '/10' || o.unit === 'ore' || o.unit === 'min') { const date = new Date(o.observedAt ?? o.createdAt); metricMap[o.metric] = [...(metricMap[o.metric] ?? []), { date, value: val }]; } } // Sort each metric by date const metrics = Object.entries(metricMap) .map(([name, entries]) => { const sorted = [...entries].sort((a, b) => a.date.getTime() - b.date.getTime()); const values = sorted.map((e) => e.value); const avg = values.reduce((a, b) => a + b, 0) / values.length; const latest = values[values.length - 1]; const t = trend(values); const ma = movingAvg(values, 3); return { name, values, avg, latest, trend: t, ma, dates: sorted.map((e) => e.date) }; }) .sort((a, b) => b.values.length - a.values.length) .slice(0, 6); // Task completion rate const recentTasks = tasks.filter((t) => new Date(t.createdAt) >= cutoff); const completedTasks = recentTasks.filter((t) => t.status === 'completed'); const taskCompletionRate = recentTasks.length > 0 ? Math.round((completedTasks.length / recentTasks.length) * 100) : null; // Overdue rate const overdueCount = tasks.filter((t) => t.status !== 'completed' && t.dueDate && new Date(t.dueDate) < new Date() ).length; // Active goals progress const activeGoals = goals.filter((g) => g.status === 'active'); const avgGoalProgress = activeGoals.length > 0 ? Math.round(activeGoals.reduce((s, g) => s + (g.progress ?? 0), 0) / activeGoals.length) : null; // Generate insights const insights: CoachInsight[] = []; for (const m of metrics) { if (m.trend === 'up' && m.latest > m.avg) { insights.push({ type: 'strength', message: `${m.name} este în creștere — medie ${m.avg.toFixed(1)}, recent ${m.latest.toFixed(1)}.`, metric: m.name }); } else if (m.trend === 'down' && m.values.length >= 3) { insights.push({ type: 'warning', message: `${m.name} scade — ultimele valori arată un trend negativ.`, metric: m.name }); } } if (taskCompletionRate !== null) { if (taskCompletionRate < 50) { insights.push({ type: 'warning', message: `Rata de finalizare taskuri este ${taskCompletionRate}% — sub jumătate. Prioritizează sau redimensionează.` }); } else if (taskCompletionRate >= 80) { insights.push({ type: 'strength', message: `Excelent! ${taskCompletionRate}% din taskuri finalizate în ultimele ${period} zile.` }); } } if (overdueCount >= 5) { insights.push({ type: 'warning', message: `${overdueCount} taskuri depășite acumulat. Revizuiește și reprogramează sau marchează ca neaplicabile.` }); } if (avgGoalProgress !== null && avgGoalProgress < 30) { insights.push({ type: 'suggestion', message: `Progres mediu obiective: ${avgGoalProgress}%. Consideră descompunerea obiectivelor în sub-taskuri mai mici.` }); } if (recentObs.length < 5) { insights.push({ type: 'suggestion', message: `Numai ${recentObs.length} observații în ultimele ${period} zile. Logarea zilnică a metricilor crește acuratețea analizei.` }); } if (insights.length === 0) { insights.push({ type: 'strength', message: 'Date insuficiente pentru insight-uri personalizate. Continuă să loghezi observații zilnic.' }); } return { metrics, taskCompletionRate, overdueCount, avgGoalProgress, insights, totalObs: recentObs.length }; }, [observations, tasks, goals, cutoff]); const TREND_ICON = { up: '↑', down: '↓', flat: '→' }; const TREND_CLS = { up: 'text-signal-ok', down: 'text-signal-danger', flat: 'text-ink-faint' }; const INSIGHT_CLS = { strength: 'border-signal-ok/30 bg-signal-ok/5', warning: 'border-signal-danger/30 bg-signal-danger/5', suggestion: 'border-primary/20 bg-primary/5' }; const INSIGHT_ICON = { strength: '💪', warning: '⚠', suggestion: '💡' }; return (

Performance Coach

Analiză de pattern-uri din observațiile și taskurile tale personale.

{[14, 30, 90].map((p) => ( ))}

Metodologie și limitări: Analiză bazată exclusiv pe date auto-raportate în CEO OS ({analytics.totalObs} observații). Nu înlocuiește evaluarea profesională medicală, psihologică sau de coaching. Tendințele reflectă datele disponibile, nu realitatea completă.

{/* KPI cards */}

= 70 ? 'text-signal-ok' : 'text-warn') : 'text-ink-faint'}`}> {analytics.taskCompletionRate !== null ? `${analytics.taskCompletionRate}%` : '—'}

rata finalizare taskuri

5 ? 'text-signal-danger' : analytics.overdueCount > 0 ? 'text-warn' : 'text-signal-ok'}`}> {analytics.overdueCount}

taskuri depășite

= 60 ? 'text-signal-ok' : 'text-warn') : 'text-ink-faint'}`}> {analytics.avgGoalProgress !== null ? `${analytics.avgGoalProgress}%` : '—'}

progres mediu obiective

{/* Insights */}

Insights personalizate

{analytics.insights.map((insight, i) => (
{INSIGHT_ICON[insight.type]}

{insight.message}

))}
{/* Metric trends */} {analytics.metrics.length > 0 && (

Trendul metricilor ({period} zile)

{analytics.metrics.map((m) => { const max = Math.max(...m.values, 0.1); return (

{m.name}

{TREND_ICON[m.trend]} {m.avg.toFixed(1)} avg
{m.values.slice(-20).map((v, i) => (
= m.avg ? 'rgb(var(--primary) / 0.6)' : 'rgb(var(--muted-foreground) / 0.2)', }} /> ))}
min: {Math.min(...m.values).toFixed(1)} max: {Math.max(...m.values).toFixed(1)} ultimul: {m.latest.toFixed(1)}
); })}
)} {oL && (
Se calculează…
)}
); }