diff --git a/client/src/components/task-center/task-center.component.jsx b/client/src/components/task-center/task-center.component.jsx index 7fb07ef2c..1b8587623 100644 --- a/client/src/components/task-center/task-center.component.jsx +++ b/client/src/components/task-center/task-center.component.jsx @@ -1,95 +1,88 @@ import { Virtuoso } from "react-virtuoso"; -import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd"; -import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons"; +import { Badge, Spin, Typography } from "antd"; import { useTranslation } from "react-i18next"; -import { forwardRef, useEffect, useRef } from "react"; +import { forwardRef, useMemo, useRef } from "react"; import { DateTimeFormat } from "../../utils/DateFormatter.jsx"; import day from "../../utils/day.js"; import "./task-center.styles.scss"; const { Text, Title } = Typography; -const TaskCenterComponent = forwardRef( - ({ visible, tasks, loading, showIncompleteOnly, toggleIncomplete, markAllComplete, onTaskClick }, ref) => { - const { t } = useTranslation(); - const virtuosoRef = useRef(null); +const TaskCenterComponent = forwardRef(({ visible, tasks, loading, onTaskClick }, ref) => { + const { t } = useTranslation(); + const virtuosoRef = useRef(null); - useEffect(() => { - if (virtuosoRef.current) { - virtuosoRef.current.scrollToIndex({ index: 0, behavior: "smooth" }); - } - }, [showIncompleteOnly]); + // Organize tasks into sections + const sections = useMemo(() => { + const now = day(); + const today = now.startOf("day"); - // Filter tasks based on showIncompleteOnly - const filteredTasks = showIncompleteOnly ? tasks.filter((task) => !task.completed) : tasks; + const overdue = tasks.filter((task) => task.due_date && day(task.due_date).isBefore(today)); + const dueToday = tasks.filter((task) => task.due_date && day(task.due_date).isSame(today, "day")); + const upcoming = tasks.filter((task) => task.due_date && day(task.due_date).isAfter(today)); + const noDueDate = tasks.filter((task) => !task.due_date); - const renderTask = (index, task) => { - const handleClick = () => { - onTaskClick(task.id); // Use the prop handler - }; + return [ + { title: t("tasks.labels.overdue"), data: overdue }, + { title: t("tasks.labels.due_today"), data: dueToday }, + { title: t("tasks.labels.upcoming"), data: upcoming }, + { title: t("tasks.labels.no_due_date"), data: noDueDate } + ].filter((section) => section.data.length > 0); + }, [tasks, t]); - return ( -
- -
- - <span className="ro-number"> - {t("notifications.labels.ro-number", { - ro_number: task.job?.ro_number || t("general.labels.na") - })} - </span> - <Text type="secondary" className="relative-time" title={DateTimeFormat(task.created_at)}> - {day(task.created_at).fromNow()} - </Text> - - - {task.title} - -
-
-
- ); + const renderTask = (index, task) => { + const handleClick = () => { + onTaskClick(task.id); }; return ( -
-
- -

{t("tasks.labels.my_tasks_center")}

- {loading && } -
-
- - - {showIncompleteOnly ? : } - toggleIncomplete(checked)} size="small" /> - - - -
); - } -); + }; + + const renderSection = (section, sectionIndex) => ( +
+ + {section.title} + + {section.data.map((task, index) => renderTask(`${sectionIndex}-${index}`, task))} +
+ ); + + return ( +
+
+

{t("tasks.labels.my_tasks_center")}

+ {loading && } +
+ renderSection(section, index)} + /> +
+ ); +}); TaskCenterComponent.displayName = "TaskCenterComponent"; diff --git a/client/src/components/task-center/task-center.container.jsx b/client/src/components/task-center/task-center.container.jsx index dad0f97e7..cfecedf75 100644 --- a/client/src/components/task-center/task-center.container.jsx +++ b/client/src/components/task-center/task-center.container.jsx @@ -1,15 +1,14 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { useMutation, useQuery } from "@apollo/client"; +import { useQuery } from "@apollo/client"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { useSocket } from "../../contexts/SocketIO/useSocket"; import { useIsEmployee } from "../../utils/useIsEmployee"; -import { useNotification } from "../../contexts/Notifications/notificationContext"; -import { MUTATION_TOGGLE_TASK_COMPLETED, QUERY_MY_TASKS_PAGINATED } from "../../graphql/tasks.queries"; +import { QUERY_MY_TASKS_PAGINATED } from "../../graphql/tasks.queries"; import TaskCenterComponent from "./task-center.component"; import dayjs from "../../utils/day"; -import { setModalContext } from "../../redux/modals/modals.actions"; // Import setModalContext +import { setModalContext } from "../../redux/modals/modals.actions"; const POLL_INTERVAL = 60; // seconds @@ -24,20 +23,28 @@ const mapDispatchToProps = (dispatch) => ({ const TaskCenterContainer = ({ visible, onClose, bodyshop, currentUser, setTaskUpsertContext }) => { const [tasks, setTasks] = useState([]); - const [showIncompleteOnly, setShowIncompleteOnly] = useState(true); const [loading, setLoading] = useState(false); const { isConnected } = useSocket(); const isEmployee = useIsEmployee(bodyshop, currentUser); - const notification = useNotification(); - const assignedToId = bodyshop?.employees?.find((e) => e.user_email === currentUser.email)?.id; - const where = useMemo(() => { - return { + // Compute assignedToId with useMemo to ensure stability + const assignedToId = useMemo(() => { + const employee = bodyshop?.employees?.find((e) => e.user_email === currentUser?.email); + if (employee?.id) { + console.log("AssignedToId computed:", employee.id); // Debug log + return employee.id; + } + return null; + }, [bodyshop, currentUser]); + + const where = useMemo( + () => ({ assigned_to: { _eq: assignedToId }, deleted: { _eq: false }, - ...(showIncompleteOnly ? { completed: { _eq: false } } : {}) - }; - }, [assignedToId, showIncompleteOnly]); + completed: { _eq: false } + }), + [assignedToId] + ); const { data, @@ -50,49 +57,24 @@ const TaskCenterContainer = ({ visible, onClose, bodyshop, currentUser, setTaskU where, offset: 0, limit: 50, - order: [{ created_at: "desc" }] + order: [{ due_date: "asc_nulls_last" }, { created_at: "desc" }] }, - skip: !bodyshop?.id || !assignedToId || !isEmployee, + // Skip query if any required data is missing + skip: !bodyshop?.id || !assignedToId || !isEmployee || !currentUser?.email, fetchPolicy: "cache-and-network", - pollInterval: isConnected ? 0 : dayjs.duration(POLL_INTERVAL, "seconds").asMilliseconds() + pollInterval: isConnected ? 0 : dayjs.duration(POLL_INTERVAL, "seconds").asMilliseconds(), + // Log errors for debugging + onError: (error) => { + console.error("Query error:", error); + } }); - const [toggleTaskCompleted] = useMutation(MUTATION_TOGGLE_TASK_COMPLETED); - useEffect(() => { if (data?.tasks) { setTasks(data.tasks); } }, [data]); - const handleToggleIncomplete = (val) => { - setShowIncompleteOnly(val); - }; - - const handleMarkAllComplete = async () => { - setLoading(true); - try { - const incompleteTasks = tasks.filter((t) => !t.completed); - await Promise.all( - incompleteTasks.map((task) => - toggleTaskCompleted({ - variables: { - id: task.id, - completed: true, - completed_at: dayjs().toISOString() - } - }) - ) - ); - notification.success({ message: "Tasks marked complete" }); - refetch(); - } catch (err) { - notification.error({ message: "Failed to mark tasks complete" }); - } finally { - setLoading(false); - } - }; - const handleTaskClick = useCallback( (id) => { const task = tasks.find((t) => t.id === id); @@ -105,7 +87,7 @@ const TaskCenterContainer = ({ visible, onClose, bodyshop, currentUser, setTaskU }); } }, - [tasks, setModalContext] + [tasks, setTaskUpsertContext] ); return ( @@ -114,10 +96,7 @@ const TaskCenterContainer = ({ visible, onClose, bodyshop, currentUser, setTaskU onClose={onClose} tasks={tasks} loading={loading || queryLoading} - showIncompleteOnly={showIncompleteOnly} - toggleIncomplete={handleToggleIncomplete} - markAllComplete={handleMarkAllComplete} - onTaskClick={handleTaskClick} // Pass the updated handler + onTaskClick={handleTaskClick} /> ); }; diff --git a/client/src/components/task-center/task-center.styles.scss b/client/src/components/task-center/task-center.styles.scss index 2970f4b39..0f62c1306 100644 --- a/client/src/components/task-center/task-center.styles.scss +++ b/client/src/components/task-center/task-center.styles.scss @@ -18,7 +18,7 @@ } .task-header { - padding: 4px 16px; + padding: 8px 16px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; @@ -30,57 +30,20 @@ font-size: 14px; color: rgba(0, 0, 0, 0.85); } + } - .task-controls { - display: flex; - align-items: center; - gap: 8px; + .task-section { + margin-bottom: 8px; + } - .task-toggle { - align-items: center; - - .anticon { - font-size: 14px; - color: #1677ff; - vertical-align: middle; - } - } - - .ant-switch { - &.ant-switch-small { - min-width: 28px; - height: 16px; - line-height: 16px; - - .ant-switch-handle { - width: 12px; - height: 12px; - } - - &.ant-switch-checked { - background-color: #1677ff; - - .ant-switch-handle { - left: calc(100% - 14px); - } - } - } - } - - .ant-btn-link { - padding: 0; - color: #1677ff; - - &:hover { - color: #69b1ff; - } - - &:disabled { - color: rgba(0, 0, 0, 0.25); - cursor: not-allowed; - } - } - } + .section-title { + padding: 8px 16px; + background: #f5f5f5; + margin: 0; + font-size: 14px; + color: rgba(0, 0, 0, 0.85); + font-weight: 500; + border-bottom: 1px solid #e8e8e8; } .task-item { @@ -91,21 +54,12 @@ width: 100%; box-sizing: border-box; cursor: pointer; + background: #fff; &:hover { background: #fafafa; } - &.task-completed { - background: #fff; - color: rgba(0, 0, 0, 0.65); - } - - &.task-incomplete { - background: #f5f5f5; - color: rgba(0, 0, 0, 0.85); - } - .task-content { width: 100%; } @@ -137,7 +91,8 @@ .task-body { margin-top: 4px; - color: inherit; + color: rgba(0, 0, 0, 0.85); + font-weight: 500; } } diff --git a/client/src/graphql/tasks.queries.js b/client/src/graphql/tasks.queries.js index 554c21a18..bd4f8e3e8 100644 --- a/client/src/graphql/tasks.queries.js +++ b/client/src/graphql/tasks.queries.js @@ -287,6 +287,27 @@ export const QUERY_JOB_TASKS_PAGINATED = gql` } `; +export const QUERY_MY_COMPLETE_TASKS_PAGINATED = gql` + ${PARTIAL_TASK_FIELDS} + query QUERY_MY_TASKS_PAGINATED( + $offset: Int + $limit: Int + $assigned_to: uuid! + $bodyshop: uuid! + $where: tasks_bool_exp + $order: [tasks_order_by!]! + ) { + tasks(offset: $offset, limit: $limit, order_by: $order, where: $where) { + ...TaskFields + } + tasks_aggregate(where: $where) { + aggregate { + count + } + } + } +`; + export const QUERY_MY_TASKS_PAGINATED = gql` ${PARTIAL_TASK_FIELDS} query QUERY_MY_TASKS_PAGINATED(