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:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user