check list
This commit is contained in:
@@ -150,7 +150,11 @@ const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
|
|||||||
|
|
||||||
<AttachmentList attachments={taskDetail?.attachments} />
|
<AttachmentList attachments={taskDetail?.attachments} />
|
||||||
|
|
||||||
<CheckLists checklists={taskDetail?.checklists} />
|
<CheckLists
|
||||||
|
checkListItems={taskDetail?.checkListItems}
|
||||||
|
checkListItemCount={taskDetail?.checkListItemCount}
|
||||||
|
completedCheckListItemCount={taskDetail?.completedCheckListItemCount}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DefaulModal>
|
</DefaulModal>
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import type DateObject from "react-date-object";
|
|||||||
import DateObjectClass from "react-date-object";
|
import DateObjectClass from "react-date-object";
|
||||||
import persian from "react-date-object/calendars/persian";
|
import persian from "react-date-object/calendars/persian";
|
||||||
import persian_fa from "react-date-object/locales/persian_fa";
|
import persian_fa from "react-date-object/locales/persian_fa";
|
||||||
import { DEFAULT_CHECKLISTS } from "./checklist/constants";
|
|
||||||
import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover";
|
import TaskDetailChecklistPopover from "./checklist/TaskDetailChecklistPopover";
|
||||||
import type { TaskChecklist } from "./checklist/types";
|
|
||||||
import TaskDetailLabelsPopover from "./labels/TaskDetailLabelsPopover";
|
import TaskDetailLabelsPopover from "./labels/TaskDetailLabelsPopover";
|
||||||
import type { LabelsView, TaskLabel } from "./labels/types";
|
import type { LabelsView, TaskLabel } from "./labels/types";
|
||||||
import TaskDetailActionBar from "./TaskDetailActionBar";
|
import TaskDetailActionBar from "./TaskDetailActionBar";
|
||||||
@@ -19,7 +17,7 @@ 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 type { TaskDetailType } from "../../task/types/TaskTypes";
|
import type { TaskDetailType } from "../../task/types/TaskTypes";
|
||||||
import { useCreateTaskRemark, useUpdateTask } from "../../task/hooks/useTaskData";
|
import { useCreateCheckListItem, useCreateTaskRemark, useUpdateTask } from "../../task/hooks/useTaskData";
|
||||||
import { ErrorType } from "../../../../helpers/types";
|
import { ErrorType } from "../../../../helpers/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -38,13 +36,13 @@ const parseDate = (value?: string): DateObject | null => {
|
|||||||
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, taskId }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const createTaskRemark = useCreateTaskRemark();
|
const createTaskRemark = useCreateTaskRemark();
|
||||||
|
const createCheckListItem = useCreateCheckListItem();
|
||||||
const updateTask = useUpdateTask();
|
const updateTask = useUpdateTask();
|
||||||
const getUsers = useGetUsers("");
|
const getUsers = useGetUsers("");
|
||||||
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
const [labelsView, setLabelsView] = useState<LabelsView>("list");
|
||||||
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
const [labels, setLabels] = useState<TaskLabel[]>([]);
|
||||||
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
const [selectedLabelIds, setSelectedLabelIds] = useState<Set<string>>(new Set());
|
||||||
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set());
|
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set());
|
||||||
const [checklists, setChecklists] = useState<TaskChecklist[]>(DEFAULT_CHECKLISTS);
|
|
||||||
const [startDate, setStartDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.startDate);
|
const [startDate, setStartDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.startDate);
|
||||||
const [endDate, setEndDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.endDate);
|
const [endDate, setEndDate] = useState<DateObject | null>(DEFAULT_DATE_RANGE.endDate);
|
||||||
|
|
||||||
@@ -67,10 +65,6 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
setSelectedUserIds(new Set());
|
setSelectedUserIds(new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskDetail.checklists?.length) {
|
|
||||||
setChecklists(taskDetail.checklists.map((checklist) => ({ id: checklist.id, title: checklist.title })));
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedStartDate = parseDate(taskDetail.startDate);
|
const parsedStartDate = parseDate(taskDetail.startDate);
|
||||||
const parsedEndDate = parseDate(taskDetail.endDate);
|
const parsedEndDate = parseDate(taskDetail.endDate);
|
||||||
if (parsedStartDate) setStartDate(parsedStartDate);
|
if (parsedStartDate) setStartDate(parsedStartDate);
|
||||||
@@ -188,13 +182,22 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
if (isChecklistOpen) onTabChange("checklist");
|
if (isChecklistOpen) onTabChange("checklist");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChecklistCreate = (title: string, _copyFromId: string) => {
|
const handleChecklistCreate = (title: string) => {
|
||||||
const newChecklist: TaskChecklist = {
|
const effectiveTaskId = taskId || taskDetail?.id;
|
||||||
id: crypto.randomUUID(),
|
if (!effectiveTaskId) return;
|
||||||
title,
|
|
||||||
};
|
createCheckListItem.mutate(
|
||||||
setChecklists((prev) => [...prev, newChecklist]);
|
{ title, isDone: false, taskId: effectiveTaskId },
|
||||||
handleChecklistClose();
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleChecklistClose();
|
||||||
|
toast.success(t("success"));
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error.message[0]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAttachmentClose = () => {
|
const handleAttachmentClose = () => {
|
||||||
@@ -247,9 +250,9 @@ const TaskDetailToolbar: FC<Props> = ({ activeTab, onTabChange, taskDetail, task
|
|||||||
checklistPopover={
|
checklistPopover={
|
||||||
isChecklistOpen ? (
|
isChecklistOpen ? (
|
||||||
<TaskDetailChecklistPopover
|
<TaskDetailChecklistPopover
|
||||||
checklists={checklists}
|
|
||||||
onClose={handleChecklistClose}
|
onClose={handleChecklistClose}
|
||||||
onSubmit={handleChecklistCreate}
|
onSubmit={handleChecklistCreate}
|
||||||
|
isSubmitting={createCheckListItem.isPending}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
import { Edit } from "iconsax-react";
|
import { Edit } from "iconsax-react";
|
||||||
import ProgressBar from "../../../../../components/ProgressBar";
|
import ProgressBar from "../../../../../components/ProgressBar";
|
||||||
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
import TrashWithConfrim from "../../../../../components/TrashWithConfrim";
|
||||||
import type { TaskChecklistType } from "../../../task/types/TaskTypes";
|
import type { TaskChecklistItemType } from "../../../task/types/TaskTypes";
|
||||||
import ChecklistItem from "./ChecklistItem";
|
import ChecklistItem from "./ChecklistItem";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
checklists?: TaskChecklistType[];
|
checkListItems?: TaskChecklistItemType[];
|
||||||
|
checkListItemCount?: number;
|
||||||
|
completedCheckListItemCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CheckLists = ({ checklists = [] }: Props) => {
|
const CheckLists = ({
|
||||||
const items = checklists.flatMap((checklist) =>
|
checkListItems = [],
|
||||||
checklist.items.map((item) => ({
|
checkListItemCount,
|
||||||
id: item.id,
|
completedCheckListItemCount,
|
||||||
title: item.title,
|
}: Props) => {
|
||||||
checked: item.isDone,
|
const items = checkListItems.map((item) => ({
|
||||||
})),
|
id: item.id,
|
||||||
);
|
title: item.title,
|
||||||
|
checked: item.isDone,
|
||||||
|
}));
|
||||||
|
|
||||||
const doneCount = items.filter((item) => item.checked).length;
|
const totalCount = checkListItemCount ?? items.length;
|
||||||
const progress = items.length > 0 ? Math.round((doneCount / items.length) * 100) : 0;
|
const doneCount = completedCheckListItemCount ?? items.filter((item) => item.checked).length;
|
||||||
|
const progress = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
|
||||||
|
|
||||||
if (checklists.length === 0) return null;
|
if (checkListItems.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|||||||
+10
-19
@@ -3,26 +3,21 @@ import { type FC, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Button from "../../../../../components/Button";
|
import Button from "../../../../../components/Button";
|
||||||
import Input from "../../../../../components/Input";
|
import Input from "../../../../../components/Input";
|
||||||
import Select from "../../../../../components/Select";
|
|
||||||
import type { TaskChecklist } from "./types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
checklists: TaskChecklist[];
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (title: string, copyFromId: string) => void;
|
onSubmit: (title: string) => void;
|
||||||
|
isSubmitting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailChecklistForm: FC<Props> = ({ checklists, onClose, onSubmit }) => {
|
const TaskDetailChecklistForm: FC<Props> = ({ onClose, onSubmit, isSubmitting = false }) => {
|
||||||
const { t } = useTranslation("global");
|
const { t } = useTranslation("global");
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [copyFromId, setCopyFromId] = useState("");
|
|
||||||
|
|
||||||
const copyFromItems = [{ value: "", label: t("taskmanager.task_detail.none") }, ...checklists.map((checklist) => ({ value: checklist.id, label: checklist.title }))];
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const trimmed = title.trim();
|
const trimmed = title.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
onSubmit(trimmed, copyFromId);
|
onSubmit(trimmed);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,20 +31,16 @@ const TaskDetailChecklistForm: FC<Props> = ({ checklists, onClose, onSubmit }) =
|
|||||||
|
|
||||||
<Input label={t("taskmanager.task_detail.label_title")} value={title} onChange={(e) => setTitle(e.target.value)} className="h-8 bg-white/63 border-white" labelClassName="text-xs" />
|
<Input label={t("taskmanager.task_detail.label_title")} value={title} onChange={(e) => setTitle(e.target.value)} className="h-8 bg-white/63 border-white" labelClassName="text-xs" />
|
||||||
|
|
||||||
<Select
|
|
||||||
label={t("taskmanager.task_detail.copy_from")}
|
|
||||||
labelClassName="text-xs"
|
|
||||||
value={copyFromId}
|
|
||||||
onChange={(e) => setCopyFromId(e.target.value)}
|
|
||||||
items={copyFromItems}
|
|
||||||
className="h-8 bg-white/63 border-white text-xs rounded-xl"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" onClick={onClose} className="h-8 bg-[#E8E4F0] text-[#292D32] rounded-xl text-xs w-[95px]">
|
<Button type="button" onClick={onClose} className="h-8 bg-[#E8E4F0] text-[#292D32] rounded-xl text-xs w-[95px]">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" onClick={handleSubmit} disabled={!title.trim()} className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]">
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!title.trim() || isSubmitting}
|
||||||
|
className="h-8 bg-[#292D32] text-white rounded-xl text-xs disabled:opacity-50 w-[95px]"
|
||||||
|
>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+4
-5
@@ -1,15 +1,14 @@
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import TaskDetailChecklistForm from "./TaskDetailChecklistForm";
|
import TaskDetailChecklistForm from "./TaskDetailChecklistForm";
|
||||||
import type { TaskChecklist } from "./types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
checklists: TaskChecklist[];
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (title: string, copyFromId: string) => void;
|
onSubmit: (title: string) => void;
|
||||||
|
isSubmitting?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskDetailChecklistPopover: FC<Props> = ({ checklists, onClose, onSubmit }) => {
|
const TaskDetailChecklistPopover: FC<Props> = ({ onClose, onSubmit, isSubmitting }) => {
|
||||||
return <TaskDetailChecklistForm checklists={checklists} onClose={onClose} onSubmit={onSubmit} />;
|
return <TaskDetailChecklistForm onClose={onClose} onSubmit={onSubmit} isSubmitting={isSubmitting} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TaskDetailChecklistPopover;
|
export default TaskDetailChecklistPopover;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import type { TaskChecklist } from "./types";
|
|
||||||
|
|
||||||
export const DEFAULT_CHECKLISTS: TaskChecklist[] = [
|
|
||||||
{ id: "1", title: "چک لیست پروژه" },
|
|
||||||
{ id: "2", title: "چک لیست انتشار" },
|
|
||||||
];
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export type TaskChecklist = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/TaskService";
|
import * as api from "../service/TaskService";
|
||||||
import { CreateTaskRemarkType, CreateTaskType, UpdateTaskType } from "../types/TaskTypes";
|
import { CreateCheckListItemType, CreateTaskRemarkType, CreateTaskType, UpdateTaskType } from "../types/TaskTypes";
|
||||||
|
|
||||||
export const useGetTaskDetail = (id: string, enabled = true) => {
|
export const useGetTaskDetail = (id: string, enabled = true) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -57,3 +57,15 @@ export const useCreateTaskRemark = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateCheckListItem = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreateCheckListItemType) => api.createCheckListItem(variables),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["task-detail", variables.taskId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios from "../../../../config/axios";
|
import axios from "../../../../config/axios";
|
||||||
import {
|
import {
|
||||||
|
CreateCheckListItemResponse,
|
||||||
|
CreateCheckListItemType,
|
||||||
CreateTaskRemarkResponse,
|
CreateTaskRemarkResponse,
|
||||||
CreateTaskRemarkType,
|
CreateTaskRemarkType,
|
||||||
CreateTaskType,
|
CreateTaskType,
|
||||||
@@ -31,3 +33,8 @@ export const createTaskRemark = async (params: CreateTaskRemarkType): Promise<Cr
|
|||||||
const { data } = await axios.post(`/task-manager/remarks`, params);
|
const { data } = await axios.post(`/task-manager/remarks`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createCheckListItem = async (params: CreateCheckListItemType): Promise<CreateCheckListItemResponse> => {
|
||||||
|
const { data } = await axios.post(`/task-manager/check-list-items`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ export type TaskChecklistItemType = {
|
|||||||
isDone: boolean;
|
isDone: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateCheckListItemType = {
|
||||||
|
title: string;
|
||||||
|
isDone: boolean;
|
||||||
|
taskId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CreateCheckListItemResponse extends IResponse<{ checkListItem: TaskChecklistItemType }> {
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type TaskChecklistType = {
|
export type TaskChecklistType = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -63,17 +73,22 @@ export type TaskAttachmentType = {
|
|||||||
|
|
||||||
export type TaskDetailType = {
|
export type TaskDetailType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string | null;
|
||||||
|
color?: string | null;
|
||||||
taskPhaseId: string;
|
taskPhaseId: string;
|
||||||
order: number;
|
order?: number;
|
||||||
projectId: string;
|
projectId?: string;
|
||||||
startDate?: string;
|
startDate?: string | null;
|
||||||
endDate?: string;
|
endDate?: string | null;
|
||||||
labels?: TaskLabelType[];
|
labels?: TaskLabelType[];
|
||||||
remarks?: TaskLabelType[];
|
remarks?: TaskLabelType[];
|
||||||
users?: TaskUserType[];
|
users?: TaskUserType[];
|
||||||
checklists?: TaskChecklistType[];
|
checkListItems?: TaskChecklistItemType[];
|
||||||
|
checkListItemCount?: number;
|
||||||
|
completedCheckListItemCount?: number;
|
||||||
attachments?: TaskAttachmentType[];
|
attachments?: TaskAttachmentType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user