diff --git a/src/app/dashboard/focus/page.tsx b/src/app/dashboard/focus/page.tsx new file mode 100644 index 0000000..947e5ec --- /dev/null +++ b/src/app/dashboard/focus/page.tsx @@ -0,0 +1,178 @@ +'use client'; + +import { useEffect, useRef, useState, useCallback } 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; priority: string | null; tags: string[]; } + +const PRESETS = [ + { label: '25 min', work: 25, break: 5 }, + { label: '50 min', work: 50, break: 10 }, + { label: '90 min', work: 90, break: 15 }, +]; + +export default function FocusSessionPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [preset, setPreset] = useState(PRESETS[0]); + const [mode, setMode] = useState<'idle' | 'work' | 'break'>('idle'); + const [secsLeft, setSecsLeft] = useState(preset.work * 60); + const [selectedTask, setSelectedTask] = useState(''); + const [completedSessions, setCompletedSessions] = useState(0); + const intervalRef = useRef | null>(null); + + const { data: tasks = [] } = useQuery({ + queryKey: ['focus-tasks', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const activeTasks = tasks.filter((t) => !['completed', 'cancelled'].includes(t.status)); + const urgentTasks = activeTasks.filter((t) => t.priority === 'urgent' || t.priority === 'high'); + + const logMut = useMutation({ + mutationFn: (taskTitle: string) => apiFetch('/v1/observations', { tenantId, method: 'POST', body: { + metric: 'focus-session', value: `${preset.work}`, unit: 'min', + subjectType: 'productivity', confidence: 1, + source: taskTitle || undefined, + observedAt: new Date().toISOString(), + }}), + onSuccess: () => qc.invalidateQueries({ queryKey: ['focus-obs', tenantId] }), + }); + + const start = useCallback(() => { + setMode('work'); + setSecsLeft(preset.work * 60); + }, [preset]); + + const stop = useCallback(() => { + if (intervalRef.current) clearInterval(intervalRef.current); + setMode('idle'); + setSecsLeft(preset.work * 60); + }, []); + + useEffect(() => { + if (mode === 'idle') { if (intervalRef.current) clearInterval(intervalRef.current); return; } + intervalRef.current = setInterval(() => { + setSecsLeft((s) => { + if (s <= 1) { + if (mode === 'work') { + setCompletedSessions((c) => c + 1); + logMut.mutate(selectedTask); + setMode('break'); + return preset.break * 60; + } else { + setMode('idle'); + return preset.work * 60; + } + } + return s - 1; + }); + }, 1000); + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; + }, [mode, preset, selectedTask]); + + useEffect(() => { + if (mode === 'idle') setSecsLeft(preset.work * 60); + }, [preset]); + + const mins = Math.floor(secsLeft / 60).toString().padStart(2, '0'); + const secs = (secsLeft % 60).toString().padStart(2, '0'); + const totalSecs = (mode === 'work' ? preset.work : preset.break) * 60; + const progressPct = mode === 'idle' ? 0 : ((totalSecs - secsLeft) / totalSecs) * 100; + + const r = 90; + const circ = 2 * Math.PI * r; + const dash = circ * (1 - progressPct / 100); + + return ( +
+
+

Focus Session

+

Timer de lucru focalizat cu logare automată.

+
+ + {/* Preset selection */} +
+ {PRESETS.map((p) => ( + + ))} +
+ + {/* Task picker */} +
+ + +
+ + {/* Timer circle */} +
+
+ + + + +
+

{mins}:{secs}

+

+ {mode === 'idle' ? 'Ready' : mode === 'work' ? 'Focus' : 'Pauză ☕'} +

+
+
+ +
+ {mode === 'idle' ? ( + + ) : ( + + )} +
+ + {completedSessions > 0 && ( +
+ {Array.from({ length: completedSessions }, (_, i) => ( + 🍅 + ))} +

{completedSessions} sesiune{completedSessions > 1 ? 'i' : ''} azi

+
+ )} +
+ +
+

+ Sesiunile completate se loghează automat ca observații (focus-session) în CEO OS. +

+
+
+ ); +}