diff --git a/src/app/dashboard/life/page.tsx b/src/app/dashboard/life/page.tsx new file mode 100644 index 0000000..39edd24 --- /dev/null +++ b/src/app/dashboard/life/page.tsx @@ -0,0 +1,199 @@ +'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 Task { id: string; status: string; priority: string; dueDate: string | null; } +interface Goal { id: string; title: string; status: string; progress: number | null; targetDate: string | null; } +interface Observation { id: string; subjectType: string; metric: string; value: string; unit: string | null; createdAt: string; observedAt: string | null; } +interface Decision { id: string; title: string; status: string; createdAt: string; } + +const PERSONAL_TYPES = ['self','person','health','energy','sleep','mood','fitness','nutrition','wellbeing','productivity','education','course','certification','skill']; + +const DOMAIN_LINKS = [ + { label: 'Rutine & Taskuri', href: '/dashboard/routines', icon: '✅', desc: 'Taskuri zilnice și rutine recurente' }, + { label: 'Obiective', href: '/dashboard/goals', icon: '🎯', desc: 'Obiective pe termen mediu și lung' }, + { label: 'Energie & Wellbeing', href: '/dashboard/energy', icon: '⚡', desc: 'Stare fizică și mentală' }, + { label: 'Educație', href: '/dashboard/education', icon: '🎓', desc: 'Cursuri, certificări, ore studiu' }, + { label: 'Personal KPI', href: '/dashboard/personal-kpi', icon: '📊', desc: 'Metrici personale cu trend' }, + { label: 'Interacțiuni', href: '/dashboard/interactions', icon: '📅', desc: 'Timeline completă de activitate' }, + { label: 'Follow-ups', href: '/dashboard/follow-ups', icon: '📞', desc: 'Urmăriri și callback-uri pendinte' }, + { label: 'Decizii', href: '/dashboard/decisions', icon: '🧠', desc: 'Log de decizii luate' }, + { label: 'Calendar', href: '/dashboard/calendar', icon: '🗓️', desc: 'Termene și scadențe viitoare' }, + { label: 'Life Analytics', href: '/dashboard/life-analytics', icon: '📈', desc: 'Tendințe și tipare personale' }, + { label: 'Rapoarte Viață', href: '/dashboard/reports/life', icon: '📋', desc: 'Rapoarte 30/90/180 zile' }, + { label: 'Tendințe', href: '/dashboard/trends', icon: '🔍', desc: 'Analiză tipare din toate datele' }, +]; + +function daysUntil(d: Date) { return Math.ceil((d.getTime() - Date.now()) / 86400_000); } + +export default function LifeOSPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const { data: tasks = [], isLoading: tL } = useQuery({ + queryKey: ['life-os-tasks', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + const { data: goals = [], isLoading: gL } = useQuery({ + queryKey: ['life-os-goals', tenantId], + queryFn: () => apiFetch('/v1/goals?status=active&limit=20', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + const { data: rawObs = [], isLoading: oL } = useQuery({ + queryKey: ['life-os-obs', tenantId], + queryFn: () => apiFetch('/v1/observations', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + const { data: decisions = [], isLoading: dL } = useQuery({ + queryKey: ['life-os-decisions', tenantId], + queryFn: () => apiFetch('/v1/decisions?limit=100', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + + const isLoading = tL || gL || oL || dL; + + const summary = useMemo(() => { + const active = tasks.filter((t) => t.status !== 'completed' && t.status !== 'cancelled'); + const overdue = active.filter((t) => t.dueDate && daysUntil(new Date(t.dueDate)) < 0); + const dueToday = active.filter((t) => t.dueDate && daysUntil(new Date(t.dueDate)) === 0); + + const cutoff7 = Date.now() - 7 * 86400_000; + const personalObs = rawObs.filter((o) => PERSONAL_TYPES.includes(o.subjectType) && new Date(o.observedAt ?? o.createdAt).getTime() >= cutoff7); + + const byMetric: Record = {}; + for (const o of personalObs) { + byMetric[o.metric] = [...(byMetric[o.metric] ?? []), o]; + } + const latestObs = Object.entries(byMetric).map(([m, obs]) => { + const latest = [...obs].sort((a, b) => new Date(b.observedAt ?? b.createdAt).getTime() - new Date(a.observedAt ?? a.createdAt).getTime())[0]; + return { metric: m, latest }; + }).slice(0, 4); + + const avgProgress = goals.length > 0 + ? Math.round(goals.reduce((s, g) => s + (g.progress ?? 0), 0) / goals.length) + : 0; + + const recentDecisions = [...decisions] + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 3); + + return { active, overdue, dueToday, latestObs, avgProgress, recentDecisions }; + }, [tasks, goals, rawObs, decisions]); + + return ( +
+
+

Life OS

+

+ {isLoading ? 'Se încarcă…' : `${new Date().toLocaleDateString('ro-RO', { weekday: 'long', day: 'numeric', month: 'long' })} · ${summary.active.length} taskuri active · ${goals.length} obiective`} +

+
+ + {/* Status cards */} +
+ +

{summary.active.length}

+

taskuri active

+ {summary.overdue.length > 0 &&

{summary.overdue.length} depășite

} + {summary.dueToday.length > 0 &&

{summary.dueToday.length} azi

} + + + +

{goals.length}

+

obiective active

+

{summary.avgProgress}% progres mediu

+ + + +

{summary.latestObs.length}

+

metrici personale active

+

ultimele 7 zile

+ + + +

{decisions.length}

+

decizii logate

+ +
+ +
+ {/* Active goals */} +
+
+

Obiective active

+ Toate → +
+ {goals.length === 0 ? ( +

Niciun obiectiv activ.

+ ) : goals.slice(0, 5).map((g) => ( +
+
+ {g.title} + {g.progress ?? 0}% +
+
+
+
+
+ ))} +
+ + {/* Latest personal obs */} +
+
+

Metrici recente

+ Wellbeing → +
+ {summary.latestObs.length === 0 ? ( +

Nicio observație recentă.

+ ) : summary.latestObs.map(({ metric, latest }) => ( +
+ {metric} + + {latest.value}{latest.unit ? ` ${latest.unit}` : ''} + +
+ ))} +
+ + {/* Recent decisions */} +
+
+

Decizii recente

+ Toate → +
+ {summary.recentDecisions.length === 0 ? ( +

Nicio decizie.

+ ) : summary.recentDecisions.map((d) => ( +
+

{d.title}

+

{new Date(d.createdAt).toLocaleDateString('ro-RO', { day: '2-digit', month: 'short' })} · {d.status}

+
+ ))} +
+
+ + {/* Domain navigation */} +
+

Module Life OS

+
+ {DOMAIN_LINKS.map((link) => ( + +
+ {link.icon} + {link.label} +
+

{link.desc}

+ + ))} +
+
+
+ ); +}