From 32984642e5042aff54bb39327232dba98173f524 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sun, 11 May 2025 22:53:52 +0300 Subject: [PATCH] feat: new tag system --- src/components/ModalTags.tsx | 21 ++--- src/pages/profile_calendar.tsx | 70 ++++++++++------ src/pages/profile_tasks.dto.ts | 34 +++++--- src/pages/profile_tasks.tsx | 147 ++++++++++++++++++++------------- 4 files changed, 168 insertions(+), 104 deletions(-) diff --git a/src/components/ModalTags.tsx b/src/components/ModalTags.tsx index 1727ae5..827e5bf 100644 --- a/src/components/ModalTags.tsx +++ b/src/components/ModalTags.tsx @@ -1,3 +1,4 @@ +import { IAPITag } from "@/pages/profile_tasks.dto"; import { cn } from "@/utils/class-merge"; import { BookOpenIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; import { FunctionComponent } from "preact"; @@ -6,8 +7,8 @@ import Button from "./ui/Button"; import ModalWindow, { ModalWindowProps } from "./ui/Modal"; export interface ITags { - first: string; - second: string; + first: number; + second: number; overdue: boolean; } @@ -18,8 +19,8 @@ interface ModalTagsProps extends ModalWindowProps { value?: ITags; onChange?: Dispatch>; tagsList?: { - first: string[]; - second: string[]; + first: IAPITag[]; + second: IAPITag[]; }; } @@ -77,7 +78,7 @@ const ModalTags: FunctionComponent = ({ class="flex cursor-pointer flex-row gap-2" onClick={() => onChange?.((prev) => { - return { ...prev, first: tag }; + return { ...prev, first: tag.id }; }) } > @@ -85,13 +86,13 @@ const ModalTags: FunctionComponent = ({ class={cn( "pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none", { - "bg-black": value?.first === tag, + "bg-black": value?.first === tag.id, } )} > ✓ -

{tag}

+

{tag.name}

))} @@ -105,7 +106,7 @@ const ModalTags: FunctionComponent = ({ class="flex cursor-pointer flex-row gap-2" onClick={() => onChange?.((prev) => { - return { ...prev, second: tag }; + return { ...prev, second: tag.id }; }) } > @@ -113,13 +114,13 @@ const ModalTags: FunctionComponent = ({ class={cn( "pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none", { - "bg-black": value?.second === tag, + "bg-black": value?.second === tag.id, } )} > ✓ -

{tag}

+

{tag.name}

))} diff --git a/src/pages/profile_calendar.tsx b/src/pages/profile_calendar.tsx index bfcbdae..ec540f8 100644 --- a/src/pages/profile_calendar.tsx +++ b/src/pages/profile_calendar.tsx @@ -22,12 +22,14 @@ import { Nullable } from "primereact/ts-helpers"; import { SubmitHandler, useForm } from "react-hook-form"; import { IApiResponse, + IAPITag, ICreateTaskResponse, IDeleteTaskResponse, IEditTaskResponse, ITask, ITaskDetails, ITaskForm, + IViewTagsResponse, } from "./profile_tasks.dto"; const calendarStyles = { @@ -61,10 +63,10 @@ const ProfileCalendar: FunctionComponent = () => { const [isEditModal, setIsEditModal] = useState(false); const [editContent, setEditContent] = useState(null); const [calendarDate, setCalendarDate] = useState>(); - const [tags, setTags] = useState({ first: "", second: "", overdue: false }); + const [tags, setTags] = useState({ first: 0, second: 0, overdue: false }); const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [subjectChoices, setSubjectChoices] = useState>({}); - const [taskTypeChoices, setTaskTypeChoices] = useState>({}); + const [subjectChoices, setSubjectChoices] = useState([]); + const [taskTypeChoices, setTaskTypeChoices] = useState([]); const [isLoading, setIsLoading] = useState(true); const { @@ -75,29 +77,46 @@ const ProfileCalendar: FunctionComponent = () => { formState: { errors }, } = useForm({ defaultValues: { - tags: [], + subject: { + name: "", + id: 0, + }, + taskType: { + name: "", + id: 0, + }, }, }); useEffect(() => { fetchTasks(); + fetchTags(); }, []); + const fetchTags = async () => { + try { + const response = await apiClient("/api/tags/view_tags/"); + setSubjectChoices(response.subjects); + setTaskTypeChoices(response.taskTypes); + } catch (error) { + console.error("Failed to fetch tags:", error); + } + }; + const fetchTasks = async () => { try { setIsLoading(true); const response = await apiClient("/api/tasks/view_tasks/"); - setSubjectChoices(response.subject_choices); - setTaskTypeChoices(response.task_type_choices); - const convertedTasks: ITask[] = response.tasks.map((apiTask) => ({ id: apiTask.id.toString(), name: apiTask.title, checked: apiTask.isCompleted, date: new Date(apiTask.due_date), description: apiTask.description, - tags: [apiTask.subject, apiTask.task_type], + priority: apiTask.priority, + subject: apiTask.subject, + taskType: apiTask.taskType, new: false, })); @@ -111,8 +130,8 @@ const ProfileCalendar: FunctionComponent = () => { const example_tags = useMemo( () => ({ - first: Object.keys(subjectChoices), - second: Object.keys(taskTypeChoices), + first: subjectChoices, + second: taskTypeChoices, }), [subjectChoices, taskTypeChoices] ); @@ -122,14 +141,14 @@ const ProfileCalendar: FunctionComponent = () => { setError("date", { message: "Выберите дату" }); return; } - if ((!editContent?.tags[0] || !editContent.tags[1]) && (!tags.first || !tags.second)) { - setError("tags", { message: "Выберите теги" }); + if ((!editContent?.subject.id || !editContent.taskType.id) && (!tags.first || !tags.second)) { + setError("subject", { message: "Выберите теги" }); return; } try { - const selectedSubject = editContent?.tags[0] || tags.first; - const selectedTaskType = editContent?.tags[1] || tags.second; + const selectedSubject = editContent?.subject.id || tags.first; + const selectedTaskType = editContent?.taskType.id || tags.second; // Format date to DD-MM-YYYYTHH:MM const formattedDate = calendarDate @@ -176,7 +195,7 @@ const ProfileCalendar: FunctionComponent = () => { await fetchTasks(); setIsOpen(false); - setTags({ first: "", second: "", overdue: false }); + setTags({ first: 0, second: 0, overdue: false }); } catch (error) { console.error("Failed to save task:", error); } @@ -226,8 +245,9 @@ const ProfileCalendar: FunctionComponent = () => { checked: false, date: new Date(taskDetails.dateTime_due), description: taskDetails.description, - tags: [taskDetails.subject, taskDetails.taskType], - new: false, + subject: taskDetails.subject, + taskType: taskDetails.task_type, + priority: taskDetails.priority, }; setIsOpen(true); @@ -263,8 +283,8 @@ const ProfileCalendar: FunctionComponent = () => { useEffect(() => { if (!editContent) return; const newEditContent = editContent; - if (tags.first) newEditContent.tags = [tags.first, newEditContent.tags[1]]; - if (tags.second) newEditContent.tags = [newEditContent.tags[0], tags.second]; + if (tags.first) newEditContent.subject = subjectChoices.find((tag) => tag.id === tags.first)!; + if (tags.second) newEditContent.taskType = taskTypeChoices.find((tag) => tag.id === tags.second)!; setEditContent(newEditContent); }, [tags]); @@ -363,7 +383,7 @@ const ProfileCalendar: FunctionComponent = () => { tagsList={example_tags} value={tags} onClose={() => { - setTags({ first: "", second: "", overdue: false }); + setTags({ first: 0, second: 0, overdue: false }); }} onChange={setTags} /> @@ -383,7 +403,7 @@ const ProfileCalendar: FunctionComponent = () => { setIsEdit(false); setEditContent(null); setIsEditModal(false); - setTags({ first: "", second: "", overdue: false }); + setTags({ first: 0, second: 0, overdue: false }); setCalendarDate(null); }} > @@ -461,7 +481,7 @@ const ProfileCalendar: FunctionComponent = () => { {errors.name &&

{errors.name.message}

} {errors.description &&

{errors.description.message}

} {errors.date &&

{errors.date.message}

} - {errors.tags &&

{errors.tags.message}

} + {errors.subject &&

{errors.subject.message}

}
{ )} onClick={() => { if (!isEditModal) return; - setTags({ first: editContent.tags[0], second: editContent.tags[1], overdue: false }); + setTags({ first: editContent.subject.id, second: editContent.taskType.id, overdue: false }); setOpenModalTags(true); }} >

- {editContent.tags[0]} + {editContent.subject.name}

- {editContent.tags[1]} + {editContent.taskType.name}

diff --git a/src/pages/profile_tasks.dto.ts b/src/pages/profile_tasks.dto.ts index d57e203..4d29b62 100644 --- a/src/pages/profile_tasks.dto.ts +++ b/src/pages/profile_tasks.dto.ts @@ -1,11 +1,20 @@ +export interface IAPITag { + name: string; + id: number; +} export interface ITask { id: string; name: string; checked: boolean; + priority: number; date: Date; description: string; - tags: string[]; - new?: boolean; + subject: IAPITag; + taskType: IAPITag; +} + +export interface ITaskView extends Omit { + task_type: IAPITag; } export interface ITaskForm extends Omit { @@ -16,17 +25,16 @@ export interface IApiTask { id: number; title: string; description: string; + priority: number; isCompleted: boolean; due_date: string; - subject: string; - task_type: string; + subject: IAPITag; + taskType: IAPITag; } export interface IApiResponse { profile: string; tasks: IApiTask[]; - subject_choices: Record; - task_type_choices: Record; } export interface ICreateTaskResponse { @@ -53,12 +61,10 @@ export interface ITaskDetails { profile: string; title: string; description: string; - subject: string; - taskType: string; + subject: IAPITag; + task_type: IAPITag; + priority: number; dateTime_due: string; - remind_before_days: number; - repeat_reminder: number; - reminder_time: string; } export interface IDeleteTaskResponse { @@ -85,3 +91,9 @@ export interface IEditTaskResponse { }; }; } + +export interface IViewTagsResponse { + profile: string; + subjects: IAPITag[]; + taskTypes: IAPITag[]; +} diff --git a/src/pages/profile_tasks.tsx b/src/pages/profile_tasks.tsx index 98041bf..3f0bf76 100644 --- a/src/pages/profile_tasks.tsx +++ b/src/pages/profile_tasks.tsx @@ -24,6 +24,7 @@ import { XMarkIcon, } from "@heroicons/react/24/outline"; import { FunctionComponent } from "preact"; +import "preact/debug"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; import { Checkbox, CheckboxPassThroughMethodOptions } from "primereact/checkbox"; import { Nullable } from "primereact/ts-helpers"; @@ -31,12 +32,14 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { v4 as uuid } from "uuid"; import { IApiResponse, + IAPITag, ICreateTaskResponse, IDeleteTaskResponse, IEditTaskResponse, ITask, ITaskDetails, ITaskForm, + IViewTagsResponse, } from "./profile_tasks.dto"; import classes from "./profile_tasks.module.scss"; @@ -51,10 +54,10 @@ const ProfileTasks: FunctionComponent = () => { const [isCreating, setIsCreating] = useState(false); // Включено создание задачи const [editContent, setEditContent] = useState(null); // Содержимое редактируемой задачи const [calendarDate, setCalendarDate] = useState>(); // Выбранная в календаре дата - const [tags, setTags] = useState({ first: "", second: "", overdue: false }); + const [tags, setTags] = useState({ first: 0, second: 0, overdue: false }); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [searchQuery, setSearchQuery] = useState(""); // Текст поиска - const [filterTags, setFilterTags] = useState({ first: "", second: "", overdue: false }); + const [filterTags, setFilterTags] = useState({ first: 0, second: 0, overdue: false }); const [openFirstList, setOpenFirstList] = useState(false); const [openSecondList, setOpenSecondList] = useState(false); const getDate = useMemo(() => { @@ -63,30 +66,38 @@ const ProfileTasks: FunctionComponent = () => { return formatter.format(date); }, []); const [tasks, setTasks] = useState([]); - const [subjectChoices, setSubjectChoices] = useState>({}); - const [taskTypeChoices, setTaskTypeChoices] = useState>({}); + const [subjectChoices, setSubjectChoices] = useState([]); + const [taskTypeChoices, setTaskTypeChoices] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetchTasks(); + fetchTags(); }, []); + const fetchTags = async () => { + try { + const response = await apiClient("/api/tags/view_tags/"); + setSubjectChoices(response.subjects); + setTaskTypeChoices(response.taskTypes); + } catch (error) { + console.error("Failed to fetch tags:", error); + } + }; + const fetchTasks = async () => { try { setIsLoading(true); const response = await apiClient("/api/tasks/view_tasks/"); - - setSubjectChoices(response.subject_choices); - setTaskTypeChoices(response.task_type_choices); - const convertedTasks: ITask[] = response.tasks.map((apiTask) => ({ id: apiTask.id.toString(), name: apiTask.title, + priority: apiTask.priority, checked: apiTask.isCompleted, date: new Date(apiTask.due_date), description: apiTask.description, - tags: [apiTask.subject, apiTask.task_type], - new: false, + subject: apiTask.subject, + taskType: apiTask.taskType, })); setTasks(convertedTasks); @@ -105,14 +116,21 @@ const ProfileTasks: FunctionComponent = () => { formState: { errors }, } = useForm({ defaultValues: { - tags: [], + subject: { + name: "", + id: 0, + }, + taskType: { + name: "", + id: 0, + }, }, }); const example_tags = useMemo( () => ({ - first: Object.keys(subjectChoices), - second: Object.keys(taskTypeChoices), + first: subjectChoices, + second: taskTypeChoices, }), [subjectChoices, taskTypeChoices] ); @@ -122,14 +140,15 @@ const ProfileTasks: FunctionComponent = () => { setError("date", { message: "Выберите дату" }); return; } - if ((!editContent?.tags[0] || !editContent.tags[1]) && (!tags.first || !tags.second)) { - setError("tags", { message: "Выберите теги" }); + if ((!editContent?.subject.id || !editContent.taskType.id) && (!tags.first || !tags.second)) { + setError("subject", { message: "Выберите теги" }); + setError("taskType", { message: "Выберите теги" }); return; } try { - const selectedSubject = editContent?.tags[0] || tags.first; - const selectedTaskType = editContent?.tags[1] || tags.second; + const selectedSubject = editContent?.subject.id || tags.first; + const selectedTaskType = editContent?.taskType.id || tags.second; // Format date to DD-MM-YYYYTHH:MM const formattedDate = calendarDate @@ -177,7 +196,7 @@ const ProfileTasks: FunctionComponent = () => { await fetchTasks(); if (isCreating) setIsOpen(false); - setTags({ first: "", second: "", overdue: false }); + setTags({ first: 0, second: 0, overdue: false }); } catch (error) { console.error("Failed to save task:", error); } @@ -192,15 +211,23 @@ const ProfileTasks: FunctionComponent = () => { name: "", description: "", date: "", - tags: [], + subject: { + name: "", + id: 0, + }, + taskType: { + name: "", + id: 0, + }, + priority: 4, checked: false, }); }, [isCreating]); useEffect(() => { if (!editContent) return; const newEditContent = editContent; - if (tags.first) newEditContent.tags = [tags.first, newEditContent.tags[1]]; - if (tags.second) newEditContent.tags = [newEditContent.tags[0], tags.second]; + if (tags.first) newEditContent.subject = subjectChoices.find((choice) => choice.id === tags.first)!; + if (tags.second) newEditContent.taskType = taskTypeChoices.find((choice) => choice.id === tags.second)!; setEditContent(newEditContent); }, [tags]); @@ -304,8 +331,8 @@ const ProfileTasks: FunctionComponent = () => { if (filterTags.first || filterTags.second) { filtered = filtered.filter( (task) => - (!filterTags.first || task.tags[0] === filterTags.first) && - (!filterTags.second || task.tags[1] === filterTags.second) + (!filterTags.first || task.subject.id === filterTags.first) && + (!filterTags.second || task.taskType.id === filterTags.second) ); } @@ -326,12 +353,14 @@ const ProfileTasks: FunctionComponent = () => { const task: ITask = { id: taskId, name: taskDetails.title, + priority: taskDetails.priority, checked: false, date: new Date(taskDetails.dateTime_due), description: taskDetails.description, - tags: [taskDetails.subject, taskDetails.taskType], - new: false, + subject: taskDetails.subject, + taskType: taskDetails.task_type, }; + console.log(task); setIsOpen(true); setIsEdit(true); @@ -357,7 +386,7 @@ const ProfileTasks: FunctionComponent = () => { tagsList={example_tags} value={tags} onClose={() => { - if (!isCreating) setTags({ first: "", second: "", overdue: false }); + if (!isCreating) setTags({ first: 0, second: 0, overdue: false }); }} onChange={setTags} /> @@ -381,7 +410,7 @@ const ProfileTasks: FunctionComponent = () => { setEditContent(null); setIsCreating(false); setIsEditModal(false); - setTags({ first: "", second: "", overdue: false }); + setTags({ first: 0, second: 0, overdue: false }); setCalendarDate(null); }} > @@ -459,7 +488,7 @@ const ProfileTasks: FunctionComponent = () => { {errors.name &&

{errors.name.message}

} {errors.description &&

{errors.description.message}

} {errors.date &&

{errors.date.message}

} - {errors.tags &&

{errors.tags.message}

} + {errors.subject &&

{errors.subject.message}

}
{ })} onClick={() => { if (!isEditModal) return; - setTags({ first: editContent.tags[0], second: editContent.tags[1], overdue: false }); + setTags({ first: editContent.subject.id, second: editContent.taskType.id, overdue: false }); setOpenModalTags(true); }} >

- {editContent.tags[0]} + {editContent.subject.name}

- {editContent.tags[1]} + {editContent.taskType.name}

@@ -547,7 +576,7 @@ const ProfileTasks: FunctionComponent = () => { {errors.name &&

{errors.name.message}

} {errors.date &&

{errors.date.message}

} - {errors.tags &&

{errors.tags.message}

} + {errors.subject?.message &&

{errors.subject.message}

}
{openFirstList && (
- {example_tags.first.map((tag) => ( -
{ - setFilterTags({ ...filterTags, first: tag }); - setOpenFirstList(false); - }} - > + {example_tags.first.map((tag) => { + return (
{ + setFilterTags({ ...filterTags, first: tag.id }); + setOpenFirstList(false); + }} > - {filterTags.first === tag &&
} +
+ {filterTags.first === tag.id &&
} +
+ {tag.name}
- {tag} -
- ))} + ); + })}
)}
@@ -859,28 +890,28 @@ const ProfileTasks: FunctionComponent = () => { }} > - {filterTags.second || "Задача"} + {taskTypeChoices.find((s) => s.id === filterTags.second)?.name || "Задача"}
{openSecondList && (
{example_tags.second.map((tag) => (
{ - setFilterTags({ ...filterTags, second: tag }); + setFilterTags({ ...filterTags, second: tag.id }); setOpenSecondList(false); }} >
- {filterTags.second === tag &&
} + {filterTags.second === tag.id &&
}
- {tag} + {tag.name}
))}
@@ -891,7 +922,7 @@ const ProfileTasks: FunctionComponent = () => {