use explicit scheduling levels

This commit is contained in:
Yisroel Baum 2026-08-11 23:02:18 +03:00
parent dfca13936a
commit 8e38b8f7e8
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
3 changed files with 33 additions and 34 deletions

View file

@ -51,7 +51,7 @@ export type ScheduleSummary = z.infer<typeof scheduleSummarySchema>
export type ScheduleDetail = z.infer<typeof scheduleDetailSchema>
export type CreateScheduleInput = {
setId: number
elementKind: string
levelId: number
startDate: string
targetDate: string
}

View file

@ -8,6 +8,7 @@ export const setElementNodeSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1),
kind: z.string().min(1),
levelId: z.number().int().positive(),
get children() {
return z.array(setElementNodeSchema)
},
@ -18,6 +19,14 @@ export const setLayoutResponseSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1),
}),
levels: z.array(
z.object({
id: z.number().int().positive(),
kind: z.string().min(1),
depth: z.number().int().nonnegative(),
elementCount: z.number().int().nonnegative(),
}),
),
elements: z.array(setElementNodeSchema),
})

View file

@ -6,9 +6,9 @@ import { z } from 'zod'
import AuthenticatedHeader from '@/components/AuthenticatedHeader.vue'
import { useSchedulesStore } from '@/stores/schedules'
import { useSetLayoutStore, type SetElementNode } from '@/stores/setLayout'
import { useSetLayoutStore } from '@/stores/setLayout'
type ScheduleField = 'elementKind' | 'startDate' | 'targetDate'
type ScheduleField = 'levelId' | 'startDate' | 'targetDate'
type ScheduleFieldErrors = Partial<Record<ScheduleField, string>>
const route = useRoute()
@ -19,34 +19,24 @@ const { layout, loading, error, notFound } = storeToRefs(setLayoutStore)
const { creating, createError } = storeToRefs(schedulesStore)
const currentSetId = ref<number | null>(null)
const form = reactive({
elementKind: '',
levelId: null as number | null,
startDate: '',
targetDate: '',
})
const fieldErrors = ref<ScheduleFieldErrors>({})
const levels = computed(() => {
const counts = new Map<string, number>()
function countNodes(nodes: SetElementNode[]): void {
for (const node of nodes) {
counts.set(node.kind, (counts.get(node.kind) ?? 0) + 1)
countNodes(node.children)
}
}
countNodes(layout.value?.elements ?? [])
return [...counts.entries()].map(([kind, count]) => ({
kind,
count,
label: humanizeKind(kind),
}))
})
const levels = computed(() =>
(layout.value?.levels ?? [])
.filter((level) => level.elementCount > 0)
.map((level) => ({
...level,
label: humanizeKind(level.kind),
})),
)
const scheduleFormSchema = z
.object({
elementKind: z.string().min(1, 'Choose a level to schedule.'),
levelId: z.number('Choose a level to schedule.').int().positive(),
startDate: z.string().min(1, 'Choose a start date.'),
targetDate: z.string().min(1, 'Choose a target date.'),
})
@ -82,7 +72,7 @@ async function submit(): Promise<void> {
for (const issue of result.error.issues) {
const field = issue.path[0]
if (
(field === 'elementKind' || field === 'startDate' || field === 'targetDate') &&
(field === 'levelId' || field === 'startDate' || field === 'targetDate') &&
errors[field] === undefined
) {
errors[field] = issue.message
@ -95,10 +85,10 @@ async function submit(): Promise<void> {
if (
currentSetId.value === null ||
!levels.value.some((level) => level.kind === result.data.elementKind)
!levels.value.some((level) => level.id === result.data.levelId)
) {
fieldErrors.value = {
elementKind: 'Choose an available level to schedule.',
levelId: 'Choose an available level to schedule.',
}
return
@ -166,18 +156,18 @@ function humanizeKind(kind: string): string {
<label for="schedule-level">Level</label>
<select
id="schedule-level"
v-model="form.elementKind"
:aria-invalid="fieldErrors.elementKind !== undefined"
:aria-describedby="fieldErrors.elementKind ? 'schedule-level-error' : undefined"
v-model="form.levelId"
:aria-invalid="fieldErrors.levelId !== undefined"
:aria-describedby="fieldErrors.levelId ? 'schedule-level-error' : undefined"
:disabled="creating"
>
<option value="">Choose a level</option>
<option v-for="level in levels" :key="level.kind" :value="level.kind">
{{ level.label }} ({{ level.count }})
<option :value="null">Choose a level</option>
<option v-for="level in levels" :key="level.id" :value="level.id">
{{ level.label }} ({{ level.elementCount }})
</option>
</select>
<p v-if="fieldErrors.elementKind" id="schedule-level-error" class="field-error">
{{ fieldErrors.elementKind }}
<p v-if="fieldErrors.levelId" id="schedule-level-error" class="field-error">
{{ fieldErrors.levelId }}
</p>
</div>