diff --git a/app/frontend/components/Project/ProjectDetails.tsx b/app/frontend/components/Project/ProjectDetails.tsx index 720e3b0..921f1b3 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 0000000..6a9ac53 --- /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/Project/ProjectModal.tsx b/app/frontend/components/Project/ProjectModal.tsx index e4727d9..6e014cc 100644 --- a/app/frontend/components/Project/ProjectModal.tsx +++ b/app/frontend/components/Project/ProjectModal.tsx @@ -5,6 +5,9 @@ import ConfirmDialog from "../Shared/ConfirmDialog"; import { useToast } from "../Shared/ToastContext"; import TagInput from "../Tag/TagInput"; import useFetchTags from "../../hooks/useFetchTags"; +import PriorityDropdown from "../Shared/PriorityDropdown"; +import { PriorityType } from "../../entities/Task"; +import Switch from "../Shared/Switch"; interface ProjectModalProps { isOpen: boolean; @@ -30,12 +33,19 @@ const ProjectModal: React.FC = ({ area_id: null, active: true, tags: [], + priority: "low", } ); - const [tags, setTags] = useState(project?.tags?.map(tag => tag.name) || []); + const [tags, setTags] = useState( + project?.tags?.map((tag) => tag.name) || [] + ); - const { tags: availableTags, isLoading: isTagsLoading, isError: isTagsError } = useFetchTags(); + const { + tags: availableTags, + isLoading: isTagsLoading, + isError: isTagsError, + } = useFetchTags(); const modalRef = useRef(null); const [isClosing, setIsClosing] = useState(false); @@ -49,7 +59,7 @@ const ProjectModal: React.FC = ({ ...project, tags: project.tags || [], }); - setTags(project.tags?.map(tag => tag.name) || []); + setTags(project.tags?.map((tag) => tag.name) || []); } else { setFormData({ name: "", @@ -127,7 +137,7 @@ const ProjectModal: React.FC = ({ }, []); const handleSubmit = () => { - onSave({ ...formData, tags: tags.map(name => ({ name })) }); + onSave({ ...formData, tags: tags.map((name) => ({ name })) }); showSuccessToast( project ? "Project updated successfully!" @@ -154,7 +164,14 @@ const ProjectModal: React.FC = ({ setTimeout(() => { onClose(); setIsClosing(false); - }, 300); + }, 300); + }; + + const handleToggleActive = () => { + setFormData((prev) => ({ + ...prev, + active: !prev.active, + })); }; if (!isOpen) return null; @@ -232,6 +249,18 @@ const ProjectModal: React.FC = ({ >
+
+ + + setFormData({ ...formData, priority: value }) + } + /> +
+ {/* Tags */}