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