studium/frontend/src/services/api/tasks.ts
Antoine Pelletier 5e160ae11c wip
2026-09-29 09:47:27 +02:00

178 lines
5.7 KiB
TypeScript

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<number>) {
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> = {}): 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<number>) {
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<number>) {
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<FocusStart> {
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<FocusStop> {
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 })
}
}