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
[[ports]]
localPort = 36505
localPort = 40125
externalPort = 3001
[[ports]]
+29
View File
@@ -9,6 +9,7 @@ import { Card } from "@/components/ui/card";
// Components
import BottomNavigation from './components/BottomNavigation';
import TaskCreationModal from './components/TaskCreationModal';
import TaskDetailsModal from './components/TaskDetailsModal';
import TasksWithCalendar from './components/TasksWithCalendar';
import CalendarView from './components/CalendarView';
import KanbanBoard from './components/KanbanBoard';
@@ -90,6 +91,8 @@ function App() {
const [currentTab, setCurrentTab] = useState('tasks');
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isTaskDetailsOpen, setIsTaskDetailsOpen] = useState(false);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const handleCreateTask = (newTask: Partial<Task>) => {
const task: Task = {
@@ -124,6 +127,22 @@ function App() {
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) => {
console.log('Creating project from template:', { templateId, startDate, projectName });
// In a real app, this would create multiple tasks based on the template
@@ -145,6 +164,7 @@ function App() {
<CalendarView
tasks={tasks}
onTaskDrop={handleTaskDrop}
onTaskClick={handleTaskClick}
onDateSelect={(date) => console.log('Date selected:', date.toLocaleDateString())}
/>
);
@@ -155,6 +175,7 @@ function App() {
tasks={tasks}
onTaskStatusChange={handleTaskStatusChange}
onTaskUpdate={handleTaskUpdate}
onTaskClick={handleTaskClick}
/>
);
@@ -230,6 +251,14 @@ function App() {
onClose={() => setIsCreateModalOpen(false)}
onSave={handleCreateTask}
/>
{/* Task Details Modal */}
<TaskDetailsModal
isOpen={isTaskDetailsOpen}
onClose={handleTaskDetailsClose}
task={selectedTask}
onSave={handleTaskDetailsSave}
/>
</div>
<Toaster />
+11 -3
View File
@@ -10,10 +10,11 @@ import { addDays, format, isSameDay, startOfWeek, subWeeks } from 'date-fns';
interface CalendarViewProps {
tasks: Task[];
onTaskDrop?: (taskId: string, newDate: Date) => void;
onTaskClick?: (task: Task) => 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 [draggedTask, setDraggedTask] = useState<string | null>(null);
@@ -146,12 +147,19 @@ export default function CalendarView({ tasks, onTaskDrop, onDateSelect }: Calend
draggable
onDragStart={() => handleDragStart(task.id)}
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}`}
>
<Badge
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 ? {
borderColor: taskLabel.color,
borderWidth: '2px',
+8 -4
View File
@@ -13,11 +13,12 @@ interface KanbanBoardProps {
tasks: Task[];
onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void;
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
onTaskClick?: (task: Task) => void;
}
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 [currentDate, setCurrentDate] = useState(new Date());
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}`);
}}
onEdit={() => {
console.log(`Edit task ${task.title}`);
onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
}}
isDragging={draggedTask === task.id}
/>
@@ -256,7 +258,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
console.log(`Timer paused for ${task.title}`);
}}
onEdit={() => {
console.log(`Edit task ${task.title}`);
onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
}}
isDragging={draggedTask === task.id}
/>
@@ -317,7 +320,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
console.log(`Timer paused for ${task.title}`);
}}
onEdit={() => {
console.log(`Edit task ${task.title}`);
onTaskClick?.(task);
console.log(`Task clicked from board: ${task.title}`);
}}
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 { 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[];
}
import { Task, Label } from '@shared/schema';
interface TimeEntry {
id: string;
@@ -32,9 +20,9 @@ interface TimeEntry {
interface TaskDetailsModalProps {
isOpen: boolean;
onClose: () => void;
task: ProjectTask | null;
onSave: (updatedTask: ProjectTask) => void;
labels?: Array<{ id: string; name: string; color: string }>;
task: Task | null;
onSave: (updatedTask: Task) => void;
labels?: Label[];
}
export default function TaskDetailsModal({
@@ -56,13 +44,23 @@ export default function TaskDetailsModal({
const [timeDescription, setTimeDescription] = useState('');
const [activeTimeTab, setActiveTimeTab] = useState('clock');
// Time entries state
// Time entries state - simulate from timeTracked for display
const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([]);
useEffect(() => {
if (task && isOpen) {
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
setHours(0);
setMinutes(0);
@@ -119,7 +117,6 @@ export default function TaskDetailsModal({
const updatedTask = {
...task,
timeEntries: updatedTimeEntries,
timeTracked: updatedTimeTracked
};
@@ -137,7 +134,10 @@ export default function TaskDetailsModal({
};
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);
if (!entryToDelete) return;
@@ -147,7 +147,6 @@ export default function TaskDetailsModal({
const updatedTask = {
...task,
timeEntries: updatedTimeEntries,
timeTracked: updatedTimeTracked
};
@@ -177,7 +176,7 @@ export default function TaskDetailsModal({
};
const getTotalTrackedTime = () => {
return timeEntries.reduce((total, entry) => total + entry.timeSpent, 0);
return task?.timeTracked || 0;
};
const getPriorityColor = (priority: string) => {
@@ -231,11 +230,6 @@ export default function TaskDetailsModal({
<Badge variant="outline" className={`text-xs ${getStatusColor(task.status)}`}>
{task.status}
</Badge>
{task.estimatedHours && (
<Badge variant="outline" className="text-xs">
{task.estimatedHours}h estimated
</Badge>
)}
{getTotalTrackedTime() > 0 && (
<Badge variant="outline" className="text-xs">
{formatMinutesToHours(getTotalTrackedTime())} tracked
@@ -502,14 +496,16 @@ export default function TaskDetailsModal({
</p>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteTimeEntry(entry.id)}
data-testid={`button-delete-time-entry-${entry.id}`}
>
<X className="w-3 h-3" />
</Button>
{entry.id !== 'tracked-time' && (
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteTimeEntry(entry.id)}
data-testid={`button-delete-time-entry-${entry.id}`}
>
<X className="w-3 h-3" />
</Button>
)}
</div>
))}
</div>