From db6b41ee42813eec0901531e80e3db1da79edb71 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Wed, 19 Aug 2026 22:57:43 +0300 Subject: [PATCH] add reschedule form --- .../src/components/ScheduleRescheduleForm.vue | 358 ++++++++++++++++++ frontend/website/src/stores/schedules.ts | 62 +++ .../website/src/views/ScheduleDetailView.vue | 138 ++++++- 3 files changed, 552 insertions(+), 6 deletions(-) create mode 100644 frontend/website/src/components/ScheduleRescheduleForm.vue diff --git a/frontend/website/src/components/ScheduleRescheduleForm.vue b/frontend/website/src/components/ScheduleRescheduleForm.vue new file mode 100644 index 0000000..e61e166 --- /dev/null +++ b/frontend/website/src/components/ScheduleRescheduleForm.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/frontend/website/src/stores/schedules.ts b/frontend/website/src/stores/schedules.ts index c6c3fde..75d3c63 100644 --- a/frontend/website/src/stores/schedules.ts +++ b/frontend/website/src/stores/schedules.ts @@ -86,10 +86,16 @@ export type CreateScheduleInput = { targetDate: string workloadPlacement: WorkloadPlacement } +export type RescheduleScheduleInput = { + startDate: string + targetDate: string + workloadPlacement: WorkloadPlacement +} 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 RESCHEDULE_ERROR = "We couldn't reschedule this schedule." const ASSIGNMENTS_ERROR = "We couldn't load today's assignments." const COMPLETION_ERROR = "We couldn't update this assignment." @@ -103,6 +109,8 @@ export const useSchedulesStore = defineStore('schedules', () => { const detailNotFound = ref(false) const creating = ref(false) const createError = ref(null) + const rescheduling = ref(false) + const rescheduleError = ref(null) const assignmentsForDate = ref([]) const assignmentsLoading = ref(false) const assignmentsError = ref(null) @@ -241,6 +249,56 @@ export const useSchedulesStore = defineStore('schedules', () => { } } + async function rescheduleSchedule( + scheduleId: number, + input: RescheduleScheduleInput, + ): Promise { + rescheduling.value = true + rescheduleError.value = null + + try { + const response = await fetch(`${API_BASE}/api/schedules/${scheduleId}`, { + method: 'PATCH', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(input), + }) + const responseBody: unknown = await response.json() + + if (response.status !== 200) { + const parsedError = errorResponseSchema.safeParse(responseBody) + rescheduleError.value = parsedError.success ? parsedError.data.error : RESCHEDULE_ERROR + + return null + } + + const rescheduled = scheduleResponseSchema.parse(responseBody).schedule + if (rescheduled.id !== scheduleId) { + throw new Error('schedule response did not match requested schedule') + } + activeSchedule.value = rescheduled + const summary = scheduleSummarySchema.parse(rescheduled) + schedules.value = schedules.value.map((schedule) => + schedule.id === scheduleId ? summary : schedule, + ) + + return rescheduled + } catch { + rescheduleError.value = RESCHEDULE_ERROR + + return null + } finally { + rescheduling.value = false + } + } + + function clearRescheduleError(): void { + rescheduleError.value = null + } + async function fetchAssignmentsForDate(date: string): Promise { const requestId = ++assignmentsRequestId assignmentsForDate.value = [] @@ -381,6 +439,8 @@ export const useSchedulesStore = defineStore('schedules', () => { detailNotFound, creating, createError, + rescheduling, + rescheduleError, assignmentsForDate, assignmentsLoading, assignmentsError, @@ -389,6 +449,8 @@ export const useSchedulesStore = defineStore('schedules', () => { fetchSchedules, fetchSchedule, createSchedule, + rescheduleSchedule, + clearRescheduleError, fetchAssignmentsForDate, setAssignmentCompleted, isAssignmentCompletionPending, diff --git a/frontend/website/src/views/ScheduleDetailView.vue b/frontend/website/src/views/ScheduleDetailView.vue index 5c72243..bf700ac 100644 --- a/frontend/website/src/views/ScheduleDetailView.vue +++ b/frontend/website/src/views/ScheduleDetailView.vue @@ -5,7 +5,12 @@ import { useRoute } from 'vue-router' import AuthenticatedHeader from '@/components/AuthenticatedHeader.vue' import ScheduleAssignmentTimeline from '@/components/ScheduleAssignmentTimeline.vue' -import { useSchedulesStore, type ScheduleDetail } from '@/stores/schedules' +import ScheduleRescheduleForm from '@/components/ScheduleRescheduleForm.vue' +import { + useSchedulesStore, + type RescheduleScheduleInput, + type ScheduleDetail, +} from '@/stores/schedules' type ScheduleDay = ScheduleDetail['days'][number] @@ -18,9 +23,13 @@ const { detailLoading, detailError, detailNotFound, + rescheduling, + rescheduleError, } = storeToRefs(schedulesStore) const currentScheduleId = ref(null) const completionAnnouncement = ref('') +const rescheduleAnnouncement = ref('') +const rescheduleFormOpen = ref(false) const todayDate = browserDate(new Date()) const remainingDays = computed(() => { @@ -28,9 +37,12 @@ const remainingDays = computed(() => { return [] } - return activeSchedule.value.days.flatMap((day) => { + const schedule = activeSchedule.value + + return schedule.days.flatMap((day) => { const assignments = day.assignments.filter((assignment) => assignment.completedAt === null) - const isRestDay = day.assignments.length === 0 + const isActiveDate = day.date >= schedule.startDate && day.date <= schedule.targetDate + const isRestDay = isActiveDate && day.assignments.length === 0 return assignments.length > 0 || isRestDay ? [{ date: day.date, assignments }] : [] }) @@ -50,6 +62,13 @@ const completedDays = computed(() => { const remainingAssignmentCount = computed(() => assignmentCount(remainingDays.value)) const completedAssignmentCount = computed(() => assignmentCount(completedDays.value)) +const activeDayCount = computed(() => { + if (activeSchedule.value === null) { + return 0 + } + + return inclusiveDayCount(activeSchedule.value.startDate, activeSchedule.value.targetDate) +}) watch( () => route.params.scheduleId, @@ -59,6 +78,9 @@ watch( : scheduleIdParameter const scheduleId = Number(rawScheduleId) currentScheduleId.value = scheduleId + rescheduleFormOpen.value = false + rescheduleAnnouncement.value = '' + schedulesStore.clearRescheduleError() if (activeSchedule.value?.id !== scheduleId) { await schedulesStore.fetchSchedule(scheduleId) @@ -92,6 +114,48 @@ function assignmentCount(days: ScheduleDay[]): number { return days.reduce((count, day) => count + day.assignments.length, 0) } +function inclusiveDayCount(startDate: string, targetDate: string): number { + const startTime = Date.parse(`${startDate}T00:00:00Z`) + const targetTime = Date.parse(`${targetDate}T00:00:00Z`) + if (Number.isNaN(startTime) || Number.isNaN(targetTime) || targetTime < startTime) { + return 0 + } + + const millisecondsPerDay = 24 * 60 * 60 * 1000 + + return Math.round((targetTime - startTime) / millisecondsPerDay) + 1 +} + +function openRescheduleForm(): void { + if (activeSchedule.value === null) { + return + } + + rescheduleAnnouncement.value = '' + schedulesStore.clearRescheduleError() + rescheduleFormOpen.value = true +} + +function cancelReschedule(): void { + schedulesStore.clearRescheduleError() + rescheduleFormOpen.value = false +} + +async function submitReschedule(input: RescheduleScheduleInput): Promise { + if (currentScheduleId.value === null) { + return + } + + const rescheduledAssignmentCount = remainingAssignmentCount.value + const schedule = await schedulesStore.rescheduleSchedule(currentScheduleId.value, input) + if (schedule !== null) { + rescheduleFormOpen.value = false + rescheduleAnnouncement.value = `${rescheduledAssignmentCount} remaining ${ + rescheduledAssignmentCount === 1 ? 'assignment' : 'assignments' + } rescheduled.` + } +} + async function setAssignmentCompleted(assignmentId: number, completed: boolean): Promise { completionAnnouncement.value = '' const updated = await schedulesStore.setAssignmentCompleted(assignmentId, completed) @@ -136,15 +200,47 @@ async function setAssignmentCompleted(assignmentId: number, completed: boolean): {{ activeSchedule.assignmentCount }} {{ activeSchedule.elementKind }} {{ activeSchedule.assignmentCount === 1 ? 'assignment' : 'assignments' }} across - {{ activeSchedule.days.length }} - {{ activeSchedule.days.length === 1 ? 'day' : 'days' }} + {{ activeDayCount }} + {{ activeDayCount === 1 ? 'day' : 'days' }}

{{ formatDate(activeSchedule.startDate) }} to {{ formatDate(activeSchedule.targetDate) }}

+ +

+ {{ rescheduleAnnouncement }} +

+ + +