diff --git a/frontend/website/src/stores/schedules.ts b/frontend/website/src/stores/schedules.ts index 090db7c..2471d66 100644 --- a/frontend/website/src/stores/schedules.ts +++ b/frontend/website/src/stores/schedules.ts @@ -17,7 +17,7 @@ export const scheduleSummarySchema = z.object({ assignmentCount: z.number().int().nonnegative(), }) -const scheduleAssignmentSchema = z.object({ +const assignmentIdentitySchema = z.object({ id: z.number().int().positive(), element: z.object({ name: z.string().min(1), @@ -26,7 +26,11 @@ const scheduleAssignmentSchema = z.object({ }), }) -export const assignmentForDateSchema = scheduleAssignmentSchema.extend({ +const scheduleAssignmentSchema = assignmentIdentitySchema.extend({ + completedAt: z.string().datetime({ offset: true }).nullable(), +}) + +export const assignmentForDateSchema = assignmentIdentitySchema.extend({ schedule: z.object({ id: z.number().int().positive(), set: z.object({ @@ -57,6 +61,13 @@ const assignmentsForDateResponseSchema = z.object({ assignments: z.array(assignmentForDateSchema), }) +const assignmentCompletionResponseSchema = z.object({ + assignment: z.object({ + id: z.number().int().positive(), + completedAt: z.string().datetime({ offset: true }).nullable(), + }), +}) + const errorResponseSchema = z.object({ error: z.string().min(1), }) @@ -75,6 +86,7 @@ const LIST_ERROR = "We couldn't load your schedules." const DETAIL_ERROR = "We couldn't load this schedule." const CREATE_ERROR = "We couldn't create this schedule." const ASSIGNMENTS_ERROR = "We couldn't load today's assignments." +const COMPLETION_ERROR = "We couldn't update this assignment." export const useSchedulesStore = defineStore('schedules', () => { const schedules = ref([]) @@ -89,6 +101,8 @@ export const useSchedulesStore = defineStore('schedules', () => { const assignmentsForDate = ref([]) const assignmentsLoading = ref(false) const assignmentsError = ref(null) + const assignmentCompletionPendingIds = ref([]) + const assignmentCompletionErrors = ref>({}) let activeDetailRequestId = 0 let assignmentsRequestId = 0 @@ -274,6 +288,84 @@ export const useSchedulesStore = defineStore('schedules', () => { } } + async function setAssignmentCompleted( + assignmentId: number, + completed: boolean, + ): Promise { + if (assignmentCompletionPendingIds.value.includes(assignmentId)) { + return false + } + + assignmentCompletionPendingIds.value = [...assignmentCompletionPendingIds.value, assignmentId] + const remainingErrors = { ...assignmentCompletionErrors.value } + delete remainingErrors[assignmentId] + assignmentCompletionErrors.value = remainingErrors + + try { + const response = await fetch(`${API_BASE}/api/assignments/${assignmentId}`, { + method: 'PATCH', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ completed }), + }) + + if (response.status !== 200) { + throw new Error('assignment completion request failed') + } + + const responseBody: unknown = await response.json() + const assignment = assignmentCompletionResponseSchema.parse(responseBody).assignment + const completionDoesNotMatchRequest = completed + ? assignment.completedAt === null + : assignment.completedAt !== null + if (assignment.id !== assignmentId || completionDoesNotMatchRequest) { + throw new Error('assignment completion response did not match request') + } + + if (activeSchedule.value !== null) { + for (const day of activeSchedule.value.days) { + const activeAssignment = day.assignments.find( + (candidate) => candidate.id === assignmentId, + ) + if (activeAssignment !== undefined) { + activeAssignment.completedAt = assignment.completedAt + break + } + } + } + + if (assignment.completedAt !== null) { + assignmentsForDate.value = assignmentsForDate.value.filter( + (candidate) => candidate.id !== assignmentId, + ) + } + + return true + } catch { + assignmentCompletionErrors.value = { + ...assignmentCompletionErrors.value, + [assignmentId]: COMPLETION_ERROR, + } + + return false + } finally { + assignmentCompletionPendingIds.value = assignmentCompletionPendingIds.value.filter( + (pendingId) => pendingId !== assignmentId, + ) + } + } + + function isAssignmentCompletionPending(assignmentId: number): boolean { + return assignmentCompletionPendingIds.value.includes(assignmentId) + } + + function assignmentCompletionError(assignmentId: number): string | null { + return assignmentCompletionErrors.value[assignmentId] ?? null + } + return { schedules, listLoading, @@ -287,9 +379,14 @@ export const useSchedulesStore = defineStore('schedules', () => { assignmentsForDate, assignmentsLoading, assignmentsError, + assignmentCompletionPendingIds, + assignmentCompletionErrors, fetchSchedules, fetchSchedule, createSchedule, fetchAssignmentsForDate, + setAssignmentCompleted, + isAssignmentCompletionPending, + assignmentCompletionError, } }) diff --git a/frontend/website/src/views/DashboardView.vue b/frontend/website/src/views/DashboardView.vue index c99ccf4..822da86 100644 --- a/frontend/website/src/views/DashboardView.vue +++ b/frontend/website/src/views/DashboardView.vue @@ -35,6 +35,10 @@ function formatDate(value: string): string { timeZone: 'UTC', }).format(new Date(`${value}T00:00:00Z`)) } + +async function completeAssignment(assignmentId: number): Promise { + await schedulesStore.setAssignmentCompleted(assignmentId, true) +}