From f918b3229dc7510fb000a7b697a4f962db4efcec Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 18:00:43 +0000 Subject: [PATCH] feat(CC-090): add Retrospective page (7/30/90 day periods, 4-section retro form, saved as observations) --- src/app/dashboard/retrospective/page.tsx | 155 +++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/app/dashboard/retrospective/page.tsx diff --git a/src/app/dashboard/retrospective/page.tsx b/src/app/dashboard/retrospective/page.tsx new file mode 100644 index 0000000..5b9035a --- /dev/null +++ b/src/app/dashboard/retrospective/page.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { id: string; title: string; status: string; tags: string[]; updatedAt: string; createdAt: string; } +interface Goal { id: string; title: string; status: string; progress: number | null; tags: string[]; createdAt: string; } +interface Decision { id: string; title: string; outcome: string | null; tags: string[]; createdAt: string; } +interface Observation { id: string; metric: string; value: string; createdAt: string; } + +const PERIODS = [ + { label: '7 zile', days: 7 }, + { label: '30 zile', days: 30 }, + { label: '90 zile (Q)', days: 90 }, +]; + +const RETRO_METRICS = ['went-well', 'improve', 'lessons', 'next-actions']; + +export default function RetrospectivePage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [period, setPeriod] = useState(PERIODS[1]); + const [wentWell, setWentWell] = useState(''); + const [improve, setImprove] = useState(''); + const [lessons, setLessons] = useState(''); + const [nextActions, setNextActions] = useState(''); + const [saved, setSaved] = useState(false); + + const since = new Date(Date.now() - period.days * 86_400_000).toISOString(); + + const { data: tasks = [] } = useQuery({ queryKey: ['retro-tasks', tenantId], queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: goals = [] } = useQuery({ queryKey: ['retro-goals', tenantId], queryFn: () => apiFetch('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: decisions = [] } = useQuery({ queryKey: ['retro-decisions', tenantId], queryFn: () => apiFetch('/v1/decisions', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + const { data: observations = [] } = useQuery({ queryKey: ['retro-obs', tenantId], queryFn: () => apiFetch('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 60_000 }); + + const periodTasks = useMemo(() => tasks.filter((t) => t.createdAt >= since || t.updatedAt >= since), [tasks, since]); + const completedTasks = useMemo(() => periodTasks.filter((t) => t.status === 'completed' && t.updatedAt >= since), [periodTasks, since]); + const periodGoals = useMemo(() => goals.filter((g) => g.createdAt >= since), [goals, since]); + const periodDecs = useMemo(() => decisions.filter((d) => d.createdAt >= since), [decisions, since]); + const periodObs = useMemo(() => observations.filter((o) => o.createdAt >= since && !RETRO_METRICS.includes(o.metric)), [observations, since]); + + const pastRetros = useMemo(() => + observations + .filter((o) => RETRO_METRICS.includes(o.metric)) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .slice(0, 4), + [observations]); + + const saveMut = useMutation({ + mutationFn: async () => { + const ts = new Date().toISOString(); + const pairs = [ + { metric: 'went-well', value: wentWell }, + { metric: 'improve', value: improve }, + { metric: 'lessons', value: lessons }, + { metric: 'next-actions', value: nextActions }, + ].filter((p) => p.value.trim()); + await Promise.all(pairs.map((p) => + apiFetch('/v1/observations', { tenantId, method: 'POST', body: { + ...p, subjectType: 'retrospective', confidence: 1, observedAt: ts, + source: `Retro ${period.label} — ${new Date().toLocaleDateString('ro-RO')}`, + }}), + )); + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['retro-obs', tenantId] }); + setSaved(true); + setWentWell(''); setImprove(''); setLessons(''); setNextActions(''); + setTimeout(() => setSaved(false), 4000); + }, + }); + + return ( +
+
+

Retrospectivă

+

Analiză periodică a progresului și lecțiilor învățate.

+
+ + {/* Period selector */} +
+ {PERIODS.map((p) => ( + + ))} +
+ + {/* Stats snapshot */} +
+ {[ + { label: 'Tasks finalizate', value: completedTasks.length, icon: '✅' }, + { label: 'Obiective noi', value: periodGoals.length, icon: '🎯' }, + { label: 'Decizii luate', value: periodDecs.length, icon: '🧠' }, + { label: 'Observații', value: periodObs.length, icon: '📊' }, + ].map((s) => ( +
+

{s.icon}

+

{s.value}

+

{s.label}

+
+ ))} +
+ + {/* Retro form */} + {saved ? ( +
+

🎉

+

Retrospectivă salvată!

+

Salvată ca observații cu metricile went-well / improve / lessons / next-actions.

+
+ ) : ( +
+ {[ + { key: 'went-well', label: '🟢 Ce a mers bine?', val: wentWell, set: setWentWell }, + { key: 'improve', label: '🟡 Ce ar fi putut merge mai bine?', val: improve, set: setImprove }, + { key: 'lessons', label: '💡 Ce ai învățat?', val: lessons, set: setLessons }, + { key: 'next-actions', label: '🚀 Ce vei face diferit?', val: nextActions, set: setNextActions }, + ].map((f) => ( +
+ +