Allow users to add and edit tasks within projects

Introduce state and handlers for managing project tasks, enabling users to add, edit, and remove tasks directly within the project creation and editing interfaces.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: full_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 15:27:36 +00:00
parent dd453e1db1
commit 5827bb7992
2 changed files with 518 additions and 73 deletions
+4
View File
@@ -14,6 +14,10 @@ run = ["npm", "run", "start"]
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 33729
externalPort = 3001
[[ports]] [[ports]]
localPort = 35345 localPort = 35345
externalPort = 3002 externalPort = 3002
+514 -73
View File
@@ -101,6 +101,12 @@ export default function ProjectTemplate({
const [isEndDateCalendarOpen, setIsEndDateCalendarOpen] = useState(false); const [isEndDateCalendarOpen, setIsEndDateCalendarOpen] = useState(false);
const [closedProjectsFilter, setClosedProjectsFilter] = useState(''); const [closedProjectsFilter, setClosedProjectsFilter] = useState('');
// Task management state
const [projectTasks, setProjectTasks] = useState<ProjectTask[]>([]);
const [isEditingTask, setIsEditingTask] = useState(false);
const [editingTaskIndex, setEditingTaskIndex] = useState<number | null>(null);
const [taskForm, setTaskForm] = useState<Partial<ProjectTask>>({});
// Label management state // Label management state
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false); const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
const [editingLabel, setEditingLabel] = useState<Label | null>(null); const [editingLabel, setEditingLabel] = useState<Label | null>(null);
@@ -155,20 +161,22 @@ export default function ProjectTemplate({
name: newProjectName.trim(), name: newProjectName.trim(),
description: newProjectDescription.trim() || undefined, description: newProjectDescription.trim() || undefined,
status: 'planning', status: 'planning',
tasks: [], tasks: projectTasks,
createdAt: new Date() createdAt: new Date()
}; };
setProjects([...projects, newProject]); setProjects([...projects, newProject]);
setNewProjectName(''); setNewProjectName('');
setNewProjectDescription(''); setNewProjectDescription('');
setProjectTasks([]);
setIsCreateProjectOpen(false); setIsCreateProjectOpen(false);
console.log('Created new project:', newProject.name); console.log('Created new project:', newProject.name, 'with', projectTasks.length, 'tasks');
} }
}; };
const handleEditProject = (project: Project) => { const handleEditProject = (project: Project) => {
setSelectedProject(project); setSelectedProject(project);
setProjectTasks([...project.tasks]);
setIsEditProjectOpen(true); setIsEditProjectOpen(true);
}; };
@@ -239,6 +247,83 @@ export default function ProjectTemplate({
(project.description && project.description.toLowerCase().includes(closedProjectsFilter.toLowerCase())) (project.description && project.description.toLowerCase().includes(closedProjectsFilter.toLowerCase()))
); );
}; };
// Task management functions
const handleAddTask = () => {
const newTask: ProjectTask = {
id: Date.now().toString(),
title: taskForm.title || 'New Task',
description: taskForm.description || '',
priority: taskForm.priority || 'medium',
status: taskForm.status || 'todo',
estimatedHours: taskForm.estimatedHours || 0,
labelId: taskForm.labelId
};
setProjectTasks([...projectTasks, newTask]);
setTaskForm({});
setIsEditingTask(false);
};
const handleEditTask = (index: number) => {
setEditingTaskIndex(index);
setTaskForm({ ...projectTasks[index] });
setIsEditingTask(true);
};
const handleSaveTask = () => {
if (editingTaskIndex !== null) {
const updatedTasks = [...projectTasks];
updatedTasks[editingTaskIndex] = {
id: projectTasks[editingTaskIndex].id,
title: taskForm.title || 'New Task',
description: taskForm.description || '',
priority: taskForm.priority || 'medium',
status: taskForm.status || 'todo',
estimatedHours: taskForm.estimatedHours || 0,
labelId: taskForm.labelId
};
setProjectTasks(updatedTasks);
}
setTaskForm({});
setIsEditingTask(false);
setEditingTaskIndex(null);
};
const handleRemoveTask = (index: number) => {
setProjectTasks(projectTasks.filter((_, i) => i !== index));
};
const handleCancelTaskEdit = () => {
setTaskForm({});
setIsEditingTask(false);
setEditingTaskIndex(null);
};
const handleStartNewTask = () => {
setTaskForm({});
setEditingTaskIndex(null);
setIsEditingTask(true);
};
const handleSaveEditedProject = () => {
if (selectedProject) {
const updatedProject = {
...selectedProject,
tasks: projectTasks
};
setProjects(projects.map(p =>
p.id === selectedProject.id ? updatedProject : p
));
setSelectedProject(null);
setProjectTasks([]);
setIsEditProjectOpen(false);
console.log('Updated project:', updatedProject.name, 'with', projectTasks.length, 'tasks');
}
};
// Label management functions // Label management functions
const handleSaveLabel = () => { const handleSaveLabel = () => {
@@ -580,35 +665,218 @@ export default function ProjectTemplate({
</Dialog> </Dialog>
{/* Create Project Dialog */} {/* Create Project Dialog */}
<Dialog open={isCreateProjectOpen} onOpenChange={setIsCreateProjectOpen}> <Dialog open={isCreateProjectOpen} onOpenChange={() => {
<DialogContent className="sm:max-w-md"> setIsCreateProjectOpen(false);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Create New Project</DialogTitle> <DialogTitle>Create New Project</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-6 py-4">
<div className="space-y-2"> {/* Project Details */}
<label className="text-sm font-medium">Project Name</label> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input <div className="space-y-2">
value={newProjectName} <label className="text-sm font-medium">Project Name</label>
onChange={(e) => setNewProjectName(e.target.value)} <Input
placeholder="Enter project name" value={newProjectName}
data-testid="input-project-name" onChange={(e) => setNewProjectName(e.target.value)}
/> placeholder="Enter project name"
data-testid="input-project-name"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description (Optional)</label>
<Input
value={newProjectDescription}
onChange={(e) => setNewProjectDescription(e.target.value)}
placeholder="Enter project description"
data-testid="input-project-description"
/>
</div>
</div> </div>
<div className="space-y-2">
<label className="text-sm font-medium">Description (Optional)</label> {/* Tasks Section */}
<Input <div className="space-y-4">
value={newProjectDescription} <div className="flex items-center justify-between">
onChange={(e) => setNewProjectDescription(e.target.value)} <h3 className="text-lg font-semibold">Project Tasks</h3>
placeholder="Enter project description" <Button
data-testid="input-project-description" variant="outline"
/> size="sm"
onClick={handleStartNewTask}
data-testid="button-add-task"
>
<Plus className="w-4 h-4 mr-2" />
Add Task
</Button>
</div>
{/* Task List */}
<div className="space-y-2 max-h-64 overflow-y-auto">
{projectTasks.map((task, index) => (
<Card key={task.id} className="p-3">
<div className="flex items-center justify-between">
<div className="flex-1">
<h4 className="font-medium text-sm">{task.title}</h4>
{task.description && (
<p className="text-xs text-muted-foreground">{task.description}</p>
)}
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{task.priority}
</Badge>
<Badge variant="outline" className="text-xs">
{task.status}
</Badge>
{task.estimatedHours ? (
<Badge variant="outline" className="text-xs">
{task.estimatedHours}h
</Badge>
) : null}
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditTask(index)}
data-testid={`button-edit-task-${index}`}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveTask(index)}
data-testid={`button-remove-task-${index}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
</Card>
))}
{projectTasks.length === 0 && (
<div className="text-center text-muted-foreground text-sm py-8 border-2 border-dashed border-muted rounded-lg">
No tasks added yet. Click "Add Task" to get started.
</div>
)}
</div>
{/* Task Form */}
{isEditingTask && (
<Card className="p-4 border-primary">
<div className="space-y-4">
<h4 className="font-semibold text-sm">
{editingTaskIndex !== null ? 'Edit Task' : 'New Task'}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Task Title</label>
<Input
value={taskForm.title || ''}
onChange={(e) => setTaskForm({...taskForm, title: e.target.value})}
placeholder="Enter task title"
data-testid="input-task-title"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description</label>
<Input
value={taskForm.description || ''}
onChange={(e) => setTaskForm({...taskForm, description: e.target.value})}
placeholder="Enter task description"
data-testid="input-task-description"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Priority</label>
<Select
value={taskForm.priority || 'medium'}
onValueChange={(value: ProjectTask['priority']) =>
setTaskForm({...taskForm, priority: value})
}
>
<SelectTrigger data-testid="select-task-priority">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Status</label>
<Select
value={taskForm.status || 'todo'}
onValueChange={(value: ProjectTask['status']) =>
setTaskForm({...taskForm, status: value})
}
>
<SelectTrigger data-testid="select-task-status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="todo">To Do</SelectItem>
<SelectItem value="inProgress">In Progress</SelectItem>
<SelectItem value="done">Done</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Estimated Hours</label>
<Input
type="number"
value={taskForm.estimatedHours || ''}
onChange={(e) => setTaskForm({...taskForm, estimatedHours: Number(e.target.value)})}
placeholder="0"
min="0"
data-testid="input-task-hours"
/>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleCancelTaskEdit}
className="flex-1"
>
Cancel
</Button>
<Button
onClick={editingTaskIndex !== null ? handleSaveTask : handleAddTask}
disabled={!taskForm.title?.trim()}
className="flex-1"
data-testid="button-save-task"
>
{editingTaskIndex !== null ? 'Save Changes' : 'Add Task'}
</Button>
</div>
</div>
</Card>
)}
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2 pt-4 border-t">
<Button <Button
variant="outline" variant="outline"
onClick={() => setIsCreateProjectOpen(false)} onClick={() => {
setIsCreateProjectOpen(false);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}}
className="flex-1" className="flex-1"
> >
Cancel Cancel
@@ -619,85 +887,258 @@ export default function ProjectTemplate({
className="flex-1" className="flex-1"
data-testid="button-save-project" data-testid="button-save-project"
> >
Create Project Create Project ({projectTasks.length} tasks)
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Edit Project Dialog */} {/* Edit Project Dialog */}
<Dialog open={isEditProjectOpen} onOpenChange={setIsEditProjectOpen}> <Dialog open={isEditProjectOpen} onOpenChange={() => {
<DialogContent className="sm:max-w-md"> setIsEditProjectOpen(false);
setSelectedProject(null);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit Project</DialogTitle> <DialogTitle>Edit Project</DialogTitle>
</DialogHeader> </DialogHeader>
{selectedProject && ( {selectedProject && (
<div className="space-y-4 py-4"> <div className="space-y-6 py-4">
<div className="space-y-2"> {/* Project Details */}
<label className="text-sm font-medium">Project Name</label> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input <div className="space-y-2">
value={selectedProject.name} <label className="text-sm font-medium">Project Name</label>
onChange={(e) => setSelectedProject({ ...selectedProject, name: e.target.value })} <Input
placeholder="Enter project name" value={selectedProject.name}
data-testid="input-edit-project-name" onChange={(e) => setSelectedProject({ ...selectedProject, name: e.target.value })}
/> placeholder="Enter project name"
data-testid="input-edit-project-name"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description (Optional)</label>
<Input
value={selectedProject.description || ''}
onChange={(e) => setSelectedProject({ ...selectedProject, description: e.target.value })}
placeholder="Enter project description"
data-testid="input-edit-project-description"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Status</label>
<Select
value={selectedProject.status}
onValueChange={(value: Project['status']) =>
setSelectedProject({ ...selectedProject, status: value })
}
>
<SelectTrigger data-testid="select-project-status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="planning">Planning</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="finished">Finished</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
<div className="space-y-2">
<label className="text-sm font-medium">Description (Optional)</label> {/* Tasks Section */}
<Input <div className="space-y-4">
value={selectedProject.description || ''} <div className="flex items-center justify-between">
onChange={(e) => setSelectedProject({ ...selectedProject, description: e.target.value })} <h3 className="text-lg font-semibold">Project Tasks</h3>
placeholder="Enter project description" <Button
data-testid="input-edit-project-description" variant="outline"
/> size="sm"
</div> onClick={handleStartNewTask}
<div className="space-y-2"> data-testid="button-add-task-edit"
<label className="text-sm font-medium">Status</label> >
<Select <Plus className="w-4 h-4 mr-2" />
value={selectedProject.status} Add Task
onValueChange={(value: Project['status']) => </Button>
setSelectedProject({ ...selectedProject, status: value }) </div>
}
> {/* Task List */}
<SelectTrigger data-testid="select-project-status"> <div className="space-y-2 max-h-64 overflow-y-auto">
<SelectValue /> {projectTasks.map((task, index) => (
</SelectTrigger> <Card key={task.id} className="p-3">
<SelectContent> <div className="flex items-center justify-between">
<SelectItem value="planning">Planning</SelectItem> <div className="flex-1">
<SelectItem value="active">Active</SelectItem> <h4 className="font-medium text-sm">{task.title}</h4>
<SelectItem value="finished">Finished</SelectItem> {task.description && (
</SelectContent> <p className="text-xs text-muted-foreground">{task.description}</p>
</Select> )}
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{task.priority}
</Badge>
<Badge variant="outline" className="text-xs">
{task.status}
</Badge>
{task.estimatedHours ? (
<Badge variant="outline" className="text-xs">
{task.estimatedHours}h
</Badge>
) : null}
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditTask(index)}
data-testid={`button-edit-task-edit-${index}`}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveTask(index)}
data-testid={`button-remove-task-edit-${index}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
</Card>
))}
{projectTasks.length === 0 && (
<div className="text-center text-muted-foreground text-sm py-8 border-2 border-dashed border-muted rounded-lg">
No tasks in this project yet. Click "Add Task" to get started.
</div>
)}
</div>
{/* Task Form */}
{isEditingTask && (
<Card className="p-4 border-primary">
<div className="space-y-4">
<h4 className="font-semibold text-sm">
{editingTaskIndex !== null ? 'Edit Task' : 'New Task'}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Task Title</label>
<Input
value={taskForm.title || ''}
onChange={(e) => setTaskForm({...taskForm, title: e.target.value})}
placeholder="Enter task title"
data-testid="input-task-title-edit"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description</label>
<Input
value={taskForm.description || ''}
onChange={(e) => setTaskForm({...taskForm, description: e.target.value})}
placeholder="Enter task description"
data-testid="input-task-description-edit"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Priority</label>
<Select
value={taskForm.priority || 'medium'}
onValueChange={(value: ProjectTask['priority']) =>
setTaskForm({...taskForm, priority: value})
}
>
<SelectTrigger data-testid="select-task-priority-edit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Status</label>
<Select
value={taskForm.status || 'todo'}
onValueChange={(value: ProjectTask['status']) =>
setTaskForm({...taskForm, status: value})
}
>
<SelectTrigger data-testid="select-task-status-edit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="todo">To Do</SelectItem>
<SelectItem value="inProgress">In Progress</SelectItem>
<SelectItem value="done">Done</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Estimated Hours</label>
<Input
type="number"
value={taskForm.estimatedHours || ''}
onChange={(e) => setTaskForm({...taskForm, estimatedHours: Number(e.target.value)})}
placeholder="0"
min="0"
data-testid="input-task-hours-edit"
/>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleCancelTaskEdit}
className="flex-1"
>
Cancel
</Button>
<Button
onClick={editingTaskIndex !== null ? handleSaveTask : handleAddTask}
disabled={!taskForm.title?.trim()}
className="flex-1"
data-testid="button-save-task-edit"
>
{editingTaskIndex !== null ? 'Save Changes' : 'Add Task'}
</Button>
</div>
</div>
</Card>
)}
</div> </div>
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2 pt-4 border-t">
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setIsEditProjectOpen(false); setIsEditProjectOpen(false);
setSelectedProject(null); setSelectedProject(null);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}} }}
className="flex-1" className="flex-1"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
onClick={() => { onClick={handleSaveEditedProject}
if (selectedProject) {
setProjects(projects.map(p =>
p.id === selectedProject.id ? selectedProject : p
));
setIsEditProjectOpen(false);
setSelectedProject(null);
console.log('Updated project:', selectedProject.name);
}
}}
disabled={!selectedProject?.name.trim()} disabled={!selectedProject?.name.trim()}
className="flex-1" className="flex-1"
data-testid="button-update-project" data-testid="button-update-project"
> >
Update Project Update Project ({projectTasks.length} tasks)
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>