feat: show tasks and creating them

This commit is contained in:
2025-05-07 11:14:17 +03:00
parent 852ac9ad0d
commit ce694f0be8
2 changed files with 198 additions and 55 deletions

View File

@@ -11,3 +11,46 @@ export interface ITask {
export interface ITaskForm extends Omit<ITask, "date"> { export interface ITaskForm extends Omit<ITask, "date"> {
date: string; date: string;
} }
export interface IApiTask {
id: number;
title: string;
description: string;
isCompleted: boolean;
due_date: string;
subject: string;
task_type: string;
}
export interface IApiDay {
date: string;
name: string;
tasks: IApiTask[];
}
export interface IApiResponse {
profile: string;
days: IApiDay[];
subject_choices: Record<string, string>;
task_type_choices: Record<string, string>;
}
export interface ICreateTaskResponse {
success: boolean;
message: string;
profile: string;
task: {
id: number;
title: string;
description: string;
subject: string;
taskType: string;
dateTime_due: string;
isCompleted: boolean;
reminder?: {
remind_before_days: number;
repeat_interval: number;
reminder_time: string;
};
};
}

View File

@@ -28,13 +28,9 @@ import { Checkbox, CheckboxPassThroughMethodOptions } from "primereact/checkbox"
import { Nullable } from "primereact/ts-helpers"; import { Nullable } from "primereact/ts-helpers";
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { ITask, ITaskForm } from "./profile_tasks.dto"; import { ITask, ITaskForm, IApiResponse, ICreateTaskResponse } from "./profile_tasks.dto";
import classes from "./profile_tasks.module.scss"; import classes from "./profile_tasks.module.scss";
import apiClient from "@/services/api";
const example_tags: { first: string[]; second: string[] } = {
first: ["Программирование", "Информатика", "Физика", "Математика"],
second: ["Лабораторная работа", "Практическая работа", "Домашнее задание", "Экзамен"],
};
const ProfileTasks: FunctionComponent = () => { const ProfileTasks: FunctionComponent = () => {
const [openModal, setIsOpen] = useState(false); // Открыта модалка const [openModal, setIsOpen] = useState(false); // Открыта модалка
@@ -58,17 +54,44 @@ const ProfileTasks: FunctionComponent = () => {
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" }); const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
return formatter.format(date); return formatter.format(date);
}, []); }, []);
const init_tasks: ITask[] = localStorage.getItem("tasks") ? JSON.parse(localStorage.getItem("tasks") as string) : []; const [tasks, setTasks] = useState<ITask[]>([]);
let clear = false; const [subjectChoices, setSubjectChoices] = useState<Record<string, string>>({});
init_tasks.forEach((task) => { const [taskTypeChoices, setTaskTypeChoices] = useState<Record<string, string>>({});
clear = clear || (task.new == undefined ? true : false); const [isLoading, setIsLoading] = useState(true);
if (!clear) task.new = true;
task.date = new Date(task.date);
});
const [tasks, setTasks] = useState<ITask[]>(clear ? [] : init_tasks);
useEffect(() => { useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks)); fetchTasks();
}, [tasks]); }, []);
const fetchTasks = async () => {
try {
setIsLoading(true);
const response = await apiClient<IApiResponse>("/api/tasks/view_tasks/");
// Update choices
setSubjectChoices(response.subject_choices);
setTaskTypeChoices(response.task_type_choices);
// Convert API tasks to our format
const convertedTasks: ITask[] = response.days.flatMap((day) =>
day.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],
new: false,
}))
);
setTasks(convertedTasks);
} catch (error) {
console.error("Failed to fetch tasks:", error);
} finally {
setIsLoading(false);
}
};
const { const {
handleSubmit, handleSubmit,
@@ -81,27 +104,79 @@ const ProfileTasks: FunctionComponent = () => {
tags: [], tags: [],
}, },
}); });
const saveTask: SubmitHandler<ITaskForm> = (data) => {
// Update example_tags to use the choices from the API
const example_tags = useMemo(
() => ({
first: Object.keys(subjectChoices),
second: Object.keys(taskTypeChoices),
}),
[subjectChoices, taskTypeChoices]
);
const saveTask: SubmitHandler<ITaskForm> = async (data) => {
if (!calendarDate) { if (!calendarDate) {
setError("date", { message: "Выберите дату" }); setError("date", { message: "Выберите дату" });
return; return;
} }
console.log(tags);
if ((!editContent?.tags[0] || !editContent.tags[1]) && (!tags.first || !tags.second)) { if ((!editContent?.tags[0] || !editContent.tags[1]) && (!tags.first || !tags.second)) {
setError("tags", { message: "Выберите теги" }); setError("tags", { message: "Выберите теги" });
return; return;
} }
const eTask: ITask = {
...data, try {
date: calendarDate, const selectedSubject = editContent?.tags[0] || tags.first;
tags: editContent?.tags.length ? editContent.tags : [tags.first, tags.second], const selectedTaskType = editContent?.tags[1] || tags.second;
new: true,
}; // Format date to DD-MM-YYYYTHH:MM
if (isCreating) setTasks([...tasks, eTask]); const formattedDate = calendarDate
else setTasks(tasks.map((task) => (task.id === eTask.id ? eTask : task))); .toLocaleString("en-GB", {
if (isCreating) setIsOpen(false); day: "2-digit",
setTags({ first: "", second: "", overdue: false }); month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
.replace(",", "T")
.replace(/\//g, "-")
.replace("T ", "T");
const taskData = {
title: data.name,
description: data.description || "",
subject: selectedSubject,
taskType: selectedTaskType,
dateTime_due: formattedDate,
telegram_notifications: false,
};
if (isCreating) {
const response = await apiClient<ICreateTaskResponse>("/api/tasks/create_task/", {
method: "POST",
body: JSON.stringify(taskData),
});
if (!response.success) {
throw new Error(response.message);
}
} else {
await apiClient(`/api/tasks/update_task/${editContent?.id}/`, {
method: "PUT",
body: JSON.stringify(taskData),
});
}
// Refresh tasks after saving
await fetchTasks();
if (isCreating) setIsOpen(false);
setTags({ first: "", second: "", overdue: false });
} catch (error) {
console.error("Failed to save task:", error);
}
}; };
useEffect(() => { useEffect(() => {
if (editContent) reset({ ...editContent, date: editContent.date.toISOString().slice(0, 16) }); if (editContent) reset({ ...editContent, date: editContent.date.toISOString().slice(0, 16) });
else reset(); else reset();
@@ -174,11 +249,37 @@ const ProfileTasks: FunctionComponent = () => {
}).format(date); }).format(date);
}; };
const handleDeleteTask = () => { const handleDeleteTask = async () => {
if (!editContent) return; if (!editContent) return;
setTasks(tasks.filter((task) => task.id !== editContent.id));
setIsOpen(false); try {
setShowDeleteDialog(false); await apiClient(`/api/delete_task/${editContent.id}/`, {
method: "DELETE",
});
// Refresh tasks after deletion
await fetchTasks();
setIsOpen(false);
setShowDeleteDialog(false);
} catch (error) {
console.error("Failed to delete task:", error);
}
};
// Update the task marking functionality
const handleMarkTask = async (taskId: string, isCompleted: boolean) => {
try {
await apiClient(`/api/update_task/${taskId}/`, {
method: "PATCH",
body: JSON.stringify({ isCompleted }),
});
// Refresh tasks after marking
await fetchTasks();
} catch (error) {
console.error("Failed to mark task:", error);
}
}; };
const filteredTasks = useMemo(() => { const filteredTasks = useMemo(() => {
@@ -211,19 +312,26 @@ const ProfileTasks: FunctionComponent = () => {
useEffect(() => { useEffect(() => {
if (searchInputRef.current && openSearchModal) searchInputRef.current.focus(); if (searchInputRef.current && openSearchModal) searchInputRef.current.focus();
}, [searchInputRef, openSearchModal]); }, [searchInputRef, openSearchModal]);
return ( return (
<div class={classes.container}> <div class={classes.container}>
<ModalTags {isLoading ? (
zIndex={70} <div class="flex w-full flex-1 items-center justify-center">
isOpen={openModalTags} <div class="text-2xl">Загрузка...</div>
setIsOpen={setOpenModalTags} </div>
tagsList={example_tags} ) : (
value={tags} <ModalTags
onClose={() => { zIndex={70}
if (!isCreating) setTags({ first: "", second: "", overdue: false }); isOpen={openModalTags}
}} setIsOpen={setOpenModalTags}
onChange={setTags} tagsList={example_tags}
/> value={tags}
onClose={() => {
if (!isCreating) setTags({ first: "", second: "", overdue: false });
}}
onChange={setTags}
/>
)}
<ModalCalendar <ModalCalendar
zIndex={80} zIndex={80}
isOpen={openModalCalendar} isOpen={openModalCalendar}
@@ -480,9 +588,7 @@ const ProfileTasks: FunctionComponent = () => {
setEditContent(task); setEditContent(task);
setCalendarDate(task.date); setCalendarDate(task.date);
}} }}
onMarkClick={() => { onMarkClick={() => handleMarkTask(task.id, !task.checked)}
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/> />
))} ))}
</div> </div>
@@ -503,9 +609,7 @@ const ProfileTasks: FunctionComponent = () => {
setEditContent(task); setEditContent(task);
setCalendarDate(task.date); setCalendarDate(task.date);
}} }}
onMarkClick={() => { onMarkClick={() => handleMarkTask(task.id, !task.checked)}
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/> />
))} ))}
</div> </div>
@@ -524,9 +628,7 @@ const ProfileTasks: FunctionComponent = () => {
setEditContent(task); setEditContent(task);
setCalendarDate(task.date); setCalendarDate(task.date);
}} }}
onMarkClick={() => { onMarkClick={() => handleMarkTask(task.id, !task.checked)}
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/> />
))} ))}
</div> </div>
@@ -578,9 +680,7 @@ const ProfileTasks: FunctionComponent = () => {
setEditContent(task); setEditContent(task);
setCalendarDate(task.date); setCalendarDate(task.date);
}} }}
onMarkClick={() => { onMarkClick={() => handleMarkTask(task.id, !task.checked)}
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
}}
/> />
)) ))
) : ( ) : (