From 5f0e1db7256adfaacf528a968f5c8cbd69bd9214 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:41:13 +0000 Subject: [PATCH] feat(CC-075): add Life Reports page (30/90/180d personal report with export) --- src/app/dashboard/reports/life/page.tsx | 229 ++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/app/dashboard/reports/life/page.tsx diff --git a/src/app/dashboard/reports/life/page.tsx b/src/app/dashboard/reports/life/page.tsx new file mode 100644 index 0000000..333c653 --- /dev/null +++ b/src/app/dashboard/reports/life/page.tsx @@ -0,0 +1,229 @@ +'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 Task { id: string; status: string; priority: string; createdAt: string; } +interface Goal { id: string; title: string; status: string; targetDate: string | null; progress: number | null; createdAt: string; } +interface Decision { id: string; title: string; status: string; createdAt: string; } +interface OutcomeReview { id: string; wasSuccessful: boolean | null; rating: number | null; createdAt: string; } +interface Observation { id: string; subjectType: string; metric: string; value: string; unit: string | null; createdAt: string; } + +const PERSONAL_TYPES = ['self','person','health','energy','sleep','mood','fitness','nutrition','wellbeing','productivity']; + +export default function LifeReportsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const [period, setPeriod] = useState<30 | 90 | 180>(30); + + const { data: tasks = [], isLoading: tL } = useQuery({ + queryKey: ['lr-tasks', tenantId], queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + const { data: goals = [], isLoading: gL } = useQuery({ + queryKey: ['lr-goals', tenantId], queryFn: () => apiFetch('/v1/goals?limit=100', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + const { data: decisions = [], isLoading: dL } = useQuery({ + queryKey: ['lr-decisions', tenantId], queryFn: () => apiFetch('/v1/decisions?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + const { data: outcomes = [], isLoading: oRL } = useQuery({ + queryKey: ['lr-outcomes', tenantId], queryFn: () => apiFetch('/v1/outcome-reviews?limit=100', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + const { data: rawObs = [], isLoading: obsL } = useQuery({ + queryKey: ['lr-obs', tenantId], queryFn: () => apiFetch('/v1/observations', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + + const isLoading = tL || gL || dL || oRL || obsL; + const cutoff = Date.now() - period * 86400_000; + + const report = useMemo(() => { + const inPeriod = (d: string) => new Date(d).getTime() >= cutoff; + + const periodTasks = tasks.filter((t) => inPeriod(t.createdAt)); + const completed = periodTasks.filter((t) => t.status === 'completed'); + const completionRate = periodTasks.length > 0 ? Math.round((completed.length / periodTasks.length) * 100) : 0; + + const periodGoals = goals.filter((g) => inPeriod(g.createdAt) || g.status === 'active'); + const goalsDone = goals.filter((g) => g.status === 'completed' && inPeriod(g.createdAt)); + const avgProgress = periodGoals.reduce((s, g) => s + (g.progress ?? 0), 0) / (periodGoals.length || 1); + + const periodDecisions = decisions.filter((d) => inPeriod(d.createdAt)); + const periodOutcomes = outcomes.filter((o) => inPeriod(o.createdAt)); + const successfulOutcomes = periodOutcomes.filter((o) => o.wasSuccessful === true); + const avgRating = periodOutcomes.filter((o) => o.rating != null).reduce((s, o) => s + (o.rating ?? 0), 0) + / (periodOutcomes.filter((o) => o.rating != null).length || 1); + + const personalObs = rawObs.filter((o) => PERSONAL_TYPES.includes(o.subjectType) && inPeriod(o.createdAt)); + const byMetric: Record = {}; + for (const o of personalObs) { + const v = parseFloat(o.value); + if (!isNaN(v)) byMetric[o.metric] = [...(byMetric[o.metric] ?? []), v]; + } + const metricSummary = Object.entries(byMetric).map(([m, vals]) => ({ + metric: m, + avg: vals.reduce((a, b) => a + b, 0) / vals.length, + latest: vals[vals.length - 1], + count: vals.length, + })).sort((a, b) => b.count - a.count); + + return { + completionRate, completed: completed.length, total: periodTasks.length, + goalsDone: goalsDone.length, activeGoals: periodGoals.filter(g => g.status === 'active').length, + avgProgress: Math.round(avgProgress), + decisions: periodDecisions.length, outcomes: periodOutcomes.length, + successRate: periodOutcomes.length > 0 ? Math.round((successfulOutcomes.length / periodOutcomes.length) * 100) : null, + avgRating: periodOutcomes.length > 0 ? avgRating : null, + observations: personalObs.length, metricSummary, + }; + }, [tasks, goals, decisions, outcomes, rawObs, cutoff]); + + const periodLabel = { 30: '30 zile', 90: '90 zile', 180: '6 luni' }[period]; + + const generateText = () => { + const lines = [ + `# Raport de viață — ultimele ${periodLabel}`, + `Data generării: ${new Date().toLocaleDateString('ro-RO')}`, + ``, + `## Execuție & Productivitate`, + `- Taskuri finalizate: ${report.completed} din ${report.total} (${report.completionRate}%)`, + `- Obiective active: ${report.activeGoals} | Finalizate în perioadă: ${report.goalsDone}`, + `- Progres mediu obiective: ${report.avgProgress}%`, + ``, + `## Decizii & Outcome-uri`, + `- Decizii luate: ${report.decisions}`, + `- Revizuiri outcome: ${report.outcomes}`, + report.successRate != null ? `- Rată de succes: ${report.successRate}%` : '', + report.avgRating != null ? `- Rating mediu: ${report.avgRating.toFixed(1)}/10` : '', + ``, + `## Observații personale (${report.observations} înregistrări)`, + ...report.metricSummary.slice(0, 8).map((m) => `- ${m.metric}: medie ${m.avg.toFixed(1)} (${m.count} măsurători)`), + ].filter(Boolean).join('\n'); + + const blob = new Blob([lines], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `raport-viata-${periodLabel.replace(' ', '')}-${new Date().toISOString().slice(0, 10)}.txt`; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+
+

Rapoarte de Viață

+

Analiză personală: obiective, timp, energie și decizii.

+
+
+
+ {([30, 90, 180] as const).map((d) => ( + + ))} +
+ +
+
+ + {isLoading ? ( +
Se generează raportul…
+ ) : ( + <> + {/* Execution */} +
+

Execuție & Productivitate · {periodLabel}

+
+
+

{report.completionRate}%

+

rată completare taskuri

+

{report.completed} din {report.total}

+
+
+

{report.activeGoals}

+

obiective active

+

{report.avgProgress}% progres mediu

+
+
+

{report.goalsDone}

+

obiective finalizate

+

în {periodLabel}

+
+
+ + {/* Completion bar */} +
+
+ Rată completare{report.completionRate}% +
+
+
+
+
+
+ + {/* Decision quality */} +
+

Calitate decizii · {periodLabel}

+
+
+

{report.decisions}

+

decizii luate

+
+
+

{report.outcomes}

+

outcome reviews

+
+
+

= 70 ? 'text-signal-ok' : report.successRate >= 50 ? 'text-warn' : 'text-signal-danger') : 'text-ink-faint'}`}> + {report.successRate !== null ? `${report.successRate}%` : '—'} +

+

rată de succes

+
+
+

= 7 ? 'text-signal-ok' : report.avgRating >= 5 ? 'text-warn' : 'text-signal-danger') : 'text-ink-faint'}`}> + {report.avgRating !== null ? report.avgRating.toFixed(1) : '—'} +

+

rating mediu /10

+
+
+
+ + {/* Personal metrics */} +
+

Metrici personale · {periodLabel}

+ {report.metricSummary.length === 0 ? ( +

Nicio observație personală în această perioadă. Adaugă din Personal KPI sau Energy & Wellbeing.

+ ) : ( +
+

{report.observations} înregistrări total

+ {report.metricSummary.slice(0, 10).map((m) => ( +
+

{m.metric}

+
+
+
+ {m.avg.toFixed(1)} + ({m.count}x) +
+ ))} +
+ )} +
+ + )} +
+ ); +}