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, BookOpenIcon, TagIcon, ListBulletIcon, } 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 { Note } from '../../entities/Note'; import { fetchProjectById, updateProject, deleteProject, } from '../../utils/projectsService'; import { createTask, 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'; 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 [notes, setNotes] = useState([]); const [loading, setLoading] = useState(true); const [error] = 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 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); // Handle project notes const projectNotes = projectData.notes || projectData.Notes || []; setNotes(projectNotes); } 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 { // Use direct fetch call like Tasks.tsx to ensure proper tag saving const response = await fetch(`/api/task/${updatedTask.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(updatedTask), }); if (!response.ok) { const errorData = await response.json(); console.error('Failed to update task:', errorData.error); throw new Error('Failed to update task'); } const savedTask = await response.json(); setTasks((prevTasks) => prevTasks.map((task) => task.id === updatedTask.id ? savedTask : 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.

)}
{/* Notes Section */}

{t('sidebar.notes', 'Notes')}

{notes.length > 0 ? (
{notes.map((note) => (
{note.title || t( 'notes.untitled', 'Untitled Note' )} {note.content && (

{note.content.length > 150 ? note.content.substring( 0, 150 ) + '...' : note.content}

)} {note.tags && note.tags.length > 0 && (
{note.tags .map( (tag) => tag.name ) .join(', ')}
)}
))}
) : (

{t('project.noNotes', 'No notes for this project.')}

)}
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;