Allow shifts without a role

Allow a shift to have no role associated with it in order to simplify
conflict resolution around situations like a shift being created while
the role it was assoiated with was deleted. This also allows for shifts
that are freestanding to be created in case having a role doesn't make
sense for it.
This commit is contained in:
Hornwitser 2025-06-30 16:14:40 +02:00
parent 1d2edf7535
commit 5144bf2b37
5 changed files with 32 additions and 55 deletions

View file

@ -105,22 +105,21 @@
</td>
</tr>
</template>
<template v-if="schedule.roles.size">
<template v-if="roleGroups.size">
<tr>
<th>Shifts</th>
<td :colSpan="totalColumns"></td>
</tr>
<template v-for="role in schedule.roles.values()" :key="role.id">
<template v-for="[id, roleGroup] in roleGroups" :key="id">
<tr
v-if="roleGroups.has(role.id)"
v-for="row, index in roleGroups.get(role.id)"
v-for="row, index in roleGroup"
:key="index"
>
<th
v-if="index === 0"
:rowSpan="roleGroups.get(role.id)!.length"
:rowSpan="roleGroup.length"
>
{{ role.name }}
{{ schedule.roles.get(id!)?.name }}
</th>
<td
v-for="cell, index in row"
@ -152,7 +151,7 @@ const oneMinMs = 60 * 1000;
/** Point in time where a time slots starts or ends. */
type Edge =
| { type: "start" | "end", source: "event", slot: ClientScheduleEventSlot }
| { type: "start" | "end", source: "shift", roleId: Id, slot: ClientScheduleShiftSlot }
| { type: "start" | "end", source: "shift", roleId?: Id, slot: ClientScheduleShiftSlot }
;
/** Point in time where multiple edges meet. */
@ -163,7 +162,7 @@ type Span = {
start: Junction;
end: Junction,
locations: Map<number, Set<ClientScheduleEventSlot>>,
roles: Map<number, Set<ClientScheduleShiftSlot>>,
roles: Map<number | undefined, Set<ClientScheduleShiftSlot>>,
};
/**
@ -238,9 +237,10 @@ function* spansFromJunctions(
const activeLocations = new Map(
[...locations.keys()].map(id => [id, new Set<ClientScheduleEventSlot>()])
);
const activeRoles = new Map(
[...roles.keys()].map(id => [id, new Set<ClientScheduleShiftSlot>()])
const activeRoles = new Map<number | undefined, Set<ClientScheduleShiftSlot>>(
[...roles.keys()].map(id => [id, new Set()]),
);
activeRoles.set(undefined, new Set());
for (const [start, end] of pairs(junctions)) {
for (const edge of start.edges) {
if (edge.type === "start") {
@ -249,7 +249,7 @@ function* spansFromJunctions(
activeLocations.get(locationId)?.add(edge.slot)
}
} else if (edge.source === "shift") {
activeRoles.get(edge.roleId)?.add(edge.slot)
activeRoles.get(edge.roleId)?.add(edge.slot);
}
}
}
@ -403,7 +403,8 @@ function tableElementsFromStretches(
const dayHeaders: DayHead[] = [];
const hourHeaders: HourHead[]= [];
const locationGroups = new Map<number, LocationRow[]>([...locations.keys()].map(id => [id, []]));
const roleGroups = new Map<number, RoleRow[]>([...roles.keys()].map(id => [id, []]));
const roleGroups = new Map<number | undefined, RoleRow[]>([...roles.keys()].map(id => [id, []]));
roleGroups.set(undefined, []);
const eventBySlotId = new Map([...events.values()].flatMap(event => [...event.slots.values()].map(slot => [slot.id, event])));
const shiftBySlotId = new Map([...shifts.values()].flatMap?.(shift => [...shift.slots.values()].map(slot =>[slot.id, shift])));
let totalColumns = 0;
@ -443,7 +444,7 @@ function tableElementsFromStretches(
row.push({ span: 0, isBreak, slot, event: eventBySlotId.get(slot.id) });
}
}
function startRole(id: number, isBreak: boolean, newSlots = new Set<ClientScheduleShiftSlot>()) {
function startRole(id: number | undefined, isBreak: boolean, newSlots = new Set<ClientScheduleShiftSlot>()) {
const group = roleGroups.get(id)!;
// Remove all slots that are no longer in the new slots.
for (const row of group) {
@ -481,9 +482,8 @@ function tableElementsFromStretches(
row[row.length - 1].span += 1;
}
}
for(const role of roles.values()) {
const group = roleGroups.get(role.id)!;
for (const row of group) {
for (const roleGroup of roleGroups.values()) {
for (const row of roleGroup) {
row[row.length - 1].span += 1;
}
}
@ -500,8 +500,8 @@ function tableElementsFromStretches(
for (const location of locations.values()) {
startLocation(location.id, false);
}
for (const role of roles.values()) {
startRole(role.id, false);
for (const roleId of roleGroups.keys()) {
startRole(roleId, false);
}
} else {
startColumnGroup(lastStretch.end, stretch.start, 1, true);
@ -514,8 +514,8 @@ function tableElementsFromStretches(
for(const location of locations.values()) {
startLocation(location.id, true);
}
for(const role of roles.values()) {
startRole(role.id, true);
for (const roleId of roleGroups.keys()) {
startRole(roleId, true);
}
pushColumn();
@ -526,8 +526,8 @@ function tableElementsFromStretches(
for(const location of locations.values()) {
startLocation(location.id, false);
}
for(const role of roles.values()) {
startRole(role.id, false);
for (const roleId of roleGroups.keys()) {
startRole(roleId, false);
}
}
@ -544,12 +544,12 @@ function tableElementsFromStretches(
startLocation(location.id, false, slots)
}
}
for (const role of roles.values()) {
const slots = cutSpan.roles.get(role.id) ?? new Set();
const group = roleGroups.get(role.id)!;
for (const roleId of roleGroups.keys()) {
const slots = cutSpan.roles.get(roleId) ?? new Set();
const group = roleGroups.get(roleId)!;
const existing = new Set(group.map(row => row[row.length - 1].slot).filter(slot => slot));
if (!setEquals(slots, existing)) {
startRole(role.id, false, slots);
startRole(roleId, false, slots);
}
}