add sparse placement controls

This commit is contained in:
Yisroel Baum 2026-08-19 22:45:57 +03:00
parent 6f2c49026a
commit becd7573c6
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
3 changed files with 78 additions and 17 deletions

View file

@ -6,7 +6,7 @@ import { API_BASE } from '@/utils/apiBase'
const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
export const workloadPlacementSchema = z.enum(['start', 'middle', 'end'])
export const workloadPlacementSchema = z.enum(['spread', 'start', 'middle', 'end'])
export const scheduleSummarySchema = z.object({
id: z.number().int().positive(),

View file

@ -14,6 +14,8 @@ import { useSetLayoutStore } from '@/stores/setLayout'
type ScheduleField = 'levelId' | 'startDate' | 'targetDate'
type ScheduleFieldErrors = Partial<Record<ScheduleField, string>>
type PackedWorkloadPlacement = Exclude<WorkloadPlacement, 'spread'>
type PlacementContext = 'unavailable' | 'sparse' | 'heavy' | 'balanced'
const route = useRoute()
const router = useRouter()
@ -26,43 +28,84 @@ const form = reactive({
levelId: null as number | null,
startDate: '',
targetDate: '',
workloadPlacement: 'middle' as WorkloadPlacement,
})
const sparseWorkloadPlacement = ref<WorkloadPlacement>('spread')
const heavyWorkloadPlacement = ref<PackedWorkloadPlacement>('middle')
const fieldErrors = ref<ScheduleFieldErrors>({})
const levels = computed(() =>
(layout.value?.levels ?? []).filter((level) => level.elementCount > 0),
)
const selectedLevel = computed(() => levels.value.find((level) => level.id === form.levelId))
const workloadPlacementOptions: Array<{
value: WorkloadPlacement
const packedWorkloadPlacementOptions: Array<{
value: PackedWorkloadPlacement
label: string
}> = [
{ value: 'start', label: 'At the start' },
{ value: 'middle', label: 'In the middle' },
{ value: 'end', label: 'At the end' },
]
const showWorkloadPlacement = computed(() => {
const placementContext = computed<PlacementContext>(() => {
if (
selectedLevel.value === undefined ||
form.startDate === '' ||
form.targetDate === '' ||
form.targetDate < form.startDate
) {
return false
return 'unavailable'
}
const startTime = Date.parse(`${form.startDate}T00:00:00Z`)
const targetTime = Date.parse(`${form.targetDate}T00:00:00Z`)
if (Number.isNaN(startTime) || Number.isNaN(targetTime)) {
return false
return 'unavailable'
}
const millisecondsPerDay = 24 * 60 * 60 * 1000
const dayCount = Math.round((targetTime - startTime) / millisecondsPerDay) + 1
const assignmentCount = selectedLevel.value.elementCount
return assignmentCount > dayCount && assignmentCount % dayCount !== 0
if (assignmentCount < dayCount) {
return 'sparse'
}
if (assignmentCount > dayCount && assignmentCount % dayCount !== 0) {
return 'heavy'
}
return 'balanced'
})
const showWorkloadPlacement = computed(
() => placementContext.value === 'sparse' || placementContext.value === 'heavy',
)
const workloadPlacementOptions = computed<Array<{ value: WorkloadPlacement; label: string }>>(
() => {
if (placementContext.value === 'sparse') {
return [{ value: 'spread', label: 'Spread evenly' }, ...packedWorkloadPlacementOptions]
}
return packedWorkloadPlacementOptions
},
)
const selectedWorkloadPlacement = computed<WorkloadPlacement>({
get() {
if (placementContext.value === 'sparse') {
return sparseWorkloadPlacement.value
}
return heavyWorkloadPlacement.value
},
set(value) {
if (placementContext.value === 'sparse') {
sparseWorkloadPlacement.value = value
return
}
if (value !== 'spread') {
heavyWorkloadPlacement.value = value
}
},
})
const scheduleFormSchema = z
@ -98,7 +141,10 @@ watch(
async function submit(): Promise<void> {
fieldErrors.value = {}
const result = scheduleFormSchema.safeParse(form)
const result = scheduleFormSchema.safeParse({
...form,
workloadPlacement: selectedWorkloadPlacement.value,
})
if (!result.success) {
const errors: ScheduleFieldErrors = {}
for (const issue of result.error.issues) {
@ -237,18 +283,28 @@ async function retry(): Promise<void> {
data-workload-placement
:disabled="creating"
>
<legend>Heavier days</legend>
<p>
<legend>
{{ placementContext === 'sparse' ? 'Assignment days' : 'Heavier days' }}
</legend>
<p v-if="placementContext === 'sparse'">
Spread assignments across the schedule or keep them on consecutive days.
</p>
<p v-else>
Some days need one extra assignment. Choose where those days appear in the schedule.
</p>
<div class="workload-placement__options">
<div
class="workload-placement__options"
:class="{
'workload-placement__options--sparse': placementContext === 'sparse',
}"
>
<label
v-for="option in workloadPlacementOptions"
:key="option.value"
class="workload-placement__option"
>
<input
v-model="form.workloadPlacement"
v-model="selectedWorkloadPlacement"
type="radio"
name="workloadPlacement"
:value="option.value"
@ -381,6 +437,10 @@ h1 {
gap: 0.65rem;
}
.workload-placement__options--sparse {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.workload-placement__option {
display: flex;
align-items: center;