add sparse placement controls
This commit is contained in:
parent
6f2c49026a
commit
becd7573c6
3 changed files with 78 additions and 17 deletions
|
|
@ -38,10 +38,11 @@ When creating a schedule, the user selects:
|
||||||
* The start date
|
* The start date
|
||||||
* The target completion date
|
* The target completion date
|
||||||
|
|
||||||
Attainly then distributes the selected elements evenly across the available
|
Attainly then distributes the selected elements across the available days.
|
||||||
days. When an uneven schedule has more than one assignment per day, the user
|
When there are fewer assignments than days, the user can spread them across
|
||||||
can place the consecutive heavier days at the start, middle, or end of the
|
the full range or pack them into consecutive days at the start, middle, or
|
||||||
schedule.
|
end. When an uneven schedule has more than one assignment per day, the user
|
||||||
|
can place the consecutive heavier days at the start, middle, or end.
|
||||||
|
|
||||||
For example, a user could choose to schedule:
|
For example, a user could choose to schedule:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { API_BASE } from '@/utils/apiBase'
|
||||||
|
|
||||||
const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
|
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({
|
export const scheduleSummarySchema = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import { useSetLayoutStore } from '@/stores/setLayout'
|
||||||
|
|
||||||
type ScheduleField = 'levelId' | 'startDate' | 'targetDate'
|
type ScheduleField = 'levelId' | 'startDate' | 'targetDate'
|
||||||
type ScheduleFieldErrors = Partial<Record<ScheduleField, string>>
|
type ScheduleFieldErrors = Partial<Record<ScheduleField, string>>
|
||||||
|
type PackedWorkloadPlacement = Exclude<WorkloadPlacement, 'spread'>
|
||||||
|
type PlacementContext = 'unavailable' | 'sparse' | 'heavy' | 'balanced'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -26,43 +28,84 @@ const form = reactive({
|
||||||
levelId: null as number | null,
|
levelId: null as number | null,
|
||||||
startDate: '',
|
startDate: '',
|
||||||
targetDate: '',
|
targetDate: '',
|
||||||
workloadPlacement: 'middle' as WorkloadPlacement,
|
|
||||||
})
|
})
|
||||||
|
const sparseWorkloadPlacement = ref<WorkloadPlacement>('spread')
|
||||||
|
const heavyWorkloadPlacement = ref<PackedWorkloadPlacement>('middle')
|
||||||
const fieldErrors = ref<ScheduleFieldErrors>({})
|
const fieldErrors = ref<ScheduleFieldErrors>({})
|
||||||
|
|
||||||
const levels = computed(() =>
|
const levels = computed(() =>
|
||||||
(layout.value?.levels ?? []).filter((level) => level.elementCount > 0),
|
(layout.value?.levels ?? []).filter((level) => level.elementCount > 0),
|
||||||
)
|
)
|
||||||
const selectedLevel = computed(() => levels.value.find((level) => level.id === form.levelId))
|
const selectedLevel = computed(() => levels.value.find((level) => level.id === form.levelId))
|
||||||
const workloadPlacementOptions: Array<{
|
const packedWorkloadPlacementOptions: Array<{
|
||||||
value: WorkloadPlacement
|
value: PackedWorkloadPlacement
|
||||||
label: string
|
label: string
|
||||||
}> = [
|
}> = [
|
||||||
{ value: 'start', label: 'At the start' },
|
{ value: 'start', label: 'At the start' },
|
||||||
{ value: 'middle', label: 'In the middle' },
|
{ value: 'middle', label: 'In the middle' },
|
||||||
{ value: 'end', label: 'At the end' },
|
{ value: 'end', label: 'At the end' },
|
||||||
]
|
]
|
||||||
const showWorkloadPlacement = computed(() => {
|
const placementContext = computed<PlacementContext>(() => {
|
||||||
if (
|
if (
|
||||||
selectedLevel.value === undefined ||
|
selectedLevel.value === undefined ||
|
||||||
form.startDate === '' ||
|
form.startDate === '' ||
|
||||||
form.targetDate === '' ||
|
form.targetDate === '' ||
|
||||||
form.targetDate < form.startDate
|
form.targetDate < form.startDate
|
||||||
) {
|
) {
|
||||||
return false
|
return 'unavailable'
|
||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.parse(`${form.startDate}T00:00:00Z`)
|
const startTime = Date.parse(`${form.startDate}T00:00:00Z`)
|
||||||
const targetTime = Date.parse(`${form.targetDate}T00:00:00Z`)
|
const targetTime = Date.parse(`${form.targetDate}T00:00:00Z`)
|
||||||
if (Number.isNaN(startTime) || Number.isNaN(targetTime)) {
|
if (Number.isNaN(startTime) || Number.isNaN(targetTime)) {
|
||||||
return false
|
return 'unavailable'
|
||||||
}
|
}
|
||||||
|
|
||||||
const millisecondsPerDay = 24 * 60 * 60 * 1000
|
const millisecondsPerDay = 24 * 60 * 60 * 1000
|
||||||
const dayCount = Math.round((targetTime - startTime) / millisecondsPerDay) + 1
|
const dayCount = Math.round((targetTime - startTime) / millisecondsPerDay) + 1
|
||||||
const assignmentCount = selectedLevel.value.elementCount
|
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
|
const scheduleFormSchema = z
|
||||||
|
|
@ -98,7 +141,10 @@ watch(
|
||||||
|
|
||||||
async function submit(): Promise<void> {
|
async function submit(): Promise<void> {
|
||||||
fieldErrors.value = {}
|
fieldErrors.value = {}
|
||||||
const result = scheduleFormSchema.safeParse(form)
|
const result = scheduleFormSchema.safeParse({
|
||||||
|
...form,
|
||||||
|
workloadPlacement: selectedWorkloadPlacement.value,
|
||||||
|
})
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const errors: ScheduleFieldErrors = {}
|
const errors: ScheduleFieldErrors = {}
|
||||||
for (const issue of result.error.issues) {
|
for (const issue of result.error.issues) {
|
||||||
|
|
@ -237,18 +283,28 @@ async function retry(): Promise<void> {
|
||||||
data-workload-placement
|
data-workload-placement
|
||||||
:disabled="creating"
|
:disabled="creating"
|
||||||
>
|
>
|
||||||
<legend>Heavier days</legend>
|
<legend>
|
||||||
<p>
|
{{ 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.
|
Some days need one extra assignment. Choose where those days appear in the schedule.
|
||||||
</p>
|
</p>
|
||||||
<div class="workload-placement__options">
|
<div
|
||||||
|
class="workload-placement__options"
|
||||||
|
:class="{
|
||||||
|
'workload-placement__options--sparse': placementContext === 'sparse',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
v-for="option in workloadPlacementOptions"
|
v-for="option in workloadPlacementOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
class="workload-placement__option"
|
class="workload-placement__option"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.workloadPlacement"
|
v-model="selectedWorkloadPlacement"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="workloadPlacement"
|
name="workloadPlacement"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
|
@ -381,6 +437,10 @@ h1 {
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workload-placement__options--sparse {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.workload-placement__option {
|
.workload-placement__option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue