import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' import { HttpStatus } from 'http-status-ts' import { toValue, type MaybeRefOrGetter } from 'vue' import type { CourseTasks, CreateTaskBody, FocusStart, FocusStop, Task, TaskBody, TimetaggerRecord, } from '@/utils/types' import { getClient } from './client' // Every mutation answers with all the tasks of the course (nested, sorted) and the // deadlines, which replace the cache: no refetch needed. function tasksKey(courseId: MaybeRefOrGetter) { return ['course', courseId, 'tasks'] } const TODO_KEY = ['todo-tasks'] function unexpected(status: number): never { throw new Error(`Unexpected status code received: ${status}`) } /** Body replacing `task` by itself, with `changes` applied */ export function toTaskBody(task: Task, changes: Partial = {}): TaskBody { return { name: task.name, description: task.description, links: task.links, deadline: task.deadline, done: task.done, tag_ids: task.tag_ids, ...changes, } } export function useCourseTasks(courseId: MaybeRefOrGetter) { return useQuery({ queryKey: tasksKey(courseId), queryFn: async () => { const { data, response } = await getClient().GET('/api/courses/{id}/tasks', { params: { path: { id: toValue(courseId) } }, }) return response.status === HttpStatus.OK && data ? data : unexpected(response.status) }, }) } /** * Tasks and subtasks of every course with a deadline in `[from, to[` (ISO dates), for * the calendar */ export function useCalendarTasks(period: MaybeRefOrGetter<{ from: string; to: string }>) { return useQuery({ queryKey: ['calendar-tasks', period], queryFn: async () => { const { data, response } = await getClient().GET('/api/tasks', { params: { query: toValue(period) }, }) return response.status === HttpStatus.OK && data ? data : unexpected(response.status) }, }) } /** Tasks not done of every course and association, the closest deadline first */ export function useTodoTasks() { return useQuery({ queryKey: TODO_KEY, queryFn: async () => { const { data, response } = await getClient().GET('/api/tasks/todo') return response.status === HttpStatus.OK && data ? data : unexpected(response.status) }, }) } /** * Mutations of the tasks of any course: the course is given with each call. See * `useTaskMutations` for the tasks of a single course. */ export function useAnyTaskMutations() { const queryClient = useQueryClient() const onSuccess = (tasks: CourseTasks, { courseId }: { courseId: number }) => { queryClient.setQueryData(tasksKey(courseId), tasks) // Refetched when they are shown again queryClient.invalidateQueries({ queryKey: ['calendar-tasks'] }) queryClient.invalidateQueries({ queryKey: TODO_KEY }) } const create = useMutation({ mutationFn: async ({ courseId, body }: { courseId: number; body: CreateTaskBody }) => { const { data, response } = await getClient().POST('/api/courses/{id}/tasks', { params: { path: { id: courseId } }, body, }) return response.status === HttpStatus.OK && data ? data : unexpected(response.status) }, onSuccess, }) const update = useMutation({ mutationFn: async ({ id, body }: { courseId: number; id: number; body: TaskBody }) => { const { data, response } = await getClient().PUT('/api/tasks/{id}', { params: { path: { id } }, body, }) return response.status === HttpStatus.OK && data ? data : unexpected(response.status) }, onSuccess, }) const remove = useMutation({ mutationFn: async ({ id }: { courseId: number; id: number }) => { const { data, response } = await getClient().DELETE('/api/tasks/{id}', { params: { path: { id } }, }) return response.status === HttpStatus.OK && data ? data : unexpected(response.status) }, onSuccess, }) return { create, update, remove } } /** Mutations of the tasks of a single course */ export function useTaskMutations(courseId: MaybeRefOrGetter) { const { create, update, remove } = useAnyTaskMutations() return { create: { ...create, mutateAsync: (body: CreateTaskBody) => create.mutateAsync({ courseId: toValue(courseId), body }), }, update: { ...update, mutateAsync: ({ id, body }: { id: number; body: TaskBody }) => update.mutateAsync({ courseId: toValue(courseId), id, body }), }, remove: { ...remove, mutateAsync: (id: number) => remove.mutateAsync({ courseId: toValue(courseId), id }), }, } } /** Starts a focus session on a task, and its TimeTagger record */ export async function startFocus(taskId: number): Promise { const { data, response } = await getClient().POST('/api/tasks/{id}/focus', { params: { path: { id: taskId } }, }) return response.status === HttpStatus.OK && data ? data : unexpected(response.status) } /** Ends (or pauses) a focus session: its time is added to the task */ export async function stopFocus( taskId: number, seconds: number, timetagger: TimetaggerRecord | null, ): Promise { const { data, response } = await getClient().POST('/api/tasks/{id}/focus/stop', { params: { path: { id: taskId } }, body: { seconds, timetagger }, }) return response.status === HttpStatus.OK && data ? data : unexpected(response.status) } /** The time spent on the tasks changed: shown again from the server */ export function useRefreshTasks() { const queryClient = useQueryClient() return (courseId: number) => { queryClient.invalidateQueries({ queryKey: tasksKey(courseId) }) queryClient.invalidateQueries({ queryKey: TODO_KEY }) } }