Improve task interaction and display within the app

Introduces a TaskDetailsModal and integrates task click handlers into CalendarView and KanbanBoard components. Updates TaskDetailsModal to display and manage time tracking, and refactors prop types for task data.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/P2PZNJ9
This commit is contained in:
paul-nothaft
2025-09-11 22:13:15 +00:00
parent 717c1b5ff5
commit d69c81185b
5 changed files with 80 additions and 43 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ localPort = 35345
externalPort = 3002 externalPort = 3002
[[ports]] [[ports]]
localPort = 36505 localPort = 40125
externalPort = 3001 externalPort = 3001
[[ports]] [[ports]]
+29
View File
@@ -9,6 +9,7 @@ import { Card } from "@/components/ui/card";
// Components // Components
import BottomNavigation from './components/BottomNavigation'; import BottomNavigation from './components/BottomNavigation';
import TaskCreationModal from './components/TaskCreationModal'; import TaskCreationModal from './components/TaskCreationModal';
import TaskDetailsModal from './components/TaskDetailsModal';
import TasksWithCalendar from './components/TasksWithCalendar'; import TasksWithCalendar from './components/TasksWithCalendar';
import CalendarView from './components/CalendarView'; import CalendarView from './components/CalendarView';
import KanbanBoard from './components/KanbanBoard'; import KanbanBoard from './components/KanbanBoard';
@@ -90,6 +91,8 @@ function App() {
const [currentTab, setCurrentTab] = useState('tasks'); const [currentTab, setCurrentTab] = useState('tasks');
const [tasks, setTasks] = useState<Task[]>(initialTasks); const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const handleCreateTask = (newTask: Partial<Task>) => { const handleCreateTask = (newTask: Partial<Task>) => {
const task: Task = { const task: Task = {
@@ -124,6 +127,22 @@ function App() {
handleTaskUpdate(taskId, { dueDate: newDate }); handleTaskUpdate(taskId, { dueDate: newDate });
}; };
const handleTaskClick = (task: Task) => {
setSelectedTask(task);
setIsTaskDetailsOpen(true);
console.log('Task clicked:', task.title);
};
const handleTaskDetailsClose = () => {
setIsTaskDetailsOpen(false);
setSelectedTask(null);
};
const handleTaskDetailsSave = (updatedTask: Task) => {
handleTaskUpdate(updatedTask.id, updatedTask);
console.log('Task details saved:', updatedTask.title);
};
const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => { const handleCreateFromTemplate = (templateId: string, startDate: Date, projectName: string) => {
console.log('Creating project from template:', { templateId, startDate, projectName }); console.log('Creating project from template:', { templateId, startDate, projectName });
// In a real app, this would create multiple tasks based on the template // In a real app, this would create multiple tasks based on the template
@@ -145,6 +164,7 @@ function App() {
<CalendarView <CalendarView
tasks={tasks} tasks={tasks}
onTaskDrop={handleTaskDrop} onTaskDrop={handleTaskDrop}
onTaskClick={handleTaskClick}
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())} onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
/> />
); );
@@ -155,6 +175,7 @@ function App() {
tasks={tasks} tasks={tasks}
onTaskStatusChange={handleTaskStatusChange} onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate} onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick}
/> />
); );
@@ -230,6 +251,14 @@ function App() {
onClose={() => setIsCreateModalOpen(false)} onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask} onSave={handleCreateTask}
/> />
{/* Task Details Modal */}
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
/>
</div> </div>
<Toaster /> <Toaster />
+11 -3
View File
@@ -10,10 +10,11 @@ import { addDays, format, isSameDay, startOfWeek, subWeeks } from 'date-fns';
interface CalendarViewProps { interface CalendarViewProps {
tasks: Task[]; tasks: Task[];
onTaskDrop?: (taskId: string, newDate: Date) => void; onTaskDrop?: (taskId: string, newDate: Date) => void;
onTaskClick?: (task: Task) => void;
onDateSelect?: (date: Date) => void; onDateSelect?: (date: Date) => void;
} }
export default function CalendarView({ tasks, onTaskDrop, onDateSelect }: CalendarViewProps) { export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSelect }: CalendarViewProps) {
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null); const [draggedTask, setDraggedTask] = useState<string | null>(null);
@@ -146,12 +147,19 @@ export default function CalendarView({ tasks, onTaskDrop, onDateSelect }: Calend
draggable draggable
onDragStart={() => handleDragStart(task.id)} onDragStart={() => handleDragStart(task.id)}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`} onClick={(e) => {
// Prevent click when dragging
if (draggedTask === task.id) return;
e.stopPropagation();
onTaskClick?.(task);
console.log('Calendar task clicked:', task.title);
}}
className={`cursor-pointer hover-elevate ${draggedTask === task.id ? 'opacity-50' : ''}`}
data-testid={`calendar-task-${task.id}`} data-testid={`calendar-task-${task.id}`}
> >
<Badge <Badge
variant="outline" variant="outline"
className="w-full justify-start text-xs p-1 h-auto" className="w-full justify-start text-xs p-1 h-auto pointer-events-none"
style={taskLabel ? { style={taskLabel ? {
borderColor: taskLabel.color, borderColor: taskLabel.color,
borderWidth: '2px', borderWidth: '2px',
+8 -4
View File
@@ -13,11 +13,12 @@ interface KanbanBoardProps {
tasks: Task[]; tasks: Task[];
onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void; onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void;
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void; onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
onTaskClick?: (task: Task) => void;
} }
type ViewMode = 'traditional' | 'weekly' | 'monthly'; type ViewMode = 'traditional' | 'weekly' | 'monthly';
export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }: KanbanBoardProps) { export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, onTaskClick }: KanbanBoardProps) {
const [viewMode, setViewMode] = useState<ViewMode>('traditional'); const [viewMode, setViewMode] = useState<ViewMode>('traditional');
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null); const [draggedTask, setDraggedTask] = useState<string | null>(null);
@@ -195,7 +196,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
console.log(`Timer paused for ${task.title}`); console.log(`Timer paused for ${task.title}`);
}} }}
onEdit={() => { onEdit={() => {
console.log(`Edit task ${task.title}`); onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
}} }}
isDragging={draggedTask === task.id} isDragging={draggedTask === task.id}
/> />
@@ -256,7 +258,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
console.log(`Timer paused for ${task.title}`); console.log(`Timer paused for ${task.title}`);
}} }}
onEdit={() => { onEdit={() => {
console.log(`Edit task ${task.title}`); onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
}} }}
isDragging={draggedTask === task.id} isDragging={draggedTask === task.id}
/> />
@@ -317,7 +320,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
console.log(`Timer paused for ${task.title}`); console.log(`Timer paused for ${task.title}`);
}} }}
onEdit={() => { onEdit={() => {
console.log(`Edit task ${task.title}`); onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
}} }}
isDragging={draggedTask === task.id} isDragging={draggedTask === task.id}
/> />
+31 -35
View File
@@ -8,19 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar } from 'lucide-react'; import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar } from 'lucide-react';
import { Task, Label } from '@shared/schema';
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 { interface TimeEntry {
id: string; id: string;
@@ -32,9 +20,9 @@ interface TimeEntry {
interface TaskDetailsModalProps { interface TaskDetailsModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
task: ProjectTask | null; task: Task | null;
onSave: (updatedTask: ProjectTask) => void; onSave: (updatedTask: Task) => void;
labels?: Array<{ id: string; name: string; color: string }>; labels?: Label[];
} }
export default function TaskDetailsModal({ export default function TaskDetailsModal({
@@ -56,13 +44,23 @@ export default function TaskDetailsModal({
const [timeDescription, setTimeDescription] = useState(''); const [timeDescription, setTimeDescription] = useState('');
const [activeTimeTab, setActiveTimeTab] = useState('clock'); const [activeTimeTab, setActiveTimeTab] = useState('clock');
// Time entries state // Time entries state - simulate from timeTracked for display
const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([]); const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([]);
useEffect(() => { useEffect(() => {
if (task && isOpen) { if (task && isOpen) {
setNotes(task.notes || ''); setNotes(task.notes || '');
setTimeEntries(task.timeEntries || []); // 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: 'Previously tracked time'
}]);
} else {
setTimeEntries([]);
}
// Reset time input state // Reset time input state
setHours(0); setHours(0);
setMinutes(0); setMinutes(0);
@@ -119,7 +117,6 @@ export default function TaskDetailsModal({
const updatedTask = { const updatedTask = {
...task, ...task,
timeEntries: updatedTimeEntries,
timeTracked: updatedTimeTracked timeTracked: updatedTimeTracked
}; };
@@ -137,7 +134,10 @@ export default function TaskDetailsModal({
}; };
const handleDeleteTimeEntry = (entryId: string) => { const handleDeleteTimeEntry = (entryId: string) => {
if (!task) return; if (!task || entryId === 'tracked-time') {
// Don't allow deleting the main tracked time entry
return;
}
const entryToDelete = timeEntries.find(entry => entry.id === entryId); const entryToDelete = timeEntries.find(entry => entry.id === entryId);
if (!entryToDelete) return; if (!entryToDelete) return;
@@ -147,7 +147,6 @@ export default function TaskDetailsModal({
const updatedTask = { const updatedTask = {
...task, ...task,
timeEntries: updatedTimeEntries,
timeTracked: updatedTimeTracked timeTracked: updatedTimeTracked
}; };
@@ -177,7 +176,7 @@ export default function TaskDetailsModal({
}; };
const getTotalTrackedTime = () => { const getTotalTrackedTime = () => {
return timeEntries.reduce((total, entry) => total + entry.timeSpent, 0); return task?.timeTracked || 0;
}; };
const getPriorityColor = (priority: string) => { const getPriorityColor = (priority: string) => {
@@ -231,11 +230,6 @@ export default function TaskDetailsModal({
<Badge variant="outline" className={`text-xs ${getStatusColor(task.status)}`}> <Badge variant="outline" className={`text-xs ${getStatusColor(task.status)}`}>
{task.status} {task.status}
</Badge> </Badge>
{task.estimatedHours && (
<Badge variant="outline" className="text-xs">
{task.estimatedHours}h estimated
</Badge>
)}
{getTotalTrackedTime() > 0 && ( {getTotalTrackedTime() > 0 && (
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{formatMinutesToHours(getTotalTrackedTime())} tracked {formatMinutesToHours(getTotalTrackedTime())} tracked
@@ -502,14 +496,16 @@ export default function TaskDetailsModal({
</p> </p>
)} )}
</div> </div>
<Button {entry.id !== 'tracked-time' && (
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => handleDeleteTimeEntry(entry.id)} size="sm"
data-testid={`button-delete-time-entry-${entry.id}`} onClick={() => handleDeleteTimeEntry(entry.id)}
> data-testid={`button-delete-time-entry-${entry.id}`}
<X className="w-3 h-3" /> >
</Button> <X className="w-3 h-3" />
</Button>
)}
</div> </div>
))} ))}
</div> </div>