Add ability to create projects and copy tasks from templates

Adds functionality to create new projects, copying existing project tasks into the global task list. Includes a helper function to convert project-specific tasks to a general format and integrates this into the project creation process.

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/P9UdTaE
This commit is contained in:
paul-nothaft
2025-09-11 22:57:58 +00:00
parent 6e8ef8a091
commit 266ee2bdc8
3 changed files with 68 additions and 21 deletions
+4 -4
View File
@@ -18,14 +18,14 @@ externalPort = 80
localPort = 35345
externalPort = 3002
[[ports]]
localPort = 40247
externalPort = 3001
[[ports]]
localPort = 41353
externalPort = 3000
[[ports]]
localPort = 44261
externalPort = 3003
[env]
PORT = "5000"
+1
View File
@@ -194,6 +194,7 @@ function App() {
return (
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
onCreateTasks={(newTasks) => setTasks(prev => [...prev, ...newTasks])}
/>
);
+63 -17
View File
@@ -3,13 +3,13 @@ import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Plus, Calendar as CalendarIcon, Clock, Copy, Play, Tag, Edit, Trash2, Palette, FileText } from 'lucide-react';
import { Label } from '@shared/schema';
import { Label, Task } from '@shared/schema';
import TaskDetailsModal from '@/components/TaskDetailsModal';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@/lib/queryClient';
@@ -20,6 +20,23 @@ const getStartOfToday = () => {
return new Date(today.getFullYear(), today.getMonth(), today.getDate());
};
// Helper function to convert ProjectTask to global Task format
const convertProjectTaskToTask = (projectTask: ProjectTask, projectId: string): Task => {
return {
id: `project-${projectId}-task-${projectTask.id}`, // Generate unique ID
title: projectTask.title,
description: projectTask.description || null,
status: projectTask.status,
priority: projectTask.priority,
dueDate: null, // Projects tasks don't have due dates by default
timeTracked: projectTask.timeTracked || 0,
isTracking: false,
projectId: projectId,
notes: projectTask.notes || null,
labelId: projectTask.labelId || null
};
};
interface TimeEntry {
id: string;
date: Date;
@@ -55,6 +72,7 @@ interface Project {
interface ProjectTemplateProps {
onCreateFromTemplate?: (templateId: string, startDate: Date, projectName: string) => void;
onCreateTemplate?: (template: any) => void;
onCreateTasks?: (tasks: Task[]) => void;
}
const defaultProjects: Project[] = [
@@ -103,7 +121,8 @@ const defaultProjects: Project[] = [
export default function ProjectTemplate({
onCreateFromTemplate,
onCreateTemplate
onCreateTemplate,
onCreateTasks
}: ProjectTemplateProps) {
const [projects, setProjects] = useState<Project[]>(defaultProjects);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
@@ -180,8 +199,9 @@ export default function ProjectTemplate({
const handleCreateProject = () => {
if (newProjectName.trim()) {
const newProjectId = Date.now().toString();
const newProject: Project = {
id: Date.now().toString(),
id: newProjectId,
name: newProjectName.trim(),
description: newProjectDescription.trim() || undefined,
status: 'planning',
@@ -190,7 +210,19 @@ export default function ProjectTemplate({
createdAt: new Date()
};
setProjects([...projects, newProject]);
// Add project to projects list
setProjects(currentProjects => [...currentProjects, newProject]);
// Convert project tasks to global tasks and add them to global state
if (projectTasks.length > 0 && onCreateTasks) {
const globalTasks = projectTasks.map(task =>
convertProjectTaskToTask(task, newProjectId)
);
onCreateTasks(globalTasks);
console.log('Added', globalTasks.length, 'tasks to global state for project:', newProject.name);
}
// Reset form state
setNewProjectName('');
setNewProjectDescription('');
setNewPlannedEndDate(undefined);
@@ -208,12 +240,12 @@ export default function ProjectTemplate({
const handleDeleteProject = (projectId: string) => {
if (confirm('Are you sure you want to delete this project?')) {
setProjects(projects.filter(p => p.id !== projectId));
setProjects(currentProjects => currentProjects.filter(p => p.id !== projectId));
}
};
const handleProjectStatusChange = (projectId: string, newStatus: Project['status']) => {
setProjects(projects.map(project =>
setProjects(currentProjects => currentProjects.map(project =>
project.id === projectId ? { ...project, status: newStatus } : project
));
};
@@ -247,6 +279,14 @@ export default function ProjectTemplate({
// Create a new project as a duplicate/copy of the closed project
const newProjectId = Date.now().toString();
const duplicatedTasks = clearHistory
? []
: restartingProject.tasks.map(task => ({
...task,
id: `${newProjectId}-task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // Generate new task ID
status: 'todo' as const // Reset all tasks to todo status
}));
const duplicatedProject: Project = {
...restartingProject,
id: newProjectId, // New unique ID
@@ -255,17 +295,20 @@ export default function ProjectTemplate({
endDate: undefined, // Clear end date since it's active again
plannedEndDate: newEndDate, // Set the new planned end date
createdAt: new Date(), // Set new creation date
tasks: clearHistory
? []
: restartingProject.tasks.map(task => ({
...task,
id: `${newProjectId}-task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // Generate new task ID
status: 'todo' as const // Reset all tasks to todo status
}))
tasks: duplicatedTasks
};
// Add the new project to the projects array without removing the original
setProjects([...projects, duplicatedProject]);
setProjects(currentProjects => [...currentProjects, duplicatedProject]);
// Convert project tasks to global tasks and add them to global state
if (duplicatedTasks.length > 0 && onCreateTasks) {
const globalTasks = duplicatedTasks.map(task =>
convertProjectTaskToTask(task, newProjectId)
);
onCreateTasks(globalTasks);
console.log('Added', globalTasks.length, 'tasks to global state for duplicated project:', duplicatedProject.name);
}
setIsRestartProjectOpen(false);
setRestartingProject(null);
@@ -407,7 +450,7 @@ export default function ProjectTemplate({
setSelectedProject(updatedProject);
// Update the projects array
setProjects(projects.map(p =>
setProjects(currentProjects => currentProjects.map(p =>
p.id === selectedProject.id ? updatedProject : p
));
}
@@ -421,7 +464,7 @@ export default function ProjectTemplate({
tasks: projectTasks
};
setProjects(projects.map(p =>
setProjects(currentProjects => currentProjects.map(p =>
p.id === selectedProject.id ? updatedProject : p
));
@@ -784,6 +827,9 @@ export default function ProjectTemplate({
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
<DialogDescription>
Create a new project with tasks, timeline, and organize your work efficiently.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Project Details */}