import React, { useRef, useState } from 'react'; import { useAuth } from '../context/AuthContext'; import sessionService from '../services/sessionService'; const Sessions = () => { const { isAuthenticated, sessionData, refreshSessions, user, makeSessionActive, updateCurrentSessionEntry } = useAuth(); const [editingSession, setEditingSession] = useState(null); const [creatingNewSession, setCreatingNewSession] = useState(false); const [formError, setFormError] = useState(''); const [isSaving, setIsSaving] = useState(false); const [editForm, setEditForm] = useState({ date: '', dateDisplay: '', startTime: '', endTime: '', pauses: [], makeActive: false // New field for making session active }); const nativeDatePickerRef = useRef(null); const parseDateValue = (value, fallbackDate) => { if (!value) return null; if (value instanceof Date) return value; if (typeof value === 'string') { if (/^\d{2}:\d{2}(:\d{2})?$/.test(value) && fallbackDate) { const [h, m, s = '0'] = value.split(':').map(Number); const d = new Date(fallbackDate); d.setHours(h, m, s, 0); return d; } const d = new Date(value); if (!Number.isNaN(d.getTime())) return d; } return null; }; const formatDateTime = (value, fallbackDate = null) => { const date = parseDateValue(value, fallbackDate); if (!date) return 'N/A'; const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${day}/${month}/${year} ${hours}:${minutes}`; }; const formatDateForInput = (date) => { if (!date) return ''; const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const formatDateForDisplay = (dateValue) => { if (!dateValue) return ''; const [year, month, day] = dateValue.split('-'); if (!year || !month || !day) return ''; return `${day}/${month}/${year}`; }; const parseDateDisplay = (value) => { if (!value) return ''; const match = value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (!match) return ''; const [, day, month, year] = match; const paddedDay = String(day).padStart(2, '0'); const paddedMonth = String(month).padStart(2, '0'); const date = new Date(`${year}-${paddedMonth}-${paddedDay}T00:00:00`); if (Number.isNaN(date.getTime())) return ''; return `${year}-${paddedMonth}-${paddedDay}`; }; const normalizeTimeInput = (value) => { if (!value) return ''; const trimmed = value.trim().toUpperCase().replace(/\s+/g, ''); const twentyFourMatch = trimmed.match(/^([01]?\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/); if (twentyFourMatch) { return `${String(Number(twentyFourMatch[1])).padStart(2, '0')}:${twentyFourMatch[2]}`; } const twelveHourMatch = trimmed.match(/^(0?[1-9]|1[0-2]):([0-5]\d)(:[0-5]\d)?(AM|PM)$/); if (!twelveHourMatch) return ''; let hour = Number(twelveHourMatch[1]); const minute = twelveHourMatch[2]; const suffix = twelveHourMatch[4]; if (suffix === 'AM' && hour === 12) hour = 0; if (suffix === 'PM' && hour !== 12) hour += 12; return `${String(hour).padStart(2, '0')}:${minute}`; }; const formatDateOnly = (date) => { if (!date) return ''; return new Date(date).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }); }; const formatTimeForInput = (date) => { if (!date) return ''; const d = new Date(date); return d.toTimeString().slice(0, 5); // HH:mm }; const formatDuration = (milliseconds) => { if (!milliseconds) return '00:00:00'; const totalSeconds = Math.floor(milliseconds / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; }; const calculateWorkTime = (session) => { // Calculate total pause time let totalPauseTime = 0; if (session.pauses) { session.pauses.forEach(pause => { if (pause.end) { totalPauseTime += new Date(pause.end) - new Date(pause.start); } else if (session.endTime) { // If session has ended but pause hasn't, count pause until session end totalPauseTime += new Date(session.endTime) - new Date(pause.start); } }); } // Work time is total duration minus pause time return session.duration - totalPauseTime; }; const startEditing = (session) => { setFormError(''); setEditingSession(session); setEditForm({ id: session.id, date: formatDateForInput(session.startTime), dateDisplay: formatDateForDisplay(formatDateForInput(session.startTime)), startTime: formatTimeForInput(session.startTime), endTime: session.endTime ? formatTimeForInput(session.endTime) : '', pauses: (session.pauses || []).map((pause) => ({ start: formatTimeForInput(pause.start), end: pause.end ? formatTimeForInput(pause.end) : '' })), makeActive: false }); }; const startEditingCurrent = (session) => { setFormError(''); setEditingSession('current'); setEditForm({ id: 'current', date: formatDateForInput(session.startTime), dateDisplay: formatDateForDisplay(formatDateForInput(session.startTime)), startTime: formatTimeForInput(session.startTime), endTime: session.endTime ? formatTimeForInput(session.endTime) : '', pauses: (session.pauses || []).map((pause) => ({ start: formatTimeForInput(pause.start), end: pause.end ? formatTimeForInput(pause.end) : '' })), makeActive: false }); }; const startCreating = () => { setFormError(''); setCreatingNewSession(true); setEditingSession('new'); // Set default to today with default times const today = formatDateForInput(new Date()); setEditForm({ date: today, dateDisplay: formatDateForDisplay(today), startTime: '09:00', endTime: '17:00', pauses: [], makeActive: false }); }; const cancelEditing = () => { setFormError(''); setEditingSession(null); setCreatingNewSession(false); setEditForm({ date: '', dateDisplay: '', startTime: '', endTime: '', pauses: [], makeActive: false }); }; const combineDateAndTime = (date, time) => { if (!date || !time) return null; const normalizedTime = normalizeTimeInput(time); if (!normalizedTime) return null; return new Date(`${date}T${normalizedTime}`); }; const withTimeout = async (promise, timeoutMs, timeoutMessage) => { let timerId; try { const timeoutPromise = new Promise((_, reject) => { timerId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs); }); return await Promise.race([promise, timeoutPromise]); } finally { clearTimeout(timerId); } }; const getResolvedFormDate = () => { if (editForm.date) return editForm.date; return parseDateDisplay(editForm.dateDisplay); }; const saveChanges = async () => { if (isSaving) { return; } setIsSaving(true); try { setFormError(''); if (!user?.id) { setFormError('Your user session is not ready yet. Please reload and try again.'); return; } const resolvedDate = getResolvedFormDate(); if (!resolvedDate) { setFormError('Please provide a valid date (dd/mm/yyyy).'); return; } const toActiveSessionShape = (record) => ({ id: record.id, startTime: new Date(record.start_time ?? record.startTime), endTime: record.end_time ? new Date(record.end_time) : null, duration: record.duration_ms ?? record.duration ?? 0, userId: record.user_id ?? record.userId ?? user?.id, pauses: record.pauses || [] }); const normalizePauses = () => (editForm.pauses || []) .map((pause) => { if (!pause?.start) { return null; } const start = combineDateAndTime(resolvedDate, pause.start); if (!start) { return null; } const end = pause.end ? combineDateAndTime(resolvedDate, pause.end) : null; return { start: start.toISOString(), end: end ? end.toISOString() : null }; }) .filter(Boolean); const normalizePausesForCurrent = () => (editForm.pauses || []) .map((pause) => { if (!pause?.start) { return null; } const start = combineDateAndTime(resolvedDate, pause.start); if (!start) { return null; } const end = pause.end ? combineDateAndTime(resolvedDate, pause.end) : null; return { start, end }; }) .filter(Boolean); if (creatingNewSession) { // Create new session with date + time combination const startDateTime = combineDateAndTime(resolvedDate, editForm.startTime); const endDateTime = editForm.makeActive ? null : combineDateAndTime(resolvedDate, editForm.endTime); if (!startDateTime || (!editForm.makeActive && !endDateTime)) { setFormError('Please provide valid start/end times in HH:mm format.'); return; } if (!editForm.makeActive && endDateTime <= startDateTime) { setFormError('End time must be later than start time.'); return; } const newSessionData = { userId: user.id, startTime: startDateTime.toISOString(), endTime: endDateTime ? endDateTime.toISOString() : null, pauses: normalizePauses() }; const result = await withTimeout( sessionService.createSession(newSessionData), 30000, 'Request timed out while creating session. Please check your network connection and try again.' ); if (result.success) { await withTimeout( refreshSessions(), 30000, 'Request timed out while refreshing sessions. Please check your network connection and try again.' ); if (editForm.makeActive && result.data) { makeSessionActive(toActiveSessionShape(result.data)); } } } else if (editingSession === 'current') { const startDateTime = combineDateAndTime(resolvedDate, editForm.startTime); if (!startDateTime) { setFormError('Please provide a valid start time in HH:mm format.'); return; } const normalizedPausesForDb = normalizePauses(); updateCurrentSessionEntry({ startTime: startDateTime, endTime: null, pauses: normalizePausesForCurrent() }); if (sessionData.currentTimeEntry?.id && sessionData.currentTimeEntry.id !== 'current') { await withTimeout( sessionService.updateSession(sessionData.currentTimeEntry.id, { start_time: startDateTime.toISOString(), end_time: null, pauses: normalizedPausesForDb }), 30000, 'Request timed out while updating current session. Please check your network connection and try again.' ); await withTimeout( refreshSessions(), 15000, 'Request timed out while refreshing sessions after current-session update.' ); } cancelEditing(); return; } else if (editingSession && editingSession.id) { // Update existing session const startDateTime = combineDateAndTime(resolvedDate, editForm.startTime); const endDateTime = editForm.makeActive ? null : combineDateAndTime(resolvedDate, editForm.endTime); if (startDateTime && endDateTime && endDateTime <= startDateTime) { setFormError('End time must be later than start time.'); return; } const updateData = { start_time: startDateTime ? startDateTime.toISOString() : undefined, end_time: endDateTime ? endDateTime.toISOString() : null, pauses: normalizePauses() }; const result = await withTimeout( sessionService.updateSession(editForm.id, updateData), 30000, 'Request timed out while updating session. Please check your network connection and try again.' ); if (result.success) { await withTimeout( refreshSessions(), 30000, 'Request timed out while refreshing sessions. Please check your network connection and try again.' ); if (editForm.makeActive && result.data) { makeSessionActive(toActiveSessionShape(result.data)); } } } // Cancel editing after save cancelEditing(); } catch (error) { console.error('Error saving session:', error); setFormError(error?.message ? `Could not save session: ${error.message}` : 'Could not save session. Please verify date/time values and try again.'); } finally { setIsSaving(false); } }; const deleteSession = async () => { if (!editingSession || editingSession === 'current' || creatingNewSession || !editForm.id) { return; } const shouldDelete = window.confirm('Are you sure you want to delete this session?'); if (!shouldDelete) { return; } try { await sessionService.deleteSession(editForm.id); await refreshSessions(); cancelEditing(); } catch (error) { console.error('Error deleting session:', error); } }; const addPause = () => { setEditForm(prev => ({ ...prev, pauses: [...prev.pauses, { start: '', end: '' }] })); }; const updatePause = (index, field, value) => { setEditForm(prev => { const newPauses = [...prev.pauses]; newPauses[index] = { ...newPauses[index], [field]: value }; return { ...prev, pauses: newPauses }; }); }; const removePause = (index) => { setEditForm(prev => ({ ...prev, pauses: prev.pauses.filter((_, i) => i !== index) })); }; // Check if end time is in the future const isEndTimeInFuture = () => { if (!editForm.date || !editForm.endTime) return false; const endDateTime = combineDateAndTime(editForm.date, editForm.endTime); return endDateTime && endDateTime > new Date(); }; if (!isAuthenticated) { return
Loading...
; } return (

History

Session History

{(editingSession || creatingNewSession) && (

{creatingNewSession ? 'Create New Session' : editingSession === 'current' ? 'Edit Current Session' : 'Edit Session'}

{formError &&
{formError}
}
{ const nextDisplay = e.target.value; const parsedDate = parseDateDisplay(nextDisplay); setEditForm({ ...editForm, dateDisplay: nextDisplay, date: nextDisplay === '' ? '' : parsedDate || editForm.date }); }} /> { const nextDate = e.target.value; setEditForm({ ...editForm, date: nextDate, dateDisplay: formatDateForDisplay(nextDate) }); }} aria-label="Select date" />
setEditForm({...editForm, startTime: e.target.value})} />
{!editForm.makeActive && (
setEditForm({...editForm, endTime: e.target.value})} />
)} {/* Show "Make Active" checkbox if end time is in the future */} {(isEndTimeInFuture() || creatingNewSession) && (
)}

Pauses

{editForm.pauses.map((pause, index) => (
updatePause(index, 'start', e.target.value)} /> updatePause(index, 'end', e.target.value)} />
))}
{editingSession && editingSession !== 'current' && !creatingNewSession && ( )}
)}

Current Session

{sessionData.currentTimeEntry ? (
Start Time: {formatDateTime(sessionData.currentTimeEntry.startTime)}
Status: Active
{sessionData.currentTimeEntry.pauses && sessionData.currentTimeEntry.pauses.length > 0 && (
Pauses: {sessionData.currentTimeEntry.pauses.map((pause, index) => (
{formatDateTime(pause.start, sessionData.currentTimeEntry.startTime)} -{' '} {pause.end ? formatDateTime(pause.end, sessionData.currentTimeEntry.startTime) : 'Currently paused'}
))}
)}
) : (

No active session

)}

Past Sessions

{sessionData.sessions.filter((session) => session.endTime).length > 0 ? (
{[...sessionData.sessions] .filter((session) => session.endTime) .reverse() .map((session) => (
{editingSession === session ? ( // Editing view would go here
Editing...
) : ( // Display view
{formatDateOnly(session.startTime)} Completed
Start Time: {formatDateTime(session.startTime)}
End Time: {formatDateTime(session.endTime)}
Total Duration {formatDuration(session.duration)}
Pause Time {formatDuration( session.pauses?.reduce((total, pause) => { const start = parseDateValue(pause.start, session.startTime); const end = parseDateValue(pause.end, session.startTime); if (!start || !end) return total; const pauseDuration = end - start; return total + (Number.isFinite(pauseDuration) ? Math.max(pauseDuration, 0) : 0); }, 0) )}
Work Time {formatDuration(calculateWorkTime(session))}
)}
))}
) : (

No past sessions found

)}
); }; export default Sessions;