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
externalPort = 80
[[ports]]
localPort = 33729
externalPort = 3001
[[ports]]
localPort = 35345
externalPort = 3002
+514 -73
View File
@@ -101,6 +101,12 @@ export default function ProjectTemplate({
const [isEndDateCalendarOpen, setIsEndDateCalendarOpen] = useState(false);
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
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
const [editingLabel, setEditingLabel] = useState<Label | null>(null);
@@ -155,20 +161,22 @@ export default function ProjectTemplate({
name: newProjectName.trim(),
description: newProjectDescription.trim() || undefined,
status: 'planning',
tasks: [],
tasks: projectTasks,
createdAt: new Date()
};
setProjects([...projects, newProject]);
setNewProjectName('');
setNewProjectDescription('');
setProjectTasks([]);
setIsCreateProjectOpen(false);
console.log('Created new project:', newProject.name);
console.log('Created new project:', newProject.name, 'with', projectTasks.length, 'tasks');
}
};
const handleEditProject = (project: Project) => {
setSelectedProject(project);
setProjectTasks([...project.tasks]);
setIsEditProjectOpen(true);
};
@@ -239,6 +247,83 @@ export default function ProjectTemplate({
(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
const handleSaveLabel = () => {
@@ -580,35 +665,218 @@ export default function ProjectTemplate({
</Dialog>
{/* Create Project Dialog */}
<Dialog open={isCreateProjectOpen} onOpenChange={setIsCreateProjectOpen}>
<DialogContent className="sm:max-w-md">
<Dialog open={isCreateProjectOpen} onOpenChange={() => {
setIsCreateProjectOpen(false);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Project Name</label>
<Input
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
placeholder="Enter project name"
data-testid="input-project-name"
/>
<div className="space-y-6 py-4">
{/* Project Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Project Name</label>
<Input
value={newProjectName}
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 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"
/>
{/* Tasks Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Project Tasks</h3>
<Button
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 className="flex gap-2">
<div className="flex gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setIsCreateProjectOpen(false)}
onClick={() => {
setIsCreateProjectOpen(false);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}}
className="flex-1"
>
Cancel
@@ -619,85 +887,258 @@ export default function ProjectTemplate({
className="flex-1"
data-testid="button-save-project"
>
Create Project
Create Project ({projectTasks.length} tasks)
</Button>
</div>
</DialogContent>
</Dialog>
{/* Edit Project Dialog */}
<Dialog open={isEditProjectOpen} onOpenChange={setIsEditProjectOpen}>
<DialogContent className="sm:max-w-md">
<Dialog open={isEditProjectOpen} onOpenChange={() => {
setIsEditProjectOpen(false);
setSelectedProject(null);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Project</DialogTitle>
</DialogHeader>
{selectedProject && (
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Project Name</label>
<Input
value={selectedProject.name}
onChange={(e) => setSelectedProject({ ...selectedProject, name: e.target.value })}
placeholder="Enter project name"
data-testid="input-edit-project-name"
/>
<div className="space-y-6 py-4">
{/* Project Details */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Project Name</label>
<Input
value={selectedProject.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 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>
{/* Tasks Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Project Tasks</h3>
<Button
variant="outline"
size="sm"
onClick={handleStartNewTask}
data-testid="button-add-task-edit"
>
<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-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 className="flex gap-2">
<div className="flex gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => {
setIsEditProjectOpen(false);
setSelectedProject(null);
setProjectTasks([]);
setTaskForm({});
setIsEditingTask(false);
}}
className="flex-1"
>
Cancel
</Button>
<Button
onClick={() => {
if (selectedProject) {
setProjects(projects.map(p =>
p.id === selectedProject.id ? selectedProject : p
));
setIsEditProjectOpen(false);
setSelectedProject(null);
console.log('Updated project:', selectedProject.name);
}
}}
onClick={handleSaveEditedProject}
disabled={!selectedProject?.name.trim()}
className="flex-1"
data-testid="button-update-project"
>
Update Project
Update Project ({projectTasks.length} tasks)
</Button>
</div>
</DialogContent>