feat(CC-088): add Weekly Review page (auto 7-day summary: wins, attention, decisions, prep)

This commit is contained in:
admin-valentin 2026-08-02 17:45:00 +00:00
parent 83b5defca2
commit 4739293077

View file

@ -0,0 +1,125 @@
'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; title: string; status: string; priority: string | null; createdAt: string; }
interface Goal { id: string; title: string; status: string; progress: number | null; updatedAt: string; }
interface Decision { id: string; title: string; impact: string | null; createdAt: string; }
interface Observation { id: string; metric: string; value: string; confidence: number | null; createdAt: string; }
function last7(): string { const d = new Date(); d.setDate(d.getDate() - 7); return d.toISOString(); }
export default function WeeklyReviewPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const since = last7();
const { data: tasks = [] } = useQuery({ queryKey: ['wr-tasks', tenantId], queryFn: () => apiFetch<Task[]>('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
const { data: goals = [] } = useQuery({ queryKey: ['wr-goals', tenantId], queryFn: () => apiFetch<Goal[]>('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
const { data: decisions = [] } = useQuery({ queryKey: ['wr-decisions', tenantId], queryFn: () => apiFetch<Decision[]>('/v1/decisions?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
const { data: observations = [] } = useQuery({ queryKey: ['wr-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 });
const week = useMemo(() => {
const isRecent = (d: string) => d >= since;
const newTasks = tasks.filter((t) => isRecent(t.createdAt));
const doneTasks = tasks.filter((t) => t.status === 'completed' && isRecent(t.createdAt));
const overdueT = tasks.filter((t) => t.status !== 'completed' && t.status !== 'cancelled');
const updGoals = goals.filter((g) => isRecent(g.updatedAt));
const newDecisions = decisions.filter((d) => isRecent(d.createdAt));
const newObs = observations.filter((o) => isRecent(o.createdAt));
const urgentOpen = tasks.filter((t) => t.priority === 'urgent' && !['completed','cancelled'].includes(t.status));
return { newTasks, doneTasks, overdueT, updGoals, newDecisions, newObs, urgentOpen };
}, [tasks, goals, decisions, observations, since]);
const weekLabel = new Date().toLocaleDateString('ro-RO', { day: '2-digit', month: 'long', year: 'numeric' });
return (
<div className="max-w-3xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Weekly Review</h1>
<p className="text-sm text-ink-faint mt-1">Rezumat săptămânal automat ce s-a întâmplat în ultimele 7 zile.</p>
<p className="text-xs text-ink-faint mt-0.5">Generat: {weekLabel}</p>
</div>
{/* Wins */}
<div className="card p-5 border-l-4 border-l-signal-ok space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-signal-ok">Realizări săptămâna asta</p>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{[
{ label: 'Taskuri finalizate', value: week.doneTasks.length },
{ label: 'Decizii luate', value: week.newDecisions.length },
{ label: 'Observații logate', value: week.newObs.length },
{ label: 'Obiective actualizate', value: week.updGoals.length },
].map((s) => (
<div key={s.label} className="text-center">
<p className="text-2xl font-bold text-ink">{s.value}</p>
<p className="text-[10px] text-ink-faint">{s.label}</p>
</div>
))}
</div>
{week.doneTasks.length > 0 && (
<div className="space-y-1 pt-2 border-t border-border/50">
{week.doneTasks.slice(0, 5).map((t) => (
<p key={t.id} className="text-xs text-ink flex items-center gap-2">
<span className="text-signal-ok"></span>{t.title}
</p>
))}
{week.doneTasks.length > 5 && <p className="text-[10px] text-ink-faint">+{week.doneTasks.length - 5} mai multe</p>}
</div>
)}
</div>
{/* Attention items */}
{(week.urgentOpen.length > 0 || week.overdueT.length > 10) && (
<div className="card p-5 border-l-4 border-l-warn space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-warn">Necesită atenție</p>
{week.urgentOpen.length > 0 && (
<div className="space-y-1">
<p className="text-[10px] text-ink-faint font-semibold">Urgente deschise:</p>
{week.urgentOpen.slice(0, 3).map((t) => (
<p key={t.id} className="text-xs text-warn"> {t.title}</p>
))}
</div>
)}
<p className="text-xs text-ink-faint">
{week.overdueT.length} taskuri active total <Link href="/dashboard/tasks" className="text-primary hover:underline">vezi lista </Link>
</p>
</div>
)}
{/* New decisions */}
{week.newDecisions.length > 0 && (
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Decizii luate această săptămână</p>
{week.newDecisions.slice(0, 5).map((d) => (
<div key={d.id} className="flex items-center gap-3">
<span className="text-primary text-sm shrink-0"></span>
<p className="text-sm text-ink">{d.title}</p>
{d.impact && <span className="text-[9px] text-ink-faint ml-auto capitalize">{d.impact}</span>}
</div>
))}
</div>
)}
{/* Next week prep */}
<div className="card p-5 space-y-2 bg-primary/5 border-primary/20">
<p className="text-xs font-semibold text-ink">Pregătire pentru săptămâna viitoare</p>
<div className="space-y-1 text-xs text-ink-faint">
<p> Revizuiește obiectivele active cu progres mai mic de 50%</p>
<p> Prioritizează taskurile urgente deschise</p>
<p> Loghează cel puțin 3 observații personale</p>
<p> Documentează deciziile importante cu rațional</p>
</div>
<div className="flex flex-wrap gap-3 mt-2 text-xs">
<Link href="/dashboard/goals" className="text-primary hover:underline">Obiective </Link>
<Link href="/dashboard/tasks" className="text-primary hover:underline">Taskuri </Link>
<Link href="/dashboard/personal-kpi" className="text-primary hover:underline">Loghează observații </Link>
</div>
</div>
</div>
);
}