From 42aa10b3787ff7a4fdfcd0e8ac03008e1fb089b7 Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Mon, 16 Dec 2024 17:44:40 +0200 Subject: [PATCH] Fix project issues with areas and active filter --- .../components/Project/ProjectDetails.tsx | 92 ++++-- .../components/Project/ProjectItem.tsx | 137 +++++++++ app/frontend/components/Projects.tsx | 280 ++++++++---------- app/frontend/hooks/useFetchProjects.ts | 4 +- app/models/task.rb | 30 +- app/routes/projects_routes.rb | 4 +- public/js/bundle.js | 21 +- 7 files changed, 373 insertions(+), 195 deletions(-) create mode 100644 app/frontend/components/Project/ProjectItem.tsx diff --git a/app/frontend/components/Project/ProjectDetails.tsx b/app/frontend/components/Project/ProjectDetails.tsx index 720e3b09..921f1b30 100644 --- a/app/frontend/components/Project/ProjectDetails.tsx +++ b/app/frontend/components/Project/ProjectDetails.tsx @@ -12,7 +12,16 @@ import ConfirmDialog from "../Shared/ConfirmDialog"; import { useDataContext } from "../../contexts/DataContext"; import NewTask from "../Task/NewTask"; import { Project } from "../../entities/Project"; -import { Task } from "../../entities/Task"; +import { PriorityType, Task } from "../../entities/Task"; + +type PriorityStyles = Record & { default: string }; + +const priorityStyles: PriorityStyles = { + high: 'bg-red-500', + medium: 'bg-yellow-500', + low: 'bg-green-500', + default: 'bg-gray-400', +}; const ProjectDetails: React.FC = () => { const { updateTask, deleteTask, updateProject, deleteProject } = useDataContext(); @@ -21,7 +30,7 @@ const ProjectDetails: React.FC = () => { const { areas } = useDataContext(); - const [project, setProject] = useState(); + const [project, setProject] = useState(undefined); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -56,14 +65,15 @@ const ProjectDetails: React.FC = () => { fetchProject(); }, [id]); - const handleTaskCreate = async (taskData: Partial) => { - if (!project?.id) { - console.error("Project ID is missing"); + const handleTaskCreate = async (taskName: string) => { + if (!project || project.id === undefined) { + console.error("Cannot create task: Project or Project ID is missing"); return; } const taskPayload = { - ...taskData, + name: taskName, + status: "not_started", project_id: project.id, }; @@ -124,10 +134,13 @@ const ProjectDetails: React.FC = () => { }; const handleSaveProject = async (updatedProject: Project) => { - if (!updatedProject) return; + if (!updatedProject || updatedProject.id === undefined) { + console.error("Cannot save project: Project or Project ID is missing"); + return; + } try { - const savedProject = await updateProject(updatedProject.id!, updatedProject); + const savedProject = await updateProject(updatedProject.id, updatedProject); setProject(savedProject); setIsModalOpen(false); } catch (err) { @@ -136,10 +149,13 @@ const ProjectDetails: React.FC = () => { }; const handleDeleteProject = async () => { - if (!project) return; + if (!project || project.id === undefined) { + console.error("Cannot delete project: Project or Project ID is missing"); + return; + } try { - await deleteProject(project.id!); + await deleteProject(project.id); navigate("/projects"); } catch (err) { console.error("Error deleting project:", err); @@ -164,6 +180,14 @@ const ProjectDetails: React.FC = () => { ); } + if (!project) { + return ( +
+
Project not found.
+
+ ); + } + const activeTasks = tasks.filter(task => task.status !== 'done'); const completedTasks = tasks.filter(task => task.status === 'done'); @@ -177,12 +201,21 @@ const ProjectDetails: React.FC = () => { {/* Project Header */}
- -

+ +

{projectTitle}

+ {/* Priority Circle placed after the title */} + {project.priority && ( +
+ )}

+ {/* Edit Project Button */} + {/* Delete Project Button */}
{/* Project Area */} - {project?.area && ( + {project.area && (
{project.area.name.toUpperCase()} @@ -213,7 +247,7 @@ const ProjectDetails: React.FC = () => { )} {/* Project Description */} - {project?.description && ( + {project.description && (

{project.description}

@@ -221,13 +255,7 @@ const ProjectDetails: React.FC = () => { {/* New Task Form */} - handleTaskCreate({ - name: taskName, - status: "not_started", - project_id: project?.id, - }) - } + onTaskCreate={handleTaskCreate} /> {/* Active Tasks */} @@ -250,7 +278,7 @@ const ProjectDetails: React.FC = () => { onClick={toggleCompleted} className="flex items-center justify-between w-full px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-md focus:outline-none" > - Completed Tasks + Completed Tasks { isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onSave={handleSaveProject} - project={project || undefined} + project={project} areas={areas} /> + {/* Confirm Delete Dialog */} {isConfirmDialogOpen && ( setIsConfirmDialogOpen(false)} /> @@ -308,4 +337,17 @@ const ProjectDetails: React.FC = () => { ); }; +const priorityLabel = (priority: PriorityType) => { + switch (priority) { + case 'high': + return 'High'; + case 'medium': + return 'Medium'; + case 'low': + return 'Low'; + default: + return ''; + } +}; + export default ProjectDetails; diff --git a/app/frontend/components/Project/ProjectItem.tsx b/app/frontend/components/Project/ProjectItem.tsx new file mode 100644 index 00000000..6a9ac53e --- /dev/null +++ b/app/frontend/components/Project/ProjectItem.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { EllipsisVerticalIcon } from "@heroicons/react/24/solid"; +import { Project } from "../../entities/Project"; + +interface ProjectItemProps { + project: Project; + viewMode: "cards" | "list"; + color: string; + getCompletionPercentage: (projectId: number | undefined) => number; + activeDropdown: number | null; + setActiveDropdown: React.Dispatch>; + handleEditProject: (project: Project) => void; + setProjectToDelete: React.Dispatch>; + setIsConfirmDialogOpen: React.Dispatch>; +} + +const getProjectInitials = (name: string) => { + const words = name + .trim() + .split(" ") + .filter((word) => word.length > 0); + if (words.length === 1) { + return name.toUpperCase(); + } + return words.map((word) => word[0].toUpperCase()).join(""); +}; + +const ProjectItem: React.FC = ({ + project, + viewMode, + color, + getCompletionPercentage, + activeDropdown, + setActiveDropdown, + handleEditProject, + setProjectToDelete, + setIsConfirmDialogOpen, +}) => { + return ( +
+ {viewMode === "cards" && ( +
+ + {getProjectInitials(project.name)} + +
+
+ )} + +
+ + {project.name} + +
+ + + {activeDropdown === project.id && ( +
+ + +
+ )} +
+
+ + {viewMode === "cards" && ( +
+
+
+
+
+ + {getCompletionPercentage(project?.id)}% + +
+
+ )} +
+ ); +}; + +export default ProjectItem; diff --git a/app/frontend/components/Projects.tsx b/app/frontend/components/Projects.tsx index 0fd09ab2..b1766341 100644 --- a/app/frontend/components/Projects.tsx +++ b/app/frontend/components/Projects.tsx @@ -1,43 +1,46 @@ import React, { useState, useEffect } from "react"; import { Project } from "../entities/Project"; -import { Link, useSearchParams } from "react-router-dom"; import { - EllipsisVerticalIcon, MagnifyingGlassIcon, - FolderIcon + FolderIcon, + Squares2X2Icon, + Bars3Icon, } from "@heroicons/react/24/solid"; import ConfirmDialog from "./Shared/ConfirmDialog"; import ProjectModal from "./Project/ProjectModal"; import { useDataContext } from "../contexts/DataContext"; import useFetchProjects from "../hooks/useFetchProjects"; - -const getProjectInitials = (name: string) => { - const words = name - .trim() - .split(" ") - .filter((word) => word.length > 0); - if (words.length === 1) { - return name.toUpperCase(); +import { PriorityType, StatusType } from "../entities/Task"; +import { useSearchParams } from "react-router-dom"; +import ProjectItem from "./Project/ProjectItem"; + +type ProjectTaskCounts = Record; + +const getPriorityStyles = (priority: PriorityType) => { + switch (priority) { + case "low": + return { color: "bg-green-500" }; + case "medium": + return { color: "bg-yellow-500" }; + case "high": + return { color: "bg-red-500" }; + default: + return { color: "bg-gray-500" }; } - return words.map((word) => word[0].toUpperCase()).join(""); }; const Projects: React.FC = () => { - const { areas, createProject, updateProject, deleteProject } = - useDataContext(); - const [taskStatusCounts, setTaskStatusCounts] = useState>( - {} - ); + const { areas, createProject, updateProject, deleteProject } = useDataContext(); + const [taskStatusCounts, setTaskStatusCounts] = useState>({}); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [projectToEdit, setProjectToEdit] = useState(null); const [projectToDelete, setProjectToDelete] = useState(null); - const [isConfirmDialogOpen, setIsConfirmDialogOpen] = - useState(false); + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [activeDropdown, setActiveDropdown] = useState(null); const [searchQuery, setSearchQuery] = useState(""); + const [viewMode, setViewMode] = useState<"cards" | "list">("cards"); const [searchParams, setSearchParams] = useSearchParams(); - const activeFilter = searchParams.get("active") || "all"; const areaFilter = searchParams.get("area_id") || ""; @@ -55,14 +58,14 @@ const Projects: React.FC = () => { const getCompletionPercentage = (projectId: number | undefined) => { if (!projectId) return 0; - const taskStatus = taskStatusCounts[projectId] || {}; - const totalTasks = - (taskStatus.done || 0) + - (taskStatus.not_started || 0) + - (taskStatus.in_progress || 0); - + const taskStatus = taskStatusCounts[projectId] || { + not_started: 0, + in_progress: 0, + done: 0, + archived: 0, + }; + const totalTasks = taskStatus.done + taskStatus.not_started + taskStatus.in_progress; if (totalTasks === 0) return 0; - return Math.round((taskStatus.done / totalTasks) * 100); }; @@ -89,9 +92,7 @@ const Projects: React.FC = () => { mutate(); }; - const handleActiveFilterChange = ( - e: React.ChangeEvent - ) => { + const handleActiveFilterChange = (e: React.ChangeEvent) => { const newActiveFilter = e.target.value; const params = new URLSearchParams(searchParams); @@ -100,7 +101,6 @@ const Projects: React.FC = () => { } else { params.set("active", newActiveFilter); } - setSearchParams(params); }; @@ -159,51 +159,79 @@ const Projects: React.FC = () => {
- {/* Filters for Active Status and Area */} -
-
-