diff --git a/src/app/dashboard/goals/[id]/page.tsx b/src/app/dashboard/goals/[id]/page.tsx new file mode 100644 index 0000000..c75118a --- /dev/null +++ b/src/app/dashboard/goals/[id]/page.tsx @@ -0,0 +1,239 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface Goal { id: string; title: string; description: string | null; status: string; progress: number | null; tags: string[]; targetDate: string | null; createdAt: string; updatedAt: string; } +interface Task { id: string; title: string; status: string; priority: string | null; tags: string[]; createdAt: string; } +interface Observation { id: string; metric: string; value: string; subjectType: string; source: string | null; createdAt: string; observedAt: string | null; } + +const STATUS_OPTIONS = ['active','completed','paused','cancelled','archived']; +const STATUS_COLORS: Record = { + active:'text-signal-ok', completed:'text-primary', paused:'text-warn', + cancelled:'text-signal-danger', archived:'text-ink-faint', +}; + +export default function GoalDetailPage() { + const { id } = useParams<{ id: string }>(); + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const router = useRouter(); + const [editProgress, setEditProgress] = useState(false); + const [progressVal, setProgressVal] = useState(0); + const [noteText, setNoteText] = useState(''); + const [taskTitle, setTaskTitle] = useState(''); + const [editTitle, setEditTitle] = useState(false); + const [titleVal, setTitleVal] = useState(''); + + const { data: goal, isLoading } = useQuery({ + queryKey: ['goal-detail', tenantId, id], + queryFn: () => apiFetch(`/v1/goals/${id}`, { tenantId }), + enabled: Boolean(tenantId) && Boolean(id), + staleTime: 30_000, + }); + + const { data: tasks = [] } = useQuery({ + queryKey: ['goal-tasks', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const { data: observations = [] } = useQuery({ + queryKey: ['goal-obs', tenantId], + queryFn: () => apiFetch('/v1/observations', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const linkedTasks = useMemo(() => + tasks.filter((t) => t.tags.includes(`goal-${id}`) || t.tags.includes(id ?? '')), + [tasks, id]); + + const notes = useMemo(() => + observations + .filter((o) => o.source === id || o.source === `goal-${id}` || o.subjectType === `goal-${id}`) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)), + [observations, id]); + + const done = linkedTasks.filter((t) => t.status === 'completed').length; + const total = linkedTasks.length; + const taskProgress = total > 0 ? Math.round((done / total) * 100) : null; + const displayProgress = goal?.progress ?? taskProgress ?? 0; + + const patchMut = useMutation({ + mutationFn: (body: Partial) => apiFetch(`/v1/goals/${id}`, { tenantId, method: 'PATCH', body }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['goal-detail', tenantId, id] }), + }); + + const addTaskMut = useMutation({ + mutationFn: () => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: { + title: taskTitle, status: 'todo', tags: [`goal-${id}`], + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['goal-tasks', tenantId] }); setTaskTitle(''); }, + }); + + const addNoteMut = useMutation({ + mutationFn: () => apiFetch('/v1/observations', { tenantId, method: 'POST', body: { + metric: 'goal-note', value: noteText, + subjectType: `goal-${id}`, source: id, + confidence: 1, observedAt: new Date().toISOString(), + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['goal-obs', tenantId] }); setNoteText(''); }, + }); + + const patchTaskMut = useMutation({ + mutationFn: ({ taskId, status }: { taskId: string; status: string }) => + apiFetch(`/v1/tasks/${taskId}`, { tenantId, method: 'PATCH', body: { status } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['goal-tasks', tenantId] }), + }); + + if (isLoading) return
Se încarcă…
; + if (!goal) return ( +
+

Obiectivul nu a fost găsit.

+ +
+ ); + + const daysLeft = goal.targetDate + ? Math.ceil((new Date(goal.targetDate).getTime() - Date.now()) / 86_400_000) + : null; + + return ( +
+ {/* Header */} +
+ +
+
+ {editTitle ? ( +
+ setTitleVal(e.target.value)} + className="flex-1 rounded-lg border bg-background px-3 py-1.5 text-lg font-semibold text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + + +
+ ) : ( +

{ setTitleVal(goal.title); setEditTitle(true); }}> + {goal.title} +

+ )} + {goal.description &&

{goal.description}

} +
+
+ + {/* Meta row */} +
+ + {goal.targetDate && ( + + 📅 {new Date(goal.targetDate).toLocaleDateString('ro-RO')} + {daysLeft !== null && ({daysLeft > 0 ? `${daysLeft} zile` : 'expirat'})} + + )} + Creat {new Date(goal.createdAt).toLocaleDateString('ro-RO')} + {goal.tags.length > 0 && ( +
+ {goal.tags.map((t) => ( + {t} + ))} +
+ )} +
+
+ + {/* Progress */} +
+
+

Progres

+
+ {displayProgress}% + +
+
+ {editProgress ? ( +
+ setProgressVal(Number(e.target.value))} className="w-full" /> +
+ {progressVal}% +
+ + +
+
+
+ ) : ( +
+
= 100 ? 'bg-signal-ok' : displayProgress > 0 ? 'bg-primary' : 'bg-muted'}`} + style={{ width: `${displayProgress}%` }} /> +
+ )} + {total > 0 &&

{done}/{total} tasks completate

} +
+ + {/* Tasks */} +
+

Tasks ({total})

+ {linkedTasks.length > 0 && ( +
+ {linkedTasks.map((t) => ( +
+ + {t.title} + {t.priority && t.priority !== 'normal' && ( + + {t.priority} + + )} +
+ ))} +
+ )} +
+ setTaskTitle(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && taskTitle.trim() && addTaskMut.mutate()} + className="flex-1 rounded-lg border bg-background px-3 py-1.5 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + +
+
+ + {/* Notes */} +
+

Note ({notes.length})

+ {notes.map((n) => ( +
+

{n.value}

+

{new Date(n.createdAt).toLocaleDateString('ro-RO', { day:'numeric', month:'short', hour:'2-digit', minute:'2-digit' })}

+
+ ))} +
+