import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../components/Toast';
import api from '../api/axios';
import { formatLocalDate, isSunday } from '../utils/dateUtils';
function History() {
const { isAuthenticated } = useAuth();
const { error, success } = useToast();
const navigate = useNavigate();
const [sessions, setSessions] = useState([]);
const [loading, setLoading] = useState(true);
const [totalCount, setTotalCount] = useState(0);
const [closingSession, setClosingSession] = useState(null);
const [filter, setFilter] = useState(() => localStorage.getItem('history-filter') || 'default');
const [limit, setLimit] = useState(() => localStorage.getItem('history-show-limit') || '5');
const [selectMode, setSelectMode] = useState(false);
const [selectedIds, setSelectedIds] = useState(new Set());
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
const longPressTimer = useRef(null);
const loadSessions = useCallback(async () => {
try {
const response = await api.get('/sessions', {
params: { filter, limit }
});
setSessions(response.data);
setTotalCount(parseInt(response.headers['x-total-count'] || '0', 10));
} catch (err) {
console.error('Failed to load sessions', err);
} finally {
setLoading(false);
}
}, [filter, limit]);
useEffect(() => {
loadSessions();
}, [loadSessions]);
useEffect(() => {
const interval = setInterval(() => {
loadSessions();
}, 3000);
return () => clearInterval(interval);
}, [loadSessions]);
const handleFilterChange = (newFilter) => {
setFilter(newFilter);
localStorage.setItem('history-filter', newFilter);
setSelectedIds(new Set());
};
const handleLimitChange = (newLimit) => {
setLimit(newLimit);
localStorage.setItem('history-show-limit', newLimit);
setSelectedIds(new Set());
};
const handleCloseSession = async (sessionId, notes) => {
try {
await api.post(`/sessions/${sessionId}/close`, { notes });
await loadSessions();
setClosingSession(null);
success('Session ended successfully');
} catch (err) {
error('Failed to close session');
}
};
// Multi-select handlers
const toggleSelection = (sessionId) => {
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(sessionId)) {
next.delete(sessionId);
} else {
next.add(sessionId);
}
return next;
});
};
const exitSelectMode = () => {
setSelectMode(false);
setSelectedIds(new Set());
setShowBulkDeleteConfirm(false);
};
const handlePointerDown = (sessionId) => {
if (!isAuthenticated || selectMode) return;
longPressTimer.current = setTimeout(() => {
setSelectMode(true);
setSelectedIds(new Set([sessionId]));
}, 500);
};
const handlePointerUp = () => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current);
longPressTimer.current = null;
}
};
const handleBulkAction = async (action) => {
try {
await api.post('/sessions/bulk', {
action,
ids: Array.from(selectedIds)
});
success(`${selectedIds.size} session${selectedIds.size !== 1 ? 's' : ''} ${action}d`);
setSelectedIds(new Set());
setShowBulkDeleteConfirm(false);
await loadSessions();
} catch (err) {
error(err.response?.data?.error || `Failed to ${action} sessions`);
}
};
if (loading) {
return (
);
}
return (
Session History
{/* Controls Bar */}
Filter:
handleFilterChange(e.target.value)}
className="px-2 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 cursor-pointer"
>
Sessions
Archived
All
Show:
handleLimitChange(e.target.value)}
className="px-2 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 cursor-pointer"
>
5
10
25
50
All
{totalCount} session{totalCount !== 1 ? 's' : ''} total
{isAuthenticated && (
setSelectMode(true)}
className={`px-3 py-1.5 rounded text-sm font-medium transition ${
selectMode
? 'bg-indigo-600 dark:bg-indigo-700 text-white hover:bg-indigo-700 dark:hover:bg-indigo-800'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`}
>
{selectMode ? 'โ Done' : 'Select'}
)}
{/* Session List */}
{sessions.length === 0 ? (
No sessions found
) : (
{sessions.map(session => {
const isActive = session.is_active === 1;
const isSelected = selectedIds.has(session.id);
const isSundaySession = isSunday(session.created_at);
const isArchived = session.archived === 1;
const canSelect = selectMode && !isActive;
return (
{
if (selectMode) {
if (!isActive) toggleSelection(session.id);
} else {
navigate(`/history/${session.id}`);
}
}}
onPointerDown={() => {
if (!isActive) handlePointerDown(session.id);
}}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
>
{selectMode && (
{isSelected && (
โ
)}
)}
Session #{session.id}
{isActive && (
Active
)}
{isSundaySession && (
โ Game Night
)}
{isArchived && (filter === 'all' || filter === 'archived') && (
Archived
)}
{session.games_played} game{session.games_played !== 1 ? 's' : ''}
{formatLocalDate(session.created_at)}
{isSundaySession && (
ยท Sunday
)}
{session.has_notes && session.notes_preview && (
{session.notes_preview}
)}
{!selectMode && isAuthenticated && isActive && (
{
e.stopPropagation();
setClosingSession(session.id);
}}
className="w-full bg-orange-600 dark:bg-orange-700 text-white px-4 py-2 rounded text-sm hover:bg-orange-700 dark:hover:bg-orange-800 transition"
>
End Session
)}
);
})}
)}
{/* Multi-select Action Bar */}
{selectMode && selectedIds.size > 0 && (
{selectedIds.size} selected
{filter !== 'archived' && (
handleBulkAction('archive')}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm hover:bg-indigo-700 transition"
>
Archive
)}
{filter !== 'default' && (
handleBulkAction('unarchive')}
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 transition"
>
Unarchive
)}
setShowBulkDeleteConfirm(true)}
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700 transition"
>
Delete
)}
{/* End Session Modal */}
{closingSession && (
setClosingSession(null)}
onConfirm={handleCloseSession}
/>
)}
{/* Bulk Delete Confirmation Modal */}
{showBulkDeleteConfirm && (
Delete {selectedIds.size} Session{selectedIds.size !== 1 ? 's' : ''}?
This will permanently delete {selectedIds.size} session{selectedIds.size !== 1 ? 's' : ''} and all associated games and chat logs. This action cannot be undone.
handleBulkAction('delete')}
className="flex-1 bg-red-600 dark:bg-red-700 text-white py-3 rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition font-semibold"
>
Delete Permanently
setShowBulkDeleteConfirm(false)}
className="flex-1 bg-gray-600 dark:bg-gray-700 text-white py-3 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-800 transition"
>
Cancel
)}
);
}
function EndSessionModal({ sessionId, onClose, onConfirm }) {
const [notes, setNotes] = useState('');
return (
End Session #{sessionId}
Session Notes (optional)
onConfirm(sessionId, notes)}
className="flex-1 bg-orange-600 dark:bg-orange-700 text-white py-3 rounded-lg hover:bg-orange-700 dark:hover:bg-orange-800 transition"
>
End Session
Cancel
);
}
export default History;