157 lines
5.4 KiB
JavaScript
157 lines
5.4 KiB
JavaScript
import { Virtuoso } from "react-virtuoso";
|
|
import { Badge, Button, Spin } from "antd";
|
|
import { useTranslation } from "react-i18next";
|
|
import { forwardRef, useMemo, useRef } from "react";
|
|
import day from "../../utils/day.js";
|
|
import "./task-center.styles.scss";
|
|
import {
|
|
ArrowRightOutlined,
|
|
CalendarOutlined,
|
|
ClockCircleOutlined,
|
|
PlusCircleOutlined,
|
|
QuestionCircleOutlined
|
|
} from "@ant-design/icons";
|
|
|
|
const TaskCenterComponent = forwardRef(
|
|
({ visible, tasks, loading, error, onTaskClick, onLoadMore, hasMore, createNewTask, incompleteTaskCount }, ref) => {
|
|
const { t } = useTranslation();
|
|
const virtuosoRef = useRef(null);
|
|
|
|
const sectionIcons = {
|
|
[t("tasks.labels.overdue")]: <ClockCircleOutlined style={{ marginRight: 8 }} />,
|
|
[t("tasks.labels.due_today")]: <CalendarOutlined style={{ marginRight: 8 }} />,
|
|
[t("tasks.labels.upcoming")]: <ArrowRightOutlined style={{ marginRight: 8 }} />,
|
|
[t("tasks.labels.no_due_date")]: <QuestionCircleOutlined style={{ marginRight: 8 }} />
|
|
};
|
|
|
|
const groups = useMemo(() => {
|
|
const now = day();
|
|
const today = now.startOf("day");
|
|
|
|
const overdue = tasks.filter((t) => t.due_date && day(t.due_date).isBefore(today));
|
|
const dueToday = tasks.filter((t) => t.due_date && day(t.due_date).isSame(today, "day"));
|
|
const upcoming = tasks.filter(
|
|
(t) => t.due_date && day(t.due_date).isAfter(today) && !day(t.due_date).isSame(today, "day")
|
|
);
|
|
const noDueDate = tasks.filter((t) => !t.due_date);
|
|
|
|
return [
|
|
{ label: t("tasks.labels.overdue"), tasks: overdue },
|
|
{ label: t("tasks.labels.due_today"), tasks: dueToday },
|
|
{ label: t("tasks.labels.upcoming"), tasks: upcoming },
|
|
{ label: t("tasks.labels.no_due_date"), tasks: noDueDate }
|
|
].filter((group) => group.tasks.length > 0);
|
|
}, [tasks, t]);
|
|
|
|
const groupCounts = useMemo(() => groups.map((group) => group.tasks.length), [groups]);
|
|
|
|
const flatTasks = useMemo(() => groups.flatMap((group) => group.tasks), [groups]);
|
|
|
|
const priorityColors = {
|
|
1: "red",
|
|
2: "orange",
|
|
3: "green"
|
|
};
|
|
|
|
const getPriorityColor = (priority) => priorityColors[priority] || null;
|
|
|
|
const groupContent = (groupIndex) => {
|
|
const { label, tasks } = groups[groupIndex];
|
|
let displayCount = tasks.length;
|
|
if (label === t("tasks.labels.no_due_date")) {
|
|
displayCount =
|
|
incompleteTaskCount -
|
|
groups.reduce((sum, group, idx) => (idx !== groupIndex ? sum + group.tasks.length : sum), 0);
|
|
}
|
|
return (
|
|
<div className="section-title">
|
|
{sectionIcons[label]}
|
|
{label} ({displayCount})
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const itemContent = (index) => {
|
|
const task = flatTasks[index];
|
|
const priorityColor = getPriorityColor(task.priority);
|
|
return (
|
|
<div
|
|
className="task-row"
|
|
onClick={() => onTaskClick(task.id)}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
onTaskClick(task.id);
|
|
}
|
|
}}
|
|
>
|
|
<div className="task-title-cell">
|
|
<div className="task-row-container">
|
|
<div className="task-title">{task.title}</div>
|
|
<div className="task-ro-number">
|
|
{t("tasks.labels.ro-number", {
|
|
ro_number: task.job?.ro_number || t("general.labels.na")
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="task-due-cell">
|
|
{task.due_date && <span>{day(task.due_date).fromNow()}</span>}
|
|
{!!priorityColor && <Badge color={priorityColor} dot style={{ marginLeft: 6 }} />}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<div className={`task-center ${visible ? "visible" : ""}`} ref={ref}>
|
|
<div className="task-header">
|
|
<h3>{t("tasks.labels.my_tasks_center")}</h3>
|
|
</div>
|
|
<div className="error-message">{t("tasks.errors.load_failed")}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`task-center ${visible ? "visible" : ""}`} ref={ref}>
|
|
<div className="task-header">
|
|
<Badge count={incompleteTaskCount} size="medium" offset={[13, -5]}>
|
|
<h3>{t("tasks.labels.my_tasks_center")}</h3>
|
|
</Badge>
|
|
<div className="task-header-actions">
|
|
<Button className="create-task-button" type="link" icon={<PlusCircleOutlined />} onClick={createNewTask} />
|
|
{loading && <Spin spinning={loading} size="small" />}
|
|
</div>
|
|
</div>
|
|
|
|
{tasks.length === 0 && !loading ? (
|
|
<div className="no-tasks-message">{t("tasks.labels.no_tasks")}</div>
|
|
) : (
|
|
<Virtuoso
|
|
ref={virtuosoRef}
|
|
style={{ height: "550px", width: "100%" }}
|
|
groupCounts={groupCounts}
|
|
groupContent={groupContent}
|
|
itemContent={itemContent}
|
|
endReached={hasMore && !loading ? onLoadMore : undefined}
|
|
components={{
|
|
Footer: () =>
|
|
loading ? (
|
|
<div className="loading-footer">
|
|
<Spin />
|
|
</div>
|
|
) : null
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
TaskCenterComponent.displayName = "TaskCenterComponent";
|
|
export default TaskCenterComponent;
|