track time
This commit is contained in:
@@ -2,4 +2,4 @@ VITE_TOKEN_NAME = 'admin_token'
|
|||||||
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||||
VITE_BASE_URL = 'https://api.danakcorp.com'
|
VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||||
|
|
||||||
VITE_BASE_URL = 'http://192.168.99.131:3500'
|
#VITE_BASE_URL = 'http://192.168.99.131:3500'
|
||||||
+7
-1
@@ -1034,7 +1034,13 @@
|
|||||||
"or": "یا",
|
"or": "یا",
|
||||||
"insert": "درج",
|
"insert": "درج",
|
||||||
"start_date": "تاریخ شروع",
|
"start_date": "تاریخ شروع",
|
||||||
"end_date": "تاریخ پایان"
|
"end_date": "تاریخ پایان",
|
||||||
|
"time_tracking": "ثبت زمان",
|
||||||
|
"start_tracking": "شروع",
|
||||||
|
"stop_tracking": "پایان",
|
||||||
|
"time_history": "سابقه",
|
||||||
|
"no_time_entries": "هنوز زمانی ثبت نشده",
|
||||||
|
"tracking_active": "در حال اجرا"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Calendar, Paperclip2, Profile2User, Tag, TickSquare, type Icon } from "iconsax-react";
|
import { Calendar, Paperclip2, Profile2User, Tag, TickSquare, Timer1, type Icon } from "iconsax-react";
|
||||||
import { type FC, type ReactNode, useRef } from "react";
|
import { type FC, type ReactNode, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import TaskDetailPopoverPanel from "./TaskDetailPopoverPanel";
|
import TaskDetailPopoverPanel from "./TaskDetailPopoverPanel";
|
||||||
@@ -16,6 +16,7 @@ const TABS: TabItem[] = [
|
|||||||
{ id: "checklist", icon: TickSquare, labelKey: "taskmanager.task_detail.checklist" },
|
{ id: "checklist", icon: TickSquare, labelKey: "taskmanager.task_detail.checklist" },
|
||||||
{ id: "attachment", icon: Paperclip2, labelKey: "taskmanager.task_detail.attachment" },
|
{ id: "attachment", icon: Paperclip2, labelKey: "taskmanager.task_detail.attachment" },
|
||||||
{ id: "date", icon: Calendar, labelKey: "taskmanager.task_detail.date_settings" },
|
{ id: "date", icon: Calendar, labelKey: "taskmanager.task_detail.date_settings" },
|
||||||
|
{ id: "time", icon: Timer1, labelKey: "taskmanager.task_detail.time_tracking" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -26,13 +27,14 @@ type Props = {
|
|||||||
checklistPopover?: ReactNode;
|
checklistPopover?: ReactNode;
|
||||||
attachmentPopover?: ReactNode;
|
attachmentPopover?: ReactNode;
|
||||||
datePopover?: ReactNode;
|
datePopover?: ReactNode;
|
||||||
|
timePopover?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const POPOVER_TABS = new Set<TaskDetailTab>(["labels", "users", "checklist", "attachment", "date"]);
|
const POPOVER_TABS = new Set<TaskDetailTab>(["labels", "users", "checklist", "attachment", "date", "time"]);
|
||||||
|
|
||||||
type PopoverTab = "labels" | "users" | "checklist" | "attachment" | "date";
|
type PopoverTab = "labels" | "users" | "checklist" | "attachment" | "date" | "time";
|
||||||
|
|
||||||
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover, datePopover }) => {
|
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover, usersPopover, checklistPopover, attachmentPopover, datePopover, timePopover }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const anchorRefs = useRef<Record<PopoverTab, { current: HTMLElement | null }>>({
|
const anchorRefs = useRef<Record<PopoverTab, { current: HTMLElement | null }>>({
|
||||||
labels: { current: null },
|
labels: { current: null },
|
||||||
@@ -40,6 +42,7 @@ const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover,
|
|||||||
checklist: { current: null },
|
checklist: { current: null },
|
||||||
attachment: { current: null },
|
attachment: { current: null },
|
||||||
date: { current: null },
|
date: { current: null },
|
||||||
|
time: { current: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
const popoverByTab: Record<PopoverTab, ReactNode> = {
|
const popoverByTab: Record<PopoverTab, ReactNode> = {
|
||||||
@@ -48,6 +51,7 @@ const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover,
|
|||||||
checklist: checklistPopover,
|
checklist: checklistPopover,
|
||||||
attachment: attachmentPopover,
|
attachment: attachmentPopover,
|
||||||
date: datePopover,
|
date: datePopover,
|
||||||
|
time: timePopover,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import DefaulModal from "../../../../components/DefaulModal";
|
|||||||
import PageLoading from "../../../../components/PageLoading";
|
import PageLoading from "../../../../components/PageLoading";
|
||||||
import { ErrorType } from "../../../../helpers/types";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
import { useChangeTaskPhase, useDeleteTask, useGetTaskDetail, useUpdateTask } from "../../task/hooks/useTaskData";
|
import { useChangeTaskPhase, useDeleteTask, useGetTaskDetail, useUpdateTask } from "../../task/hooks/useTaskData";
|
||||||
|
import { useGetTaskTimes } from "../../task/hooks/useTaskTimeData";
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
||||||
import type { Column, Task } from "../../types";
|
import type { Column, Task } from "../../types";
|
||||||
import AttachmentList from "./attachment/List";
|
import AttachmentList from "./attachment/List";
|
||||||
import CheckLists from "./checklist/List";
|
import CheckLists from "./checklist/List";
|
||||||
|
import TimeList from "./time/List";
|
||||||
import TaskDetailCoverColorBar from "./cover/TaskDetailCoverColorBar";
|
import TaskDetailCoverColorBar from "./cover/TaskDetailCoverColorBar";
|
||||||
import TaskDetailDescription from "./TaskDetailDescription";
|
import TaskDetailDescription from "./TaskDetailDescription";
|
||||||
import TaskDetailHeader from "./TaskDetailHeader";
|
import TaskDetailHeader from "./TaskDetailHeader";
|
||||||
@@ -34,6 +36,7 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
const initialDescriptionRef = useRef("");
|
const initialDescriptionRef = useRef("");
|
||||||
|
|
||||||
const getTaskDetail = useGetTaskDetail(task?.id ?? "", open && !!task?.id);
|
const getTaskDetail = useGetTaskDetail(task?.id ?? "", open && !!task?.id);
|
||||||
|
const getTaskTimes = useGetTaskTimes(task?.id ?? "", open && !!task?.id);
|
||||||
const updateTask = useUpdateTask();
|
const updateTask = useUpdateTask();
|
||||||
const changeTaskPhase = useChangeTaskPhase();
|
const changeTaskPhase = useChangeTaskPhase();
|
||||||
const deleteTask = useDeleteTask();
|
const deleteTask = useDeleteTask();
|
||||||
@@ -212,6 +215,8 @@ const TaskDetailModal: FC<Props> = ({ open, task, columns, projectId, onClose, o
|
|||||||
checkListItemCount={taskDetail?.checkListItemCount}
|
checkListItemCount={taskDetail?.checkListItemCount}
|
||||||
completedCheckListItemCount={taskDetail?.completedCheckListItemCount}
|
completedCheckListItemCount={taskDetail?.completedCheckListItemCount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TimeList times={getTaskTimes.data?.data} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DefaulModal>
|
</DefaulModal>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import TaskDetailActionBar from "./TaskDetailActionBar";
|
|||||||
import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview";
|
||||||
import type { TaskDetailTab } from "./types";
|
import type { TaskDetailTab } from "./types";
|
||||||
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
import TaskDetailUsersPopover from "./users/TaskDetailUsersPopover";
|
||||||
|
import TaskDetailTimePopover from "./time/TaskDetailTimePopover";
|
||||||
import type { TaskUser } from "./users/types";
|
import type { TaskUser } from "./users/types";
|
||||||
import type { UserItemType } from "../../../users/types/UserTypes";
|
import type { UserItemType } from "../../../users/types/UserTypes";
|
||||||
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
||||||
@@ -93,6 +94,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
const isChecklistOpen = activeTab === "checklist";
|
const isChecklistOpen = activeTab === "checklist";
|
||||||
const isAttachmentOpen = activeTab === "attachment";
|
const isAttachmentOpen = activeTab === "attachment";
|
||||||
const isDateOpen = activeTab === "date";
|
const isDateOpen = activeTab === "date";
|
||||||
|
const isTimeOpen = activeTab === "time";
|
||||||
|
|
||||||
const handleTabChange = (tab: TaskDetailTab) => {
|
const handleTabChange = (tab: TaskDetailTab) => {
|
||||||
if (tab === "labels" && activeTab !== "labels") {
|
if (tab === "labels" && activeTab !== "labels") {
|
||||||
@@ -340,6 +342,7 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
timePopover={isTimeOpen ? <TaskDetailTimePopover taskId={taskId} /> : null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TaskDetailMetadataPreview
|
<TaskDetailMetadataPreview
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { type FC, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { TaskTimeEntryType } from "../../../task/types/TaskTimeTypes";
|
||||||
|
import TimeListItem from "./TimeListItem";
|
||||||
|
import { getActiveTimeEntry, sortTaskTimes } from "./utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
times?: TaskTimeEntryType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimeList: FC<Props> = ({ times = [] }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const sortedTimes = useMemo(() => sortTaskTimes(times), [times]);
|
||||||
|
const activeEntry = useMemo(() => getActiveTimeEntry(times), [times]);
|
||||||
|
|
||||||
|
if (times.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="text-sm font-bold">{t("taskmanager.task_detail.time_tracking")}</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
|
{sortedTimes.map((entry) => (
|
||||||
|
<TimeListItem key={entry.id} entry={entry} isActive={activeEntry?.id === entry.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeList;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { type FC, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { ErrorType } from "../../../../../helpers/types";
|
||||||
|
import { useEndTimeTracking, useGetTaskTimes, useStartTimeTracking } from "../../../task/hooks/useTaskTimeData";
|
||||||
|
import { formatElapsedTime, getActiveTimeEntry, getElapsedSeconds } from "./utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
taskId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskDetailTimePopover: FC<Props> = ({ taskId }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const getTaskTimes = useGetTaskTimes(taskId);
|
||||||
|
const startTimeTracking = useStartTimeTracking();
|
||||||
|
const endTimeTracking = useEndTimeTracking();
|
||||||
|
|
||||||
|
const times = useMemo(() => getTaskTimes.data?.data ?? [], [getTaskTimes.data]);
|
||||||
|
const activeEntry = useMemo(() => getActiveTimeEntry(times), [times]);
|
||||||
|
const isTracking = !!activeEntry;
|
||||||
|
const isSubmitting = startTimeTracking.isPending || endTimeTracking.isPending;
|
||||||
|
|
||||||
|
const [elapsedSeconds, setElapsedSeconds] = useState(() =>
|
||||||
|
activeEntry ? getElapsedSeconds(activeEntry.startedAt) : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeEntry) {
|
||||||
|
setElapsedSeconds(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateElapsed = () => {
|
||||||
|
setElapsedSeconds(getElapsedSeconds(activeEntry.startedAt));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateElapsed();
|
||||||
|
const intervalId = window.setInterval(updateElapsed, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [activeEntry?.id, activeEntry?.startedAt]);
|
||||||
|
|
||||||
|
const handleStart = () => {
|
||||||
|
startTimeTracking.mutate(taskId, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("success"));
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error.message[0]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = () => {
|
||||||
|
endTimeTracking.mutate(taskId, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("success"));
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error.message[0]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-9 w-full items-center">
|
||||||
|
{isTracking ? (
|
||||||
|
<div className="flex w-full items-center justify-between gap-3">
|
||||||
|
<span className="font-mono text-sm font-bold tracking-wider text-[#01347A]">
|
||||||
|
{formatElapsedTime(elapsedSeconds)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleStop}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="shrink-0 rounded-lg bg-[#FF4D4F] px-3 py-1.5 text-[11px] font-medium text-white disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{t("taskmanager.task_detail.stop_tracking")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleStart}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full rounded-lg bg-primary px-3 py-2 text-[11px] font-medium text-white disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{t("taskmanager.task_detail.start_tracking")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskDetailTimePopover;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import moment from "moment-jalaali";
|
||||||
|
import { type FC, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { TaskTimeEntryType } from "../../../task/types/TaskTimeTypes";
|
||||||
|
import { formatElapsedTime, getEntryDurationSeconds } from "./utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: TaskTimeEntryType;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimeListItem: FC<Props> = ({ entry, isActive }) => {
|
||||||
|
const { t } = useTranslation("global");
|
||||||
|
const [durationSeconds, setDurationSeconds] = useState(() => getEntryDurationSeconds(entry));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
setDurationSeconds(getEntryDurationSeconds(entry));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDuration = () => {
|
||||||
|
setDurationSeconds(getEntryDurationSeconds(entry));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDuration();
|
||||||
|
const intervalId = window.setInterval(updateDuration, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [entry.startedAt, entry.endedAt, entry.duration, isActive]);
|
||||||
|
|
||||||
|
const startLabel = moment(entry.startedAt).format("jYYYY/jMM/jDD HH:mm");
|
||||||
|
const endLabel = entry.endedAt ? moment(entry.endedAt).format("jYYYY/jMM/jDD HH:mm") : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 h-[27px] items-center">
|
||||||
|
<div className="group flex justify-between flex-1 h-full hover:bg-white hover:bg-opacity-10 items-center px-2 relative">
|
||||||
|
<div className="text-xs truncate">
|
||||||
|
{startLabel}
|
||||||
|
{endLabel ? ` - ${endLabel}` : ` (${t("taskmanager.task_detail.tracking_active")})`}
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs font-bold text-[#01347A] shrink-0 mr-2">
|
||||||
|
{formatElapsedTime(durationSeconds)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeListItem;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export const formatElapsedTime = (totalSeconds: number): string => {
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
return [hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getElapsedSeconds = (startedAt: string, endedAt?: string | null): number => {
|
||||||
|
const start = new Date(startedAt).getTime();
|
||||||
|
if (Number.isNaN(start)) return 0;
|
||||||
|
|
||||||
|
const end = endedAt ? new Date(endedAt).getTime() : Date.now();
|
||||||
|
if (Number.isNaN(end)) return 0;
|
||||||
|
|
||||||
|
return Math.max(0, Math.floor((end - start) / 1000));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveTimeEntry = <T extends { startedAt: string; endedAt?: string | null }>(times: T[]): T | null => {
|
||||||
|
return times.find((entry) => entry.startedAt && !entry.endedAt) ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntryDurationSeconds = (entry: { startedAt: string; endedAt?: string | null; duration?: number | null }): number => {
|
||||||
|
if (entry.endedAt) {
|
||||||
|
return entry.duration ?? getElapsedSeconds(entry.startedAt, entry.endedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getElapsedSeconds(entry.startedAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortTaskTimes = <T extends { startedAt: string }>(times: T[]): T[] => {
|
||||||
|
return [...times].sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
||||||
|
};
|
||||||
@@ -1 +1 @@
|
|||||||
export type TaskDetailTab = "labels" | "users" | "checklist" | "attachment" | "date";
|
export type TaskDetailTab = "labels" | "users" | "checklist" | "attachment" | "date" | "time";
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/TaskTimeService";
|
||||||
|
|
||||||
|
export const useGetTaskTimes = (taskId: string, enabled = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["task-times", taskId],
|
||||||
|
queryFn: () => api.getTaskTimes(taskId),
|
||||||
|
enabled: !!taskId && enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStartTimeTracking = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (taskId: string) => api.startTimeTracking(taskId),
|
||||||
|
onSuccess: (_data, taskId) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-times", taskId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEndTimeTracking = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (taskId: string) => api.endTimeTracking(taskId),
|
||||||
|
onSuccess: (_data, taskId) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-times", taskId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import axios from "../../../../config/axios";
|
||||||
|
import { EndTaskTimeResponse, GetTaskTimesResponse, StartTaskTimeResponse } from "../types/TaskTimeTypes";
|
||||||
|
|
||||||
|
export const getTaskTimes = async (taskId: string): Promise<GetTaskTimesResponse> => {
|
||||||
|
const { data } = await axios.get(`/task-manager/times/task/${taskId}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startTimeTracking = async (taskId: string): Promise<StartTaskTimeResponse> => {
|
||||||
|
const { data } = await axios.post(`/task-manager/times/start/${taskId}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const endTimeTracking = async (taskId: string): Promise<EndTaskTimeResponse> => {
|
||||||
|
const { data } = await axios.post(`/task-manager/times/end/${taskId}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { IResponse } from "../../../../types/response.types";
|
||||||
|
|
||||||
|
export type TaskTimeEntryType = {
|
||||||
|
id: string;
|
||||||
|
taskId: string;
|
||||||
|
startedAt: string;
|
||||||
|
endedAt?: string | null;
|
||||||
|
duration?: number | null;
|
||||||
|
adminId?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StartTaskTimeResponse extends IResponse<TaskTimeEntryType> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EndTaskTimeResponse extends IResponse<TaskTimeEntryType> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetTaskTimesResponse extends IResponse<TaskTimeEntryType[]> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
@@ -10,18 +10,13 @@ import HeaderWorkspace from "./components/HeaderWorkspace";
|
|||||||
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
||||||
import { useGetProject } from "./project/hooks/useProjectData";
|
import { useGetProject } from "./project/hooks/useProjectData";
|
||||||
import { useUpdateTaskPhase } from "./task-phase/hooks/useTaskPhaseData";
|
import { useUpdateTaskPhase } from "./task-phase/hooks/useTaskPhaseData";
|
||||||
import { useChangeTaskPhase, useChangeTaskPriority } from "./task/hooks/useTaskData";
|
|
||||||
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
import type { TaskPhaseItemType } from "./task-phase/types/TaskPhaseTypes";
|
||||||
|
import { useChangeTaskPhase, useChangeTaskPriority } from "./task/hooks/useTaskData";
|
||||||
import type { Column as ColumnType, Task } from "./types";
|
import type { Column as ColumnType, Task } from "./types";
|
||||||
import { getPriorityDestinationTaskId } from "./utils/getPriorityDestinationTaskId";
|
import { getPriorityDestinationTaskId } from "./utils/getPriorityDestinationTaskId";
|
||||||
import { reorderColumns } from "./utils/reorderColumns";
|
import { reorderColumns } from "./utils/reorderColumns";
|
||||||
import { reorderTaskItems } from "./utils/reorderTaskItems";
|
import { reorderTaskItems } from "./utils/reorderTaskItems";
|
||||||
import {
|
import { getTaskPhaseItems, insertTaskPhaseItem, updateTaskPhaseItems, type TasksByTaskPhaseInfiniteData } from "./utils/taskPhaseQueryCache";
|
||||||
getTaskPhaseItems,
|
|
||||||
insertTaskPhaseItem,
|
|
||||||
type TasksByTaskPhaseInfiniteData,
|
|
||||||
updateTaskPhaseItems,
|
|
||||||
} from "./utils/taskPhaseQueryCache";
|
|
||||||
|
|
||||||
const Workspace: FC = () => {
|
const Workspace: FC = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -68,18 +63,13 @@ const Workspace: FC = () => {
|
|||||||
setDraggedTask(null);
|
setDraggedTask(null);
|
||||||
|
|
||||||
if (!columnChanged) {
|
if (!columnChanged) {
|
||||||
const previousData = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>([
|
const previousData = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId]);
|
||||||
"tasks-by-task-phase",
|
|
||||||
columnId,
|
|
||||||
]);
|
|
||||||
const currentItems = getTaskPhaseItems(previousData);
|
const currentItems = getTaskPhaseItems(previousData);
|
||||||
const destTaskId = getPriorityDestinationTaskId(currentItems, activeTask.id, index);
|
const destTaskId = getPriorityDestinationTaskId(currentItems, activeTask.id, index);
|
||||||
|
|
||||||
if (!destTaskId) return;
|
if (!destTaskId) return;
|
||||||
|
|
||||||
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
|
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) => updateTaskPhaseItems(current, (items) => reorderTaskItems(items, activeTask.id, index)));
|
||||||
updateTaskPhaseItems(current, (items) => reorderTaskItems(items, activeTask.id, index)),
|
|
||||||
);
|
|
||||||
|
|
||||||
changeTaskPriority.mutate(
|
changeTaskPriority.mutate(
|
||||||
{ srcId: activeTask.id, destId: destTaskId, taskPhaseId: columnId, projectId },
|
{ srcId: activeTask.id, destId: destTaskId, taskPhaseId: columnId, projectId },
|
||||||
@@ -95,41 +85,34 @@ const Workspace: FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousSource = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>([
|
const previousSource = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId]);
|
||||||
"tasks-by-task-phase",
|
const previousDest = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId]);
|
||||||
sourceColumnId,
|
|
||||||
]);
|
|
||||||
const previousDest = queryClient.getQueryData<TasksByTaskPhaseInfiniteData>([
|
|
||||||
"tasks-by-task-phase",
|
|
||||||
columnId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId], (current) =>
|
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", sourceColumnId], (current) =>
|
||||||
updateTaskPhaseItems(current, (items) => items.filter((task) => task.id !== activeTask.id)),
|
updateTaskPhaseItems(current, (items) => items.filter((task) => task.id !== activeTask.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
|
queryClient.setQueryData<TasksByTaskPhaseInfiniteData>(["tasks-by-task-phase", columnId], (current) =>
|
||||||
insertTaskPhaseItem(current, {
|
insertTaskPhaseItem(
|
||||||
id: activeTask.id,
|
current,
|
||||||
title: activeTask.title,
|
{
|
||||||
taskPhaseId: columnId,
|
id: activeTask.id,
|
||||||
order: index,
|
title: activeTask.title,
|
||||||
priority: index,
|
taskPhaseId: columnId,
|
||||||
tag: activeTask.tag,
|
order: index,
|
||||||
dateRange: activeTask.dateRange,
|
priority: index,
|
||||||
color: activeTask.color,
|
tag: activeTask.tag,
|
||||||
attachments: activeTask.attachments,
|
dateRange: activeTask.dateRange,
|
||||||
checklistDone: activeTask.checklistDone,
|
color: activeTask.color,
|
||||||
checklistTotal: activeTask.checklistTotal,
|
attachments: activeTask.attachments,
|
||||||
}, index),
|
checklistDone: activeTask.checklistDone,
|
||||||
|
checklistTotal: activeTask.checklistTotal,
|
||||||
|
},
|
||||||
|
index,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const destTaskId = getPriorityDestinationTaskId(
|
const destTaskId = getPriorityDestinationTaskId(getTaskPhaseItems(previousDest), activeTask.id, index, false);
|
||||||
getTaskPhaseItems(previousDest),
|
|
||||||
activeTask.id,
|
|
||||||
index,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedTask?.id === activeTask.id) {
|
if (selectedTask?.id === activeTask.id) {
|
||||||
setSelectedTask({ ...activeTask, columnId });
|
setSelectedTask({ ...activeTask, columnId });
|
||||||
@@ -214,16 +197,10 @@ const Workspace: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextColumnOrder = columns.length
|
const nextColumnOrder = columns.length ? Math.max(...columns.map((column) => column.order ?? 0)) + 1 : 1;
|
||||||
? Math.max(...columns.map((column) => column.order ?? 0)) + 1
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
const handleColumnCreated = (phase: TaskPhaseItemType) => {
|
const handleColumnCreated = (phase: TaskPhaseItemType) => {
|
||||||
setColumns((prev) =>
|
setColumns((prev) => [...prev, { id: phase.id, title: phase.name, color: phase.color, order: phase.order }].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)));
|
||||||
[...prev, { id: phase.id, title: phase.name, color: phase.color, order: phase.order }].sort(
|
|
||||||
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleColumnDeleted = (columnId: string) => {
|
const handleColumnDeleted = (columnId: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user