import React, { useEffect, useState } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useToast } from "../Shared/ToastContext"; import { PencilSquareIcon, TrashIcon, FolderIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import TaskList from "../Task/TaskList"; import ProjectModal from "../Project/ProjectModal"; import ConfirmDialog from "../Shared/ConfirmDialog"; import { useStore } from "../../store/useStore"; import NewTask from "../Task/NewTask"; import { Project } from "../../entities/Project"; import { PriorityType, Task } from "../../entities/Task"; import { fetchProjectById, updateProject, deleteProject } from "../../utils/projectsService"; import { createTask, updateTask, deleteTask, toggleTaskToday } from "../../utils/tasksService"; import { fetchAreas } from "../../utils/areasService"; import { isAuthError } from "../../utils/authUtils"; import { CalendarDaysIcon, InformationCircleIcon } from "@heroicons/react/24/solid"; import { getAutoSuggestNextActionsEnabled } from "../../utils/profileService"; import AutoSuggestNextActionBox from "./AutoSuggestNextActionBox"; import { useModalEvents } from "../../hooks/useModalEvents"; 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 { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { t, i18n } = useTranslation(); const { showSuccessToast } = useToast(); const areas = useStore((state) => state.areasStore.areas); const [project, setProject] = useState(undefined); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [showCompleted, setShowCompleted] = useState(false); const [showAutoSuggestForm, setShowAutoSuggestForm] = useState(false); // Dispatch global modal events useModalEvents(isModalOpen); useEffect(() => { const loadProjectData = async () => { if (!id) { console.error("Project ID is missing."); return; } setLoading(true); try { fetchAreas(); const projectData = await fetchProjectById(id); setProject(projectData); // Handle both 'tasks' and 'Tasks' property names const projectTasks = projectData.tasks || projectData.Tasks || []; setTasks(projectTasks); } catch (error) { console.error("Error fetching project data:", error); } finally { setLoading(false); } }; loadProjectData(); }, [id, fetchAreas]); // Check if we should show auto-suggest form for projects with no tasks useEffect(() => { const checkAutoSuggest = async () => { if (project && tasks.length === 0 && !loading) { const autoSuggestEnabled = await getAutoSuggestNextActionsEnabled(); if (autoSuggestEnabled) { setShowAutoSuggestForm(true); } } }; checkAutoSuggest(); }, [project, tasks, loading]); const handleTaskCreate = async (taskName: string) => { if (!project) { console.error("Cannot create task: Project is missing"); throw new Error("Cannot create task: Project is missing"); } try { const newTask = await createTask({ name: taskName, status: "not_started", project_id: project.id, }); setTasks((prevTasks) => [...prevTasks, newTask]); // Show success toast with task link const taskLink = ( {t('task.created', 'Task')} {newTask.name} {t('task.createdSuccessfully', 'created successfully!')} ); showSuccessToast(taskLink); } catch (err: any) { console.error("Error creating task:", err); // Check if it's an authentication error if (isAuthError(err)) { return; } throw err; // Re-throw to allow proper error handling by NewTask component } }; const handleTaskUpdate = async (updatedTask: Task) => { if (!updatedTask.id) { console.error("Cannot update task: Task ID is missing"); return; } try { await updateTask(updatedTask.id, updatedTask); setTasks((prevTasks) => prevTasks.map((task) => task.id === updatedTask.id ? updatedTask : task ) ); } catch (err) { console.error("Error updating task:", err); } }; const handleTaskDelete = async (taskId: number | undefined) => { if (!taskId) { console.error("Cannot delete task: Task ID is missing"); return; } try { await deleteTask(taskId); setTasks((prevTasks) => prevTasks.filter((task) => task.id !== taskId)); } catch (err) { console.error("Error deleting task:", err); } }; const handleToggleToday = async (taskId: number): Promise => { try { const updatedTask = await toggleTaskToday(taskId); // Update the task in the local state immediately to avoid UI flashing setTasks(prevTasks => prevTasks.map(task => task.id === taskId ? { ...task, today: updatedTask.today, today_move_count: updatedTask.today_move_count } : task ) ); } catch (error) { console.error("Error toggling task today status:", error); // Optionally refetch data on error to ensure consistency if (id) { try { const updatedProject = await fetchProjectById(id); setProject(updatedProject); setTasks(updatedProject.tasks || []); } catch (refetchError) { console.error("Error refetching project data:", refetchError); } } } }; const handleEditProject = () => { setIsModalOpen(true); }; const handleSaveProject = async (updatedProject: Project) => { if (!updatedProject.id) { console.error("Cannot save project: Project ID is missing"); return; } try { const savedProject = await updateProject(updatedProject.id, updatedProject); setProject(savedProject); setIsModalOpen(false); } catch (err) { console.error("Error saving project:", err); } }; const handleCreateNextAction = async (projectId: number, actionDescription: string) => { try { const newTask = await createTask({ name: actionDescription, status: "not_started", project_id: projectId, priority: "medium" }); // Update the tasks list to include the new task setTasks(prevTasks => [...prevTasks, newTask]); setShowAutoSuggestForm(false); // Show success toast with task link const taskLink = ( {t('task.created', 'Task')} {newTask.name} {t('task.createdSuccessfully', 'created successfully!')} ); showSuccessToast(taskLink); } catch (error) { console.error("Error creating next action:", error); } }; const handleSkipNextAction = () => { setShowAutoSuggestForm(false); }; const handleDeleteProject = async () => { if (!project?.id) { console.error("Cannot delete project: Project ID is missing"); return; } try { await deleteProject(project.id); navigate("/projects"); } catch (err) { console.error("Error deleting project:", err); } }; if (loading) { return (
Loading project details...
); } if (error) { return (
{error}
); } if (!project) { return (
Project not found.
); } const activeTasks = tasks?.filter((task) => { return typeof task.status === 'number' ? task.status !== 2 : task.status !== 'done'; }) || []; //TODO: Also add archived const completedTasks = tasks?.filter((task) => { return typeof task.status === 'number' ? task.status === 2 : task.status === 'done'; }); const displayTasks = showCompleted ? [...activeTasks, ...completedTasks] : activeTasks; const formatProjectDueDate = (dateString: string) => { const date = new Date(dateString); const currentLang = i18n.language; // Format based on language const formatOptions: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }; return date.toLocaleDateString(currentLang, formatOptions); }; return (
{/* Project Banner Image */} {project.image_url && (
{project.name} {/* Title Overlay */}

{project.name}

{/* Priority Indicator on Image */} {project.priority !== undefined && project.priority !== null && (
)} {/* Edit/Delete Buttons on Image */}
)} {/* Project Metadata Box */} {(project.description || project.area || project.due_date_at || (project.tags && project.tags.length > 0)) && (
{project.description && (
Description:

{project.description}

)} {project.area && (
Area: {project.area.name}
)} {project.due_date_at && (
Due Date: {formatProjectDueDate(project.due_date_at)}
)} {project.tags && project.tags.length > 0 && (
Tags:
{project.tags.map((tag, index) => ( ))}
)}
)} {/* Project Header - Only show when no image */} {!project.image_url && (

{project.name}

{/* Show priority indicator only when no image */} {project.priority !== undefined && project.priority !== null && (
)}
)} {!showAutoSuggestForm && (

{t('sidebar.tasks', 'Tasks')}

{completedTasks.length > 0 && ( )}
)} {!showAutoSuggestForm && ( )}
{displayTasks.length > 0 ? ( ) : showAutoSuggestForm ? ( { if (project?.id) { handleCreateNextAction(project.id, actionDescription); } }} onDismiss={handleSkipNextAction} projectName={project?.name || ""} /> ) : (

No tasks.

)}
setIsModalOpen(false)} onSave={handleSaveProject} project={project} areas={areas} /> {isConfirmDialogOpen && ( setIsConfirmDialogOpen(false)} /> )}
); }; const priorityLabel = (priority: PriorityType | number) => { // Handle both string and numeric priorities const normalizedPriority = typeof priority === 'number' ? (['low', 'medium', 'high'][priority] as PriorityType) : priority; switch (normalizedPriority) { case 'high': return 'High'; case 'medium': return 'Medium'; case 'low': return 'Low'; default: return ''; } }; const getPriorityStyle = (priority: PriorityType | number) => { // Handle both string and numeric priorities const normalizedPriority = typeof priority === 'number' ? (['low', 'medium', 'high'][priority] as PriorityType) : priority; return priorityStyles[normalizedPriority] || priorityStyles.default; }; export default ProjectDetails;