import { useState, useEffect } from "react"; import Layout from "@/components/Layout"; import SEO from "@/components/SEO"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Clock, Play, Square, Plus, Loader2, Calendar, Timer, DollarSign, Trash2, Edit, } from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { aethexToast } from "@/lib/aethex-toast"; interface Project { id: string; name: string; } interface TimeEntry { id: string; description: string; date: string; start_time?: string; end_time?: string; duration_minutes: number; is_billable: boolean; status: string; notes?: string; project?: Project; task?: { id: string; title: string }; } interface Stats { totalHours: number; billableHours: number; entriesCount: number; avgHoursPerDay: number; } const formatDuration = (minutes: number) => { const hours = Math.floor(minutes / 60); const mins = minutes % 60; if (hours === 0) return `${mins}m`; return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; }; const formatTime = (time?: string) => { if (!time) return "-"; return time.substring(0, 5); }; export default function StaffTimeTracking() { const { session } = useAuth(); const [entries, setEntries] = useState([]); const [projects, setProjects] = useState([]); const [stats, setStats] = useState({ totalHours: 0, billableHours: 0, entriesCount: 0, avgHoursPerDay: 0 }); const [loading, setLoading] = useState(true); const [view, setView] = useState("week"); const [activeTimer, setActiveTimer] = useState(null); // Dialog states const [createDialog, setCreateDialog] = useState(false); const [editEntry, setEditEntry] = useState(null); // Form state const [newEntry, setNewEntry] = useState({ project_id: "", description: "", date: new Date().toISOString().split("T")[0], start_time: "", end_time: "", duration_minutes: 0, is_billable: true, notes: "" }); useEffect(() => { if (session?.access_token) { fetchEntries(); } }, [session?.access_token, view]); // Check for running timer useEffect(() => { const running = entries.find(e => e.start_time && !e.end_time && e.duration_minutes === 0); setActiveTimer(running || null); }, [entries]); const fetchEntries = async () => { try { const res = await fetch(`/api/staff/time-tracking?view=${view}`, { headers: { Authorization: `Bearer ${session?.access_token}` }, }); const data = await res.json(); if (res.ok) { setEntries(data.entries || []); setProjects(data.projects || []); setStats(data.stats || { totalHours: 0, billableHours: 0, entriesCount: 0, avgHoursPerDay: 0 }); } } catch (err) { aethexToast.error("Failed to load time entries"); } finally { setLoading(false); } }; const createEntry = async () => { if (!newEntry.description && !newEntry.project_id) { aethexToast.error("Please add a description or project"); return; } // Calculate duration from times if provided let duration = newEntry.duration_minutes; if (newEntry.start_time && newEntry.end_time) { const [sh, sm] = newEntry.start_time.split(":").map(Number); const [eh, em] = newEntry.end_time.split(":").map(Number); duration = (eh * 60 + em) - (sh * 60 + sm); } try { const res = await fetch("/api/staff/time-tracking", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session?.access_token}`, }, body: JSON.stringify({ action: "create_entry", ...newEntry, duration_minutes: duration }), }); if (res.ok) { aethexToast.success("Time entry created!"); setCreateDialog(false); resetForm(); fetchEntries(); } } catch (err) { aethexToast.error("Failed to create entry"); } }; const startTimer = async () => { try { const res = await fetch("/api/staff/time-tracking", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session?.access_token}`, }, body: JSON.stringify({ action: "start_timer", description: "Time tracking" }), }); if (res.ok) { aethexToast.success("Timer started!"); fetchEntries(); } } catch (err) { aethexToast.error("Failed to start timer"); } }; const stopTimer = async () => { if (!activeTimer) return; try { const res = await fetch("/api/staff/time-tracking", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session?.access_token}`, }, body: JSON.stringify({ action: "stop_timer", entry_id: activeTimer.id }), }); if (res.ok) { aethexToast.success("Timer stopped!"); fetchEntries(); } } catch (err) { aethexToast.error("Failed to stop timer"); } }; const deleteEntry = async (entryId: string) => { if (!confirm("Delete this time entry?")) return; try { const res = await fetch(`/api/staff/time-tracking?id=${entryId}`, { method: "DELETE", headers: { Authorization: `Bearer ${session?.access_token}` }, }); if (res.ok) { aethexToast.success("Entry deleted"); fetchEntries(); } } catch (err) { aethexToast.error("Failed to delete entry"); } }; const resetForm = () => { setNewEntry({ project_id: "", description: "", date: new Date().toISOString().split("T")[0], start_time: "", end_time: "", duration_minutes: 0, is_billable: true, notes: "" }); }; // Group entries by date const groupedEntries = entries.reduce((groups, entry) => { const date = entry.date; if (!groups[date]) groups[date] = []; groups[date].push(entry); return groups; }, {} as Record); if (loading) { return (
); } return (
{/* Header */}

Time Tracking

Track your work hours and projects

{activeTimer ? ( ) : ( )}
{/* Stats */}

Total Hours

{stats.totalHours}

Billable

{stats.billableHours}h

Entries

{stats.entriesCount}

Avg/Day

{stats.avgHoursPerDay}h

{/* View Toggle */}
{["week", "month", "all"].map((v) => ( ))}
{/* Active Timer Banner */} {activeTimer && (

Timer Running

Started at {formatTime(activeTimer.start_time)} • {activeTimer.description}

)} {/* Time Entries by Date */}
{Object.entries(groupedEntries).map(([date, dayEntries]) => (

{new Date(date).toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })}

{formatDuration(dayEntries.reduce((sum, e) => sum + e.duration_minutes, 0))}
{dayEntries.map((entry) => (

{formatDuration(entry.duration_minutes)}

{formatTime(entry.start_time)} - {formatTime(entry.end_time)}

{entry.description || "No description"}

{entry.project && ( {entry.project.name} )} {entry.is_billable && ( Billable )}
{entry.status === "draft" && ( )}
))}
))}
{entries.length === 0 && (

No time entries for this period

)}
{/* Create Entry Dialog */} Add Time Entry
setNewEntry({ ...newEntry, description: e.target.value })} className="bg-slate-700 border-slate-600 text-slate-100" />
setNewEntry({ ...newEntry, date: e.target.value })} className="bg-slate-700 border-slate-600 text-slate-100" />
setNewEntry({ ...newEntry, start_time: e.target.value })} className="bg-slate-700 border-slate-600 text-slate-100" />
setNewEntry({ ...newEntry, end_time: e.target.value })} className="bg-slate-700 border-slate-600 text-slate-100" />
setNewEntry({ ...newEntry, duration_minutes: parseInt(e.target.value) || 0 })} className="bg-slate-700 border-slate-600 text-slate-100" />