Improve task management by adding project restart functionality and filtering closed projects
Enhances the task management app by introducing a "restart project" feature with options to clear history or reset tasks, refining calendar and Kanban views to use a Monday start, and enabling filtering for closed projects. 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/RVJYTwu
This commit is contained in:
+17
-38
@@ -29,6 +29,7 @@ const initialTasks: Task[] = [
|
|||||||
timeTracked: 0,
|
timeTracked: 0,
|
||||||
isTracking: false,
|
isTracking: false,
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
|
notes: null,
|
||||||
labelId: '2e8c3aa0-278f-4220-b2c5-aa109b4279b7' // Urgent label
|
labelId: '2e8c3aa0-278f-4220-b2c5-aa109b4279b7' // Urgent label
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -41,16 +42,20 @@ const initialTasks: Task[] = [
|
|||||||
timeTracked: 120,
|
timeTracked: 120,
|
||||||
isTracking: true,
|
isTracking: true,
|
||||||
projectId: 'project-2',
|
projectId: 'project-2',
|
||||||
|
notes: null,
|
||||||
labelId: 'cb44bed1-8ba3-43fe-9498-bb28e483ed1f' // Work label
|
labelId: 'cb44bed1-8ba3-43fe-9498-bb28e483ed1f' // Work label
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
title: 'Team standup meeting',
|
title: 'Team standup meeting',
|
||||||
|
description: null,
|
||||||
status: 'todo',
|
status: 'todo',
|
||||||
priority: 'low',
|
priority: 'low',
|
||||||
dueDate: undefined, // Make unscheduled
|
dueDate: null,
|
||||||
timeTracked: 0,
|
timeTracked: 0,
|
||||||
isTracking: false,
|
isTracking: false,
|
||||||
|
projectId: null,
|
||||||
|
notes: null,
|
||||||
labelId: '274f0ba4-a133-471a-bbe9-8189aa3b0106' // Personal label
|
labelId: '274f0ba4-a133-471a-bbe9-8189aa3b0106' // Personal label
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,21 +64,25 @@ const initialTasks: Task[] = [
|
|||||||
description: 'Users unable to login with special characters in password',
|
description: 'Users unable to login with special characters in password',
|
||||||
status: 'todo',
|
status: 'todo',
|
||||||
priority: 'high',
|
priority: 'high',
|
||||||
dueDate: undefined, // Make unscheduled
|
dueDate: null,
|
||||||
timeTracked: 30,
|
timeTracked: 30,
|
||||||
isTracking: false,
|
isTracking: false,
|
||||||
projectId: 'project-2',
|
projectId: 'project-2',
|
||||||
|
notes: null,
|
||||||
labelId: 'c5eccac9-7919-4eb1-bb50-badacad6b1c1' // Study label
|
labelId: 'c5eccac9-7919-4eb1-bb50-badacad6b1c1' // Study label
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '5',
|
||||||
title: 'Deploy new features',
|
title: 'Deploy new features',
|
||||||
|
description: null,
|
||||||
status: 'done',
|
status: 'done',
|
||||||
priority: 'medium',
|
priority: 'medium',
|
||||||
dueDate: subDays(new Date(), 2),
|
dueDate: subDays(new Date(), 2),
|
||||||
timeTracked: 120,
|
timeTracked: 120,
|
||||||
isTracking: false,
|
isTracking: false,
|
||||||
projectId: 'project-1'
|
projectId: 'project-1',
|
||||||
|
notes: null,
|
||||||
|
labelId: null
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -86,14 +95,15 @@ function App() {
|
|||||||
const task: Task = {
|
const task: Task = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
title: newTask.title || '',
|
title: newTask.title || '',
|
||||||
description: newTask.description,
|
description: newTask.description || null,
|
||||||
status: 'todo',
|
status: 'todo',
|
||||||
priority: newTask.priority || 'medium',
|
priority: newTask.priority || 'medium',
|
||||||
dueDate: newTask.dueDate,
|
dueDate: newTask.dueDate || null,
|
||||||
timeTracked: 0,
|
timeTracked: 0,
|
||||||
isTracking: false,
|
isTracking: false,
|
||||||
projectId: newTask.projectId,
|
projectId: newTask.projectId || null,
|
||||||
labelId: newTask.labelId
|
notes: newTask.notes || null,
|
||||||
|
labelId: newTask.labelId || null
|
||||||
};
|
};
|
||||||
setTasks(prev => [...prev, task]);
|
setTasks(prev => [...prev, task]);
|
||||||
console.log('Task created:', task);
|
console.log('Task created:', task);
|
||||||
@@ -155,37 +165,6 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'settings':
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold">Settings</h2>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">Dark Mode</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">Toggle between light and dark themes</p>
|
|
||||||
</div>
|
|
||||||
<ThemeToggle />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="font-semibold">About TaskFlow</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
A personal task management app with calendar scheduling, kanban boards, and project templates.
|
|
||||||
</p>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
Version 1.0.0 • Built with React and Tailwind CSS
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ export default function CalendarView({ tasks, onTaskDrop, onDateSelect }: Calend
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get 6 weeks: 1 previous week (grayed out) + current week + 4 future weeks
|
// Get 5 weeks: 1 previous week (grayed out) + current week + 3 future weeks
|
||||||
const currentWeekStart = startOfWeek(currentDate);
|
const currentWeekStart = startOfWeek(currentDate, { weekStartsOn: 1 }); // Monday = 1
|
||||||
const startDate = subWeeks(currentWeekStart, 1); // Start from 1 week before current week
|
const startDate = subWeeks(currentWeekStart, 1); // Start from 1 week before current week
|
||||||
const dates = Array.from({ length: 42 }, (_, i) => addDays(startDate, i));
|
const dates = Array.from({ length: 35 }, (_, i) => addDays(startDate, i));
|
||||||
|
|
||||||
const getTasksForDate = (date: Date) => {
|
const getTasksForDate = (date: Date) => {
|
||||||
return tasks.filter(task =>
|
return tasks.filter(task =>
|
||||||
@@ -69,7 +69,7 @@ export default function CalendarView({ tasks, onTaskDrop, onDateSelect }: Calend
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="w-5 h-5 text-primary" />
|
<Calendar className="w-5 h-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold" data-testid="text-calendar-title">
|
<h2 className="text-lg font-semibold" data-testid="text-calendar-title">
|
||||||
6-Week Calendar View
|
5-Week Calendar View
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
|
|||||||
let filteredTasks = tasks.filter(task => task.status === status);
|
let filteredTasks = tasks.filter(task => task.status === status);
|
||||||
|
|
||||||
if (viewMode === 'weekly') {
|
if (viewMode === 'weekly') {
|
||||||
const weekStart = startOfWeek(currentDate);
|
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 }); // Monday = 1
|
||||||
const weekEnd = endOfWeek(currentDate);
|
const weekEnd = endOfWeek(currentDate, { weekStartsOn: 1 });
|
||||||
filteredTasks = filteredTasks.filter(task =>
|
filteredTasks = filteredTasks.filter(task =>
|
||||||
task.dueDate && isWithinInterval(task.dueDate, { start: weekStart, end: weekEnd })
|
task.dueDate && isWithinInterval(task.dueDate, { start: weekStart, end: weekEnd })
|
||||||
);
|
);
|
||||||
@@ -87,7 +87,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }:
|
|||||||
|
|
||||||
const getPeriodTitle = () => {
|
const getPeriodTitle = () => {
|
||||||
if (viewMode === 'weekly') {
|
if (viewMode === 'weekly') {
|
||||||
return `Week of ${format(startOfWeek(currentDate), 'MMM d')}`;
|
return `Week of ${format(startOfWeek(currentDate, { weekStartsOn: 1 }), 'MMM d')}`;
|
||||||
} else if (viewMode === 'monthly') {
|
} else if (viewMode === 'monthly') {
|
||||||
return format(currentDate, 'MMMM yyyy');
|
return format(currentDate, 'MMMM yyyy');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Calendar } from '@/components/ui/calendar';
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Plus, Calendar as CalendarIcon, Clock, Copy, Play, Tag, Edit, Trash2, Palette } from 'lucide-react';
|
import { Plus, Calendar as CalendarIcon, Clock, Copy, Play, Tag, Edit, Trash2, Palette } from 'lucide-react';
|
||||||
import { Task } from './TaskCard';
|
|
||||||
import { Label } from '@shared/schema';
|
import { Label } from '@shared/schema';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiRequest } from '@/lib/queryClient';
|
import { apiRequest } from '@/lib/queryClient';
|
||||||
@@ -95,6 +94,12 @@ export default function ProjectTemplate({
|
|||||||
const [isEditProjectOpen, setIsEditProjectOpen] = useState(false);
|
const [isEditProjectOpen, setIsEditProjectOpen] = useState(false);
|
||||||
const [newProjectName, setNewProjectName] = useState('');
|
const [newProjectName, setNewProjectName] = useState('');
|
||||||
const [newProjectDescription, setNewProjectDescription] = useState('');
|
const [newProjectDescription, setNewProjectDescription] = useState('');
|
||||||
|
const [isRestartProjectOpen, setIsRestartProjectOpen] = useState(false);
|
||||||
|
const [restartingProject, setRestartingProject] = useState<Project | null>(null);
|
||||||
|
const [newEndDate, setNewEndDate] = useState<Date | undefined>();
|
||||||
|
const [clearHistory, setClearHistory] = useState(false);
|
||||||
|
const [isEndDateCalendarOpen, setIsEndDateCalendarOpen] = useState(false);
|
||||||
|
const [closedProjectsFilter, setClosedProjectsFilter] = useState('');
|
||||||
|
|
||||||
// Label management state
|
// Label management state
|
||||||
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
|
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
|
||||||
@@ -197,6 +202,43 @@ export default function ProjectTemplate({
|
|||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRestartProject = (project: Project) => {
|
||||||
|
setRestartingProject(project);
|
||||||
|
setIsRestartProjectOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmRestart = () => {
|
||||||
|
if (restartingProject && newEndDate) {
|
||||||
|
const restartedProject: Project = {
|
||||||
|
...restartingProject,
|
||||||
|
status: 'planning',
|
||||||
|
startDate: new Date(),
|
||||||
|
endDate: newEndDate,
|
||||||
|
tasks: clearHistory ? [] : restartingProject.tasks.map(task => ({ ...task, status: 'todo' as const }))
|
||||||
|
};
|
||||||
|
|
||||||
|
setProjects(projects.map(p =>
|
||||||
|
p.id === restartingProject.id ? restartedProject : p
|
||||||
|
));
|
||||||
|
|
||||||
|
setIsRestartProjectOpen(false);
|
||||||
|
setRestartingProject(null);
|
||||||
|
setNewEndDate(undefined);
|
||||||
|
setClearHistory(false);
|
||||||
|
console.log('Project restarted:', restartedProject.name, 'Clear history:', clearHistory);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilteredClosedProjects = () => {
|
||||||
|
const closedProjects = projects.filter(project => project.status === 'finished');
|
||||||
|
if (!closedProjectsFilter) return closedProjects;
|
||||||
|
|
||||||
|
return closedProjects.filter(project =>
|
||||||
|
project.name.toLowerCase().includes(closedProjectsFilter.toLowerCase()) ||
|
||||||
|
(project.description && project.description.toLowerCase().includes(closedProjectsFilter.toLowerCase()))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Label management functions
|
// Label management functions
|
||||||
const handleSaveLabel = () => {
|
const handleSaveLabel = () => {
|
||||||
@@ -378,8 +420,165 @@ export default function ProjectTemplate({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Closed Projects Section */}
|
||||||
|
<div className="space-y-4 mt-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">Closed Projects</h3>
|
||||||
|
<Input
|
||||||
|
placeholder="Filter closed projects..."
|
||||||
|
value={closedProjectsFilter}
|
||||||
|
onChange={(e) => setClosedProjectsFilter(e.target.value)}
|
||||||
|
className="max-w-xs"
|
||||||
|
data-testid="input-filter-closed-projects"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{getFilteredClosedProjects().map((project) => (
|
||||||
|
<Card key={project.id} className="p-4 hover-elevate" data-testid={`closed-project-card-${project.id}`}>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-sm" data-testid={`text-closed-project-name-${project.id}`}>
|
||||||
|
{project.name}
|
||||||
|
</h4>
|
||||||
|
{project.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-200">
|
||||||
|
Finished
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>{getTotalEstimatedHours(project)}h</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{project.tasks.length} tasks
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{project.endDate && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Finished: {project.endDate.toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRestartProject(project)}
|
||||||
|
className="w-full"
|
||||||
|
data-testid={`button-restart-${project.id}`}
|
||||||
|
>
|
||||||
|
<Play className="w-3 h-3 mr-2" />
|
||||||
|
Start Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{getFilteredClosedProjects().length === 0 && (
|
||||||
|
<div className="col-span-full text-center text-muted-foreground text-sm py-8">
|
||||||
|
{closedProjectsFilter ? 'No closed projects match your filter' : 'No closed projects yet'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Restart Project Dialog */}
|
||||||
|
<Dialog open={isRestartProjectOpen} onOpenChange={setIsRestartProjectOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Restart Project</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{restartingProject && (
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
|
<div className="text-sm font-medium">{restartingProject.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{restartingProject.tasks.length} tasks • {getTotalEstimatedHours(restartingProject)}h estimated
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Planned End Date</label>
|
||||||
|
<Popover open={isEndDateCalendarOpen} onOpenChange={setIsEndDateCalendarOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left"
|
||||||
|
data-testid="button-select-end-date"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{newEndDate ? newEndDate.toLocaleDateString() : "Select planned end date"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={newEndDate}
|
||||||
|
onSelect={(date) => {
|
||||||
|
setNewEndDate(date);
|
||||||
|
setIsEndDateCalendarOpen(false);
|
||||||
|
}}
|
||||||
|
disabled={(date) => date < new Date()}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="clearHistory"
|
||||||
|
checked={clearHistory}
|
||||||
|
onChange={(e) => setClearHistory(e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
data-testid="checkbox-clear-history"
|
||||||
|
/>
|
||||||
|
<label htmlFor="clearHistory" className="text-sm">
|
||||||
|
Clear project history (remove all tasks and start fresh)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsRestartProjectOpen(false);
|
||||||
|
setRestartingProject(null);
|
||||||
|
setNewEndDate(undefined);
|
||||||
|
setClearHistory(false);
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirmRestart}
|
||||||
|
disabled={!newEndDate}
|
||||||
|
className="flex-1"
|
||||||
|
data-testid="button-confirm-restart"
|
||||||
|
>
|
||||||
|
Start Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Create Project Dialog */}
|
{/* Create Project Dialog */}
|
||||||
<Dialog open={isCreateProjectOpen} onOpenChange={setIsCreateProjectOpen}>
|
<Dialog open={isCreateProjectOpen} onOpenChange={setIsCreateProjectOpen}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
|
|||||||
Reference in New Issue
Block a user