diff --git a/frontend/src/pages/TimeclockPage.tsx b/frontend/src/pages/TimeclockPage.tsx index 39dbe58..137ce73 100644 --- a/frontend/src/pages/TimeclockPage.tsx +++ b/frontend/src/pages/TimeclockPage.tsx @@ -1,24 +1,82 @@ import { useEffect, useState } from 'react'; -import { Clock, LogIn, LogOut } from 'lucide-react'; +import { + Clock, LogIn, LogOut, QrCode, Smartphone, MapPin, + Timer, CalendarDays, Users, TrendingUp, ChevronRight, + CheckCircle2, AlertCircle, MoreHorizontal, History +} from 'lucide-react'; import api from '../lib/api'; -import { PageHeader } from '../components/ui/LinearPrimitives'; +import { Card } from '../components/ui/card'; +import { cn } from '../lib/utils'; +import { motion, AnimatePresence } from 'framer-motion'; -export default function TimeclockPage() { - const [logs, setLogs] = useState([]); +// Types +interface TimeLog { + id: string; + startTime: string; + endTime?: string; + activityType: string; + method?: 'manual' | 'qr' | 'nfc' | 'gps'; + location?: string; +} + +interface ShiftStats { + hoursToday: number; + hoursThisWeek: number; + shiftsThisWeek: number; + averageShift: number; +} + +// Clock-in method types (scaffolded) +type ClockMethod = 'manual' | 'qr' | 'nfc' | 'gps'; + +const CLOCK_METHODS: { id: ClockMethod; label: string; icon: any; available: boolean; description: string }[] = [ + { id: 'manual', label: 'Manual', icon: Clock, available: true, description: 'Standard clock in/out' }, + { id: 'qr', label: 'QR Code', icon: QrCode, available: false, description: 'Scan facility QR code' }, + { id: 'nfc', label: 'NFC Tap', icon: Smartphone, available: false, description: 'Tap your badge' }, + { id: 'gps', label: 'Location', icon: MapPin, available: false, description: 'Verify by location' }, +]; + +export default function TimeClockPage() { + const [logs, setLogs] = useState([]); const [status, setStatus] = useState<'CLOCKED_OUT' | 'CLOCKED_IN'>('CLOCKED_OUT'); const [loading, setLoading] = useState(true); + const [clockLoading, setClockLoading] = useState(false); + const [activeMethod, setActiveMethod] = useState('manual'); + const [currentSession, setCurrentSession] = useState(null); + const [elapsedTime, setElapsedTime] = useState(0); + + // Stats (mock - would come from API) + const stats: ShiftStats = { + hoursToday: currentSession ? elapsedTime / 3600 : 0, + hoursThisWeek: 32.5, + shiftsThisWeek: 4, + averageShift: 8.1 + }; useEffect(() => { fetchLogs(); }, []); + // Timer for active session + useEffect(() => { + if (currentSession && !currentSession.endTime) { + const interval = setInterval(() => { + const start = new Date(currentSession.startTime).getTime(); + const now = Date.now(); + setElapsedTime(Math.floor((now - start) / 1000)); + }, 1000); + return () => clearInterval(interval); + } + }, [currentSession]); + const fetchLogs = async () => { setLoading(true); try { const { data } = await api.get('/timeclock/logs'); setLogs(data); - const active = data.find((l: any) => !l.endTime); + const active = data.find((l: TimeLog) => !l.endTime); setStatus(active ? 'CLOCKED_IN' : 'CLOCKED_OUT'); + setCurrentSession(active || null); } catch (e) { console.error(e); } finally { @@ -27,117 +85,292 @@ export default function TimeclockPage() { }; const handleClock = async (action: 'in' | 'out') => { + setClockLoading(true); try { if (action === 'in') { - await api.post('/timeclock/clock-in', { activityType: 'General' }); + await api.post('/timeclock/clock-in', { + activityType: 'General', + method: activeMethod + }); } else { await api.post('/timeclock/clock-out', {}); } await fetchLogs(); } catch (e: any) { - alert(e.response?.data?.message || 'Error clocking'); + console.error(e.response?.data?.message || 'Error clocking'); + } finally { + setClockLoading(false); } }; + const formatDuration = (seconds: number) => { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; + }; + + const formatHours = (hours: number) => hours.toFixed(1); + return ( -
- - - {/* Status Card */} -
-
- - Current Status - -
- {status === 'CLOCKED_IN' ? 'Clocked In' : 'Clocked Out'} +
+ {/* Header */} +
+
+

+ Shift Tracking +

+
+ + {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} + +
+
+ {status === 'CLOCKED_IN' ? 'On Shift' : 'Off Shift'} +
- -
- - -
- {/* Logs Table */} -
-
- -

Recent Logs

+ {/* Stats Grid */} +
+ + + + +
+ +
+ {/* Left: Clock Controls */} +
+ {/* Active Session Timer */} + + {status === 'CLOCKED_IN' && currentSession ? ( + +

+ Current Session +

+
+ {formatDuration(elapsedTime)} +
+

+ Started {new Date(currentSession.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +

+
+ ) : ( +
+

+ Ready to Clock In +

+
+ 00:00:00 +
+

+ Select a method below +

+
+ )} +
+ + {/* Clock Method Selector */} + +

+ Clock-In Method +

+
+ {CLOCK_METHODS.map(method => ( + + ))} +
+
+ + {/* Clock Action Button */} +
- {loading ? ( -
-
-
- ) : logs.length === 0 ? ( -
- No time logs yet -
- ) : ( -
- {logs.slice(0, 10).map(log => ( -
-
-
- {new Date(log.startTime).toLocaleDateString()} -
-
- {log.activityType} -
-
-
-
- {new Date(log.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - {' → '} - {log.endTime - ? new Date(log.endTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - : Active - } -
-
+ {/* Right: History */} +
+ +
+
+ +

Recent Shifts

- ))} -
- )} + +
+ + {loading ? ( +
+
+
+ ) : logs.length === 0 ? ( +
+ +

No shifts recorded yet

+
+ ) : ( +
+ + {logs.slice(0, 8).map((log, idx) => ( + +
+
+ {!log.endTime ? : } +
+
+

+ {new Date(log.startTime).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} +

+

+ {log.activityType} + {log.method && log.method !== 'manual' && ( + • {log.method.toUpperCase()} + )} +

+
+
+
+

+ {new Date(log.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {' → '} + {log.endTime + ? new Date(log.endTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : Active + } +

+ {log.endTime && ( +

+ {((new Date(log.endTime).getTime() - new Date(log.startTime).getTime()) / 3600000).toFixed(1)}h +

+ )} +
+ +
+ ))} +
+
+ )} + +
); } + +function StatCard({ label, value, icon: Icon, active = false }: { label: string; value: string; icon: any; active?: boolean }) { + return ( + +
+
+

{label}

+

+ {value} +

+
+
+ +
+
+
+ ); +}