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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { cn } from "@/lib/utils"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useQuery } from '@tanstack/react-query'; import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar as CalendarIcon, Tag, Link2, Check, LayoutList, Trash2, ArrowRight, CheckCircle2 } from 'lucide-react'; import { Calendar } from '@/components/ui/calendar'; import { format } from 'date-fns'; import { de, enUS } from 'date-fns/locale'; import { Task, Label } from '@shared/schema'; import { useTranslation } from 'react-i18next'; interface TimeEntry { id: string; date: Date; timeSpent: number; // in minutes description?: string; } interface TaskDetailsModalProps { isOpen: boolean; onClose: () => void; task: Task | null; onSave: (updatedTask: Task) => void; labels?: Label[]; onNavigate?: (taskId: string) => void; } export default function TaskDetailsModal({ isOpen, onClose, task, onSave, labels = [], onNavigate }: TaskDetailsModalProps) { const { t, i18n } = useTranslation(); const currentLocale = i18n.language === 'de' ? de : enUS; // Edit form state const [editedTitle, setEditedTitle] = useState(''); const [editedDescription, setEditedDescription] = useState(''); const [editedStatus, setEditedStatus] = useState<'todo' | 'inProgress' | 'done'>('todo'); const [editedPriority, setEditedPriority] = useState<'low' | 'medium' | 'high'>('medium'); const [editedLabelId, setEditedLabelId] = useState(null); const [editedDueDate, setEditedDueDate] = useState(''); const [editedStartDate, setEditedStartDate] = useState(''); const [editedEstimatedDuration, setEditedEstimatedDuration] = useState(undefined); const [editedDependencies, setEditedDependencies] = useState([]); const [isDependenciesOpen, setIsDependenciesOpen] = useState(false); // Subtasks state const [newSubtaskTitle, setNewSubtaskTitle] = useState(''); const [isCreatingSubtask, setIsCreatingSubtask] = useState(false); const [isScheduling, setIsScheduling] = useState(false); const { data: tasksData } = useQuery({ queryKey: ['/api/tasks'], }); const tasks = tasksData ?? []; // 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'); // Mark as done flow state const [showTimeRequiredDialog, setShowTimeRequiredDialog] = useState(false); const [markDoneHours, setMarkDoneHours] = useState(0); const [markDoneMinutes, setMarkDoneMinutes] = useState(0); const [markDoneManualHours, setMarkDoneManualHours] = useState(''); const [markDoneTimeTab, setMarkDoneTimeTab] = useState('clock'); // Time entries state - simulate from timeTracked for display const [timeEntries, setTimeEntries] = useState([]); useEffect(() => { if (task && isOpen) { // Initialize edit form with task data setEditedTitle(task.title); setEditedDescription(task.description || ''); setEditedStatus(task.status as 'todo' | 'inProgress' | 'done'); setEditedPriority(task.priority as 'low' | 'medium' | 'high'); setEditedLabelId(task.labelId || null); setEditedDueDate(task.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : ''); setEditedStartDate(task.startDate ? new Date(task.startDate).toISOString().split('T')[0] : ''); setEditedEstimatedDuration(task.estimatedDuration || undefined); setEditedDependencies(task.dependencies || []); setNotes(task.notes || ''); // Create a single time entry from timeTracked for display purposes if (task.timeTracked > 0) { setTimeEntries([{ id: 'tracked-time', date: new Date(), timeSpent: task.timeTracked, description: t('taskDetails.previouslyTrackedTime') }]); } else { setTimeEntries([]); } // 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(''); setShowTimeRequiredDialog(false); setMarkDoneHours(0); setMarkDoneMinutes(0); setMarkDoneManualHours(''); onClose(); }; // Handle Mark as Done button click const handleMarkAsDone = () => { if (!task) return; // Check if time has been tracked if (!task.timeTracked || task.timeTracked === 0) { // Show dialog to enter time first setShowTimeRequiredDialog(true); setMarkDoneHours(0); setMarkDoneMinutes(0); setMarkDoneManualHours(''); setMarkDoneTimeTab('clock'); } else { // Time already tracked, mark as done directly completeTaskAsDone(); } }; // Complete the task as done const completeTaskAsDone = () => { if (!task) return; const updatedTask: Task = { ...task, title: editedTitle, description: editedDescription || null, status: 'done', priority: editedPriority, labelId: editedLabelId, dueDate: editedDueDate ? new Date(editedDueDate) : null, startDate: editedStartDate ? new Date(editedStartDate) : null, estimatedDuration: editedEstimatedDuration ?? null, dependencies: editedDependencies }; onSave(updatedTask); handleClose(); }; // Handle saving time and then marking as done const handleSaveTimeAndMarkDone = () => { if (!task) return; let totalMinutes = 0; if (markDoneTimeTab === 'clock') { totalMinutes = markDoneHours * 60 + markDoneMinutes; } else { const parsedHours = parseFloat(markDoneManualHours) || 0; totalMinutes = Math.round(parsedHours * 60); } if (totalMinutes <= 0) return; // Create updated task with time and done status const updatedTask: Task = { ...task, title: editedTitle, description: editedDescription || null, status: 'done', priority: editedPriority, labelId: editedLabelId, dueDate: editedDueDate ? new Date(editedDueDate) : null, startDate: editedStartDate ? new Date(editedStartDate) : null, estimatedDuration: editedEstimatedDuration ?? null, dependencies: editedDependencies, timeTracked: (task.timeTracked || 0) + totalMinutes }; onSave(updatedTask); setShowTimeRequiredDialog(false); handleClose(); }; const handleSaveTaskEdit = () => { if (!task) return; const updatedTask: Task = { ...task, title: editedTitle, description: editedDescription || null, status: editedStatus, priority: editedPriority, labelId: editedLabelId, dueDate: editedDueDate ? new Date(editedDueDate) : null, startDate: editedStartDate ? new Date(editedStartDate) : null, estimatedDuration: editedEstimatedDuration ?? null, dependencies: editedDependencies }; onSave(updatedTask); }; 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, 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 handleCreateSubtask = async () => { if (!task || !newSubtaskTitle.trim()) return; try { setIsCreatingSubtask(true); const subtaskData = { title: newSubtaskTitle, description: '', status: 'todo', priority: 'medium', parentTaskId: task.id, dueDate: null, estimatedDuration: null }; await apiRequest('POST', '/api/tasks', subtaskData); await queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); setNewSubtaskTitle(''); // Switch to the newly created task? Or just show it in the list. } catch (error) { console.error('Failed to create subtask:', error); } finally { setIsCreatingSubtask(false); } }; const handleAutoSchedule = async () => { if (!task) return; setIsScheduling(true); try { const res = await apiRequest('POST', `/api/tasks/${task.id}/schedule`, {}); const data = await res.json(); if (data.success && data.scheduledDate) { setEditedDueDate(new Date(data.scheduledDate).toISOString().split('T')[0]); // Also update the local task object immediately for smoother UX, or let queryClient invalidate await queryClient.invalidateQueries({ queryKey: ['/api/tasks'] }); } else { console.error("Scheduling failed: ", data.error); // Could add a toast here ideally } } catch (error) { console.error("Auto-schedule error", error); } finally { setIsScheduling(false); } }; const subtasks = tasks.filter(t => t.parentTaskId === task?.id); const handleDeleteTimeEntry = (entryId: string) => { if (!task || entryId === 'tracked-time') { // Don't allow deleting the main tracked time entry 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, 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 task?.timeTracked || 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; const parentTask = task.parentTaskId ? tasks.find(t => String(t.id) === String(task.parentTaskId)) : null; const isSubtask = !!task.parentTaskId; return ( <> {t('taskDetails.title')}
{/* Task Information */}
{parentTask && (
onNavigate?.(String(parentTask.id))} > {t('taskDetails.subtaskOf')} {parentTask.title}
)}

{task.title}

{task.description && (

{task.description}

)}
{t(`taskDetails.priorityValue.${task.priority}`)} {t(`taskDetails.statusValue.${task.status}`)} {getTotalTrackedTime() > 0 && ( {formatMinutesToHours(getTotalTrackedTime())} {t('taskDetails.tracked')} )}
{t('taskDetails.editTab')} {t('taskDetails.notesTab')} {t('taskDetails.subtasksTab')} {t('taskDetails.timeTab')} {/* Subtasks Tab */}

{t('taskDetails.subtasks')}

{!isSubtask ? (
setNewSubtaskTitle(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleCreateSubtask(); }} />
) : (
{t('taskDetails.hierarchyRestriction')}
)}
{subtasks.length === 0 ? (
{t('taskDetails.noSubtasks')}
) : ( subtasks.map(subtask => (
onNavigate?.(String(subtask.id))} >
{t(`taskDetails.statusValue.${subtask.status}`)} {subtask.title}
)) )}
{/* Edit Tab */}

{t('taskDetails.editTask')}

{/* Title */}
setEditedTitle(e.target.value)} placeholder={t('taskDetails.titlePlaceholder')} data-testid="input-task-title" />
{/* Description */}