check list

This commit is contained in:
hamid zarghami
2026-07-12 11:56:46 +03:30
parent 61282e1d1b
commit 50d4ff9281
10 changed files with 97 additions and 71 deletions
@@ -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">
@@ -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>
@@ -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;
};
+21 -6
View File
@@ -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[];
}; };