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 ScheduleDetail = z.infer<typeof scheduleDetailSchema>
export type CreateScheduleInput = { export type CreateScheduleInput = {
setId: number setId: number
elementKind: string levelId: number
startDate: string startDate: string
targetDate: string targetDate: string
} }

View file

@ -8,6 +8,7 @@ export const setElementNodeSchema = z.object({
id: z.number().int().positive(), id: z.number().int().positive(),
name: z.string().min(1), name: z.string().min(1),
kind: z.string().min(1), kind: z.string().min(1),
levelId: z.number().int().positive(),
get children() { get children() {
return z.array(setElementNodeSchema) return z.array(setElementNodeSchema)
}, },
@ -18,6 +19,14 @@ export const setLayoutResponseSchema = z.object({
id: z.number().int().positive(), id: z.number().int().positive(),
name: z.string().min(1), 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), elements: z.array(setElementNodeSchema),
}) })

View file

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