tududi/frontend/components/Task/TasksToday.tsx
2025-06-21 08:24:51 +03:00

379 lines
13 KiB
TypeScript

import React, { useEffect, useState, useCallback } from "react";
import { format } from "date-fns";
import { el, enUS, es, ja, uk, de } from "date-fns/locale";
import { useTranslation } from "react-i18next";
import i18n from "i18next";
import { Link } from "react-router-dom";
import {
ClipboardDocumentListIcon,
ArrowPathIcon,
CalendarDaysIcon,
ClockIcon,
InboxIcon,
FolderIcon,
ArchiveBoxIcon,
} from "@heroicons/react/24/outline";
import { fetchTasks, updateTask, deleteTask } from "../../utils/tasksService";
import { fetchProjects } from "../../utils/projectsService";
import { loadInboxItemsToStore } from "../../utils/inboxService";
import { Task } from "../../entities/Task";
import { useStore } from "../../store/useStore";
import TaskList from "./TaskList";
import { Metrics } from "../../entities/Metrics";
import ProductivityAssistant from "../Productivity/ProductivityAssistant";
import { getProductivityAssistantEnabled } from "../../utils/profileService";
const getLocale = (language: string) => {
switch (language) {
case 'el':
return el;
case 'es':
return es;
case 'jp':
return ja;
case 'ua':
return uk;
case 'de':
return de;
default:
return enUS;
}
};
const TasksToday: React.FC = () => {
const { t } = useTranslation();
// Don't use multiple separate useStore calls - combine them into one
const store = useStore();
// Use local state for data instead of directly using store state
// This prevents unnecessary re-renders from store updates
const [localTasks, setLocalTasks] = useState<Task[]>([]);
const [localProjects, setLocalProjects] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [productivityAssistantEnabled, setProductivityAssistantEnabled] = useState(true);
// Metrics from the API
const [metrics, setMetrics] = useState<Metrics>({
total_open_tasks: 0,
tasks_pending_over_month: 0,
tasks_in_progress_count: 0,
tasks_in_progress: [],
tasks_due_today: [],
suggested_tasks: [],
});
// Track mounting state to prevent state updates after unmount
const isMounted = React.useRef(false);
// Load data once on component mount
useEffect(() => {
isMounted.current = true;
// Only fetch data once on mount
const loadData = async () => {
if (!isMounted.current) return;
setIsLoading(true);
setIsError(false);
try {
// Load inbox items to ensure the notification appears correctly
loadInboxItemsToStore();
} catch (error) {
console.error("Failed to load inbox items:", error);
}
try {
// Load productivity assistant setting
const isEnabled = await getProductivityAssistantEnabled();
if (isMounted.current) {
setProductivityAssistantEnabled(isEnabled);
}
} catch (error) {
console.error("Failed to load productivity assistant setting:", error);
}
try {
// Load projects first
const projectsData = await fetchProjects();
if (isMounted.current) {
const safeProjectsData = Array.isArray(projectsData) ? projectsData : [];
setLocalProjects(safeProjectsData);
store.projectsStore.setProjects(safeProjectsData);
}
} catch (error) {
if (isMounted.current) {
setLocalProjects([]);
setIsError(true);
}
}
try {
// Load tasks with metrics
const { tasks: fetchedTasks, metrics: fetchedMetrics } = await fetchTasks("?type=today");
if (isMounted.current) {
setLocalTasks(fetchedTasks);
setMetrics(fetchedMetrics);
// Also update the store
store.tasksStore.setTasks(fetchedTasks);
}
} catch (error) {
console.error("Failed to fetch tasks:", error);
if (isMounted.current) {
setIsError(true);
}
} finally {
if (isMounted.current) {
setIsLoading(false);
}
}
};
loadData();
// Cleanup function to prevent state updates after unmount
return () => {
isMounted.current = false;
};
}, []); // Empty dependency array - only run once on mount
// Memoize task handlers to prevent recreating functions on each render
const handleTaskUpdate = useCallback(async (updatedTask: Task): Promise<void> => {
if (!updatedTask.id || !isMounted.current) return;
setIsLoading(true);
try {
await updateTask(updatedTask.id, updatedTask);
// Refetch data to ensure consistency
const { tasks: updatedTasks, metrics } = await fetchTasks("?type=today");
if (isMounted.current) {
setLocalTasks(updatedTasks);
setMetrics(metrics);
// Update store
store.tasksStore.setTasks(updatedTasks);
}
} catch (error) {
console.error("Error updating task:", error);
if (isMounted.current) {
setIsError(true);
}
} finally {
if (isMounted.current) {
setIsLoading(false);
}
}
}, [store.tasksStore]);
const handleTaskDelete = useCallback(async (taskId: number): Promise<void> => {
if (!isMounted.current) return;
setIsLoading(true);
try {
await deleteTask(taskId);
// Refetch data to ensure consistency
const { tasks: updatedTasks, metrics } = await fetchTasks("?type=today");
if (isMounted.current) {
setLocalTasks(updatedTasks);
setMetrics(metrics);
// Update store
store.tasksStore.setTasks(updatedTasks);
}
} catch (error) {
console.error("Error deleting task:", error);
if (isMounted.current) {
setIsError(true);
}
} finally {
if (isMounted.current) {
setIsLoading(false);
}
}
}, [store.tasksStore]);
// Get inbox items count from store for the notification
const inboxItemsCount = store.inboxStore.inboxItems.length;
// Show loading state
if (isLoading && localTasks.length === 0) {
return (
<div className="flex justify-center items-center h-64">
<p className="text-gray-500 dark:text-gray-400">{t('common.loading', 'Loading...')}</p>
</div>
);
}
// Show error state
if (isError && localTasks.length === 0) {
return (
<div className="flex justify-center items-center h-64">
<p className="text-red-500">{t('errors.somethingWentWrong', 'Something went wrong')}</p>
</div>
);
}
return (
<div className="flex justify-center px-4 lg:px-2">
<div className="w-full max-w-5xl">
<div className="flex items-center mb-8">
<h2 className="text-2xl font-light flex items-center">
<CalendarDaysIcon className="h-5 w-5 mr-2" /> {t('tasks.today')}
</h2>
<span className="ml-4 text-gray-500">
{format(new Date(), "PPP", { locale: getLocale(i18n.language) })}
</span>
</div>
<div className="mb-6 grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Task Metrics */}
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-4">
<h3 className="text-lg font-medium mb-3 text-gray-700 dark:text-gray-300">{t('tasks.metrics', 'Tasks')}</h3>
<div className="grid grid-cols-2 gap-4">
{/* Left column */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center">
<ClipboardDocumentListIcon className="h-6 w-6 text-blue-500 mr-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">{t('tasks.backlog')}</p>
</div>
<p className="text-xl font-semibold">
{metrics.total_open_tasks}
</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<ArrowPathIcon className="h-6 w-6 text-green-500 mr-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">{t('tasks.inProgress')}</p>
</div>
<p className="text-xl font-semibold">
{metrics.tasks_in_progress_count}
</p>
</div>
</div>
{/* Right column */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center">
<CalendarDaysIcon className="h-6 w-6 text-red-500 mr-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">{t('tasks.dueToday')}</p>
</div>
<p className="text-xl font-semibold">
{metrics.tasks_due_today.length}
</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<ClockIcon className="h-6 w-6 text-yellow-500 mr-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">{t('tasks.stale')}</p>
</div>
<p className="text-xl font-semibold">
{metrics.tasks_pending_over_month}
</p>
</div>
</div>
</div>
</div>
{/* Project Metrics */}
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-4">
<h3 className="text-lg font-medium mb-3 text-gray-700 dark:text-gray-300">{t('projects.metrics', 'Projects')}</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center">
<FolderIcon className="h-6 w-6 text-blue-500 mr-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">{t('projects.active')}</p>
</div>
<p className="text-xl font-semibold">
{Array.isArray(localProjects) ? localProjects.filter(project => project.active).length : 0}
</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<ArchiveBoxIcon className="h-6 w-6 text-gray-500 mr-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">{t('projects.inactive')}</p>
</div>
<p className="text-xl font-semibold">
{Array.isArray(localProjects) ? localProjects.filter(project => !project.active).length : 0}
</p>
</div>
</div>
</div>
</div>
{/* Inbox Notification */}
{inboxItemsCount > 0 && (
<div className="mb-2 p-4 bg-white dark:bg-gray-900 border-l-4 border-blue-500 rounded-lg shadow">
<Link to="/inbox" className="flex items-center">
<InboxIcon className="h-6 w-6 text-blue-500 dark:text-blue-400 mr-3" />
<div>
<p className="text-gray-700 dark:text-gray-300 font-medium">
{t('inbox.unprocessedItems', { count: inboxItemsCount, defaultValue: `You have ${inboxItemsCount} item(s) in your inbox.` })}
</p>
<p className="text-blue-600 dark:text-blue-400 text-sm">
{t('inbox.processNow', 'Process them now')}
</p>
</div>
</Link>
</div>
)}
{/* Productivity Assistant */}
{productivityAssistantEnabled && (
<ProductivityAssistant tasks={localTasks} projects={localProjects} />
)}
{metrics.tasks_due_today.length > 0 && (
<>
<h3 className="text-xl font-medium mt-6 mb-2">{t('tasks.dueToday')}</h3>
<TaskList
tasks={metrics.tasks_due_today}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
/>
</>
)}
{metrics.tasks_in_progress.length > 0 && (
<>
<h3 className="text-xl font-medium mt-6 mb-2">{t('tasks.inProgress')}</h3>
<TaskList
tasks={metrics.tasks_in_progress}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
/>
</>
)}
{metrics.suggested_tasks.length > 0 && (
<>
<h3 className="text-xl font-medium mt-6 mb-2">{t('tasks.suggested')}</h3>
<TaskList
tasks={metrics.suggested_tasks}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
/>
</>
)}
{metrics.tasks_due_today.length === 0 &&
metrics.tasks_in_progress.length === 0 &&
metrics.suggested_tasks.length === 0 && (
<p className="text-gray-500 text-center mt-4">
{t('tasks.noTasksAvailable')}
</p>
)}
</div>
</div>
);
};
export default TasksToday;