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:
@@ -19,7 +19,7 @@ localPort = 35345
|
|||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 36505
|
localPort = 40125
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user