diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 4dbdea4..cefa349 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -1,29 +1,42 @@ -import React, { useState, useEffect, useCallback } from 'react'; +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 } from '../utils/dateUtils'; +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 [showAllSessions, setShowAllSessions] = useState(false); + + 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'); + 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(); @@ -36,6 +49,18 @@ function History() { 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 }); @@ -47,6 +72,55 @@ function History() { } }; + // 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 (
@@ -60,75 +134,200 @@ function History() {

Session History

-
-

Sessions

- {sessions.length > 5 && ( - - )} + {/* Controls Bar */} +
+
+
+ Filter: + +
+
+ Show: + +
+
+
+ + {totalCount} session{totalCount !== 1 ? 's' : ''} total + + {isAuthenticated && ( + + )} +
+ {/* Session List */} {sessions.length === 0 ? (

No sessions found

) : (
- {(showAllSessions ? sessions : sessions.slice(0, 5)).map(session => ( -
+ {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 (
navigate(`/history/${session.id}`)} - className="p-4" + key={session.id} + className={`border rounded-lg transition ${ + selectMode && isActive + ? 'opacity-50 cursor-not-allowed border-gray-300 dark:border-gray-600' + : isSelected + ? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 cursor-pointer' + : 'border-gray-300 dark:border-gray-600 hover:border-indigo-400 dark:hover:border-indigo-500 cursor-pointer' + }`} + onClick={() => { + if (selectMode) { + if (!isActive) toggleSelection(session.id); + } else { + navigate(`/history/${session.id}`); + } + }} + onPointerDown={() => { + if (!isActive) handlePointerDown(session.id); + }} + onPointerUp={handlePointerUp} + onPointerLeave={handlePointerUp} > -
-
- - Session #{session.id} - - {session.is_active === 1 && ( - - Active - +
+
+ {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} +
+ )} +
- - {session.games_played} game{session.games_played !== 1 ? 's' : ''} -
-
- {formatLocalDate(session.created_at)} -
- {session.has_notes && session.notes_preview && ( -
- {session.notes_preview} + + {!selectMode && isAuthenticated && isActive && ( +
+
)}
+ ); + })} +
+ )} - {isAuthenticated && session.is_active === 1 && ( -
- -
- )} -
- ))} + {/* Multi-select Action Bar */} + {selectMode && selectedIds.size > 0 && ( +
+ + {selectedIds.size} selected + +
+ {filter !== 'archived' && ( + + )} + {filter !== 'default' && ( + + )} + +
)}
+ {/* End Session Modal */} {closingSession && ( )} + + {/* 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. +

+
+ + +
+
+
+ )}
); } diff --git a/frontend/src/pages/SessionDetail.jsx b/frontend/src/pages/SessionDetail.jsx index 235007e..0a41907 100644 --- a/frontend/src/pages/SessionDetail.jsx +++ b/frontend/src/pages/SessionDetail.jsx @@ -4,7 +4,7 @@ import Markdown from 'react-markdown'; import { useAuth } from '../context/AuthContext'; import { useToast } from '../components/Toast'; import api from '../api/axios'; -import { formatLocalDateTime, formatLocalTime } from '../utils/dateUtils'; +import { formatLocalDateTime, formatLocalTime, isSunday } from '../utils/dateUtils'; import PopularityBadge from '../components/PopularityBadge'; function SessionDetail() { @@ -94,6 +94,17 @@ function SessionDetail() { } }; + const handleArchive = async () => { + const action = session.archived === 1 ? 'unarchive' : 'archive'; + try { + await api.post(`/sessions/${id}/${action}`); + await loadSession(); + success(`Session ${action}d`); + } catch (err) { + showError(err.response?.data?.error || `Failed to ${action} session`); + } + }; + const handleCloseSession = async (sessionId, notes) => { try { await api.post(`/sessions/${sessionId}/close`, { notes }); @@ -155,6 +166,22 @@ function SessionDetail() { ← Back to History + {session.archived === 1 && ( +
+ + This session is archived + + {isAuthenticated && ( + + )} +
+ )} +
@@ -167,9 +194,17 @@ function SessionDetail() { 🟢 Active )} + {isSunday(session.created_at) && ( + + ☀ Game Night + + )}

{formatLocalDateTime(session.created_at)} + {isSunday(session.created_at) && ( + · Sunday + )} {' • '} {session.games_played} game{session.games_played !== 1 ? 's' : ''} played

@@ -209,12 +244,24 @@ function SessionDetail() { )} {isAuthenticated && session.is_active === 0 && ( - + <> + + + )}