diff --git a/.replit b/.replit index dddfbf4..b0a2b1f 100644 --- a/.replit +++ b/.replit @@ -19,7 +19,7 @@ localPort = 35345 externalPort = 3002 [[ports]] -localPort = 38369 +localPort = 36505 externalPort = 3001 [[ports]] diff --git a/client/src/components/ProjectTemplate.tsx b/client/src/components/ProjectTemplate.tsx index fce5f18..e8e28a9 100644 --- a/client/src/components/ProjectTemplate.tsx +++ b/client/src/components/ProjectTemplate.tsx @@ -8,8 +8,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Calendar } from '@/components/ui/calendar'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Plus, Calendar as CalendarIcon, Clock, Copy, Play, Tag, Edit, Trash2, Palette } from 'lucide-react'; +import { Plus, Calendar as CalendarIcon, Clock, Copy, Play, Tag, Edit, Trash2, Palette, FileText } from 'lucide-react'; import { Label } from '@shared/schema'; +import TaskDetailsModal from '@/components/TaskDetailsModal'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiRequest } from '@/lib/queryClient'; @@ -19,6 +20,13 @@ const getStartOfToday = () => { return new Date(today.getFullYear(), today.getMonth(), today.getDate()); }; +interface TimeEntry { + id: string; + date: Date; + timeSpent: number; // in minutes + description?: string; +} + interface ProjectTask { id: string; title: string; @@ -27,6 +35,9 @@ interface ProjectTask { status: 'todo' | 'inProgress' | 'done'; estimatedHours?: number; labelId?: string; + notes?: string; + timeTracked?: number; // in minutes + timeEntries?: TimeEntry[]; } interface Project { @@ -122,6 +133,10 @@ export default function ProjectTemplate({ const [labelName, setLabelName] = useState(''); const [labelColor, setLabelColor] = useState('#3B82F6'); + // Task details modal state + const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false); + const [selectedTaskForDetails, setSelectedTaskForDetails] = useState(null); + const queryClient = useQueryClient(); // Fetch labels @@ -362,6 +377,43 @@ export default function ProjectTemplate({ setIsEditingTask(true); }; + // Task details modal handlers + const handleOpenTaskDetails = (task: ProjectTask, isFromProject: boolean = false) => { + setSelectedTaskForDetails(task); + setIsTaskDetailsOpen(true); + }; + + const handleCloseTaskDetails = () => { + setSelectedTaskForDetails(null); + setIsTaskDetailsOpen(false); + }; + + const handleSaveTaskDetails = (updatedTask: ProjectTask) => { + // Update the task in projectTasks array + const taskIndex = projectTasks.findIndex(task => task.id === updatedTask.id); + if (taskIndex !== -1) { + const updatedTasks = [...projectTasks]; + updatedTasks[taskIndex] = updatedTask; + setProjectTasks(updatedTasks); + console.log('Task updated:', updatedTask.title, 'with notes and time tracking'); + } + + // If we're in edit project mode, also update the selected project + if (selectedProject) { + const updatedProject = { ...selectedProject }; + const projectTaskIndex = updatedProject.tasks.findIndex(task => task.id === updatedTask.id); + if (projectTaskIndex !== -1) { + updatedProject.tasks[projectTaskIndex] = updatedTask; + setSelectedProject(updatedProject); + + // Update the projects array + setProjects(projects.map(p => + p.id === selectedProject.id ? updatedProject : p + )); + } + } + }; + const handleSaveEditedProject = () => { if (selectedProject) { const updatedProject = { @@ -1206,7 +1258,11 @@ export default function ProjectTemplate({ ) : ( // Display Mode
-
+
handleOpenTaskDetails(task)} + data-testid={`clickable-task-${index}`} + >

{task.title}

{task.description && (

{task.description}

@@ -1223,22 +1279,53 @@ export default function ProjectTemplate({ {task.estimatedHours}h ) : null} + {task.timeTracked && task.timeTracked > 0 && ( + + {Math.floor(task.timeTracked / 60)}h {task.timeTracked % 60}m tracked + + )} + {task.notes && task.notes.trim() && ( + + + Notes + + )}
+ @@ -1422,6 +1509,15 @@ export default function ProjectTemplate({ )} + + {/* Task Details Modal */} +
); } \ No newline at end of file diff --git a/client/src/components/TaskDetailsModal.tsx b/client/src/components/TaskDetailsModal.tsx new file mode 100644 index 0000000..1599327 --- /dev/null +++ b/client/src/components/TaskDetailsModal.tsx @@ -0,0 +1,541 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Card } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar } from 'lucide-react'; + +interface ProjectTask { + id: string; + title: string; + description?: string; + priority: 'low' | 'medium' | 'high'; + status: 'todo' | 'inProgress' | 'done'; + estimatedHours?: number; + labelId?: string; + notes?: string; + timeTracked?: number; // in minutes + timeEntries?: TimeEntry[]; +} + +interface TimeEntry { + id: string; + date: Date; + timeSpent: number; // in minutes + description?: string; +} + +interface TaskDetailsModalProps { + isOpen: boolean; + onClose: () => void; + task: ProjectTask | null; + onSave: (updatedTask: ProjectTask) => void; + labels?: Array<{ id: string; name: string; color: string }>; +} + +export default function TaskDetailsModal({ + isOpen, + onClose, + task, + onSave, + labels = [] +}: TaskDetailsModalProps) { + // Notes state + const [notes, setNotes] = useState(''); + const [isEditingNotes, setIsEditingNotes] = useState(false); + + // Time reporting state + const [isAddingTime, setIsAddingTime] = useState(false); + const [hours, setHours] = useState(0); + const [minutes, setMinutes] = useState(0); + const [manualHours, setManualHours] = useState(''); + const [timeDescription, setTimeDescription] = useState(''); + const [activeTimeTab, setActiveTimeTab] = useState('clock'); + + // Time entries state + const [timeEntries, setTimeEntries] = useState([]); + + useEffect(() => { + if (task && isOpen) { + setNotes(task.notes || ''); + setTimeEntries(task.timeEntries || []); + // Reset time input state + setHours(0); + setMinutes(0); + setManualHours(''); + setTimeDescription(''); + setIsAddingTime(false); + setIsEditingNotes(false); + } + }, [task, isOpen]); + + const handleClose = () => { + setNotes(''); + setTimeEntries([]); + setIsEditingNotes(false); + setIsAddingTime(false); + setHours(0); + setMinutes(0); + setManualHours(''); + setTimeDescription(''); + onClose(); + }; + + const handleSaveNotes = () => { + if (!task) return; + + const updatedTask = { ...task, notes }; + onSave(updatedTask); + setIsEditingNotes(false); + }; + + const handleSaveTimeEntry = () => { + if (!task) return; + + let totalMinutes = 0; + + if (activeTimeTab === 'clock') { + totalMinutes = hours * 60 + minutes; + } else { + const parsedHours = parseFloat(manualHours) || 0; + totalMinutes = Math.round(parsedHours * 60); + } + + if (totalMinutes <= 0) return; + + const newTimeEntry: TimeEntry = { + id: Date.now().toString(), + date: new Date(), + timeSpent: totalMinutes, + description: timeDescription.trim() || undefined + }; + + const updatedTimeEntries = [...timeEntries, newTimeEntry]; + const updatedTimeTracked = (task.timeTracked || 0) + totalMinutes; + + const updatedTask = { + ...task, + timeEntries: updatedTimeEntries, + timeTracked: updatedTimeTracked + }; + + setTimeEntries(updatedTimeEntries); + onSave(updatedTask); + + // Reset time input + setHours(0); + setMinutes(0); + setManualHours(''); + setTimeDescription(''); + setIsAddingTime(false); + + console.log(`Time entry saved: ${totalMinutes} minutes for task: ${task.title}`); + }; + + const handleDeleteTimeEntry = (entryId: string) => { + if (!task) return; + + const entryToDelete = timeEntries.find(entry => entry.id === entryId); + if (!entryToDelete) return; + + const updatedTimeEntries = timeEntries.filter(entry => entry.id !== entryId); + const updatedTimeTracked = Math.max(0, (task.timeTracked || 0) - entryToDelete.timeSpent); + + const updatedTask = { + ...task, + timeEntries: updatedTimeEntries, + timeTracked: updatedTimeTracked + }; + + setTimeEntries(updatedTimeEntries); + onSave(updatedTask); + }; + + const formatTime = (h: number, m: number) => { + return `${h}h ${m}m`; + }; + + const formatMinutesToHours = (minutes: number) => { + const h = Math.floor(minutes / 60); + const m = minutes % 60; + return formatTime(h, m); + }; + + const getCurrentTimeDisplay = () => { + if (activeTimeTab === 'clock') { + return formatTime(hours, minutes); + } else { + const parsedHours = parseFloat(manualHours) || 0; + const h = Math.floor(parsedHours); + const m = Math.round((parsedHours - h) * 60); + return formatTime(h, m); + } + }; + + const getTotalTrackedTime = () => { + return timeEntries.reduce((total, entry) => total + entry.timeSpent, 0); + }; + + const getPriorityColor = (priority: string) => { + switch (priority) { + case 'high': return 'text-red-600 bg-red-50 border-red-200'; + case 'medium': return 'text-yellow-600 bg-yellow-50 border-yellow-200'; + case 'low': return 'text-green-600 bg-green-50 border-green-200'; + default: return 'text-gray-600 bg-gray-50 border-gray-200'; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'done': return 'text-green-600 bg-green-50 border-green-200'; + case 'inProgress': return 'text-blue-600 bg-blue-50 border-blue-200'; + case 'todo': return 'text-gray-600 bg-gray-50 border-gray-200'; + default: return 'text-gray-600 bg-gray-50 border-gray-200'; + } + }; + + if (!task) return null; + + return ( + + + + + + Task Details + + + +
+ {/* Task Information */} + +
+

+ {task.title} +

+ + {task.description && ( +

+ {task.description} +

+ )} + +
+ + {task.priority} priority + + + {task.status} + + {task.estimatedHours && ( + + {task.estimatedHours}h estimated + + )} + {getTotalTrackedTime() > 0 && ( + + {formatMinutesToHours(getTotalTrackedTime())} tracked + + )} +
+
+
+ + + + + + Notes + + + + Time Tracking + + + + {/* Notes Tab */} + + +
+

Task Notes

+ {!isEditingNotes && ( + + )} +
+ + {isEditingNotes ? ( +
+