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

@ -39,12 +39,7 @@
v-model="newShiftId"
/>
</td>
<td>
<SelectSingleEntity
:entities="schedule.roles"
v-model="newShiftRoleId"
/>
</td>
<td></td>
<td></td>
<td>
Add at
@ -139,12 +134,7 @@
v-model="newShiftId"
/>
</td>
<td>
<SelectSingleEntity
:entities="schedule.roles"
v-model="newShiftRoleId"
/>
</td>
<td></td>
<td colspan="2">
<button
type="button"
@ -244,10 +234,6 @@ const newShiftEnd = computed({
newShiftDuration.value = dropDay(end.diff(start)).toFormat("hh:mm");
},
});
const newShiftRoleId = ref(props.roleId);
watch(() => props.roleId, () => {
newShiftRoleId.value = props.roleId;
});
function endFromTime(start: DateTime, time: string) {
let end = start.startOf("day").plus(Duration.fromISOTime(time, { locale: accountStore.activeLocale }));
@ -291,11 +277,6 @@ function newShiftSlot(options: { start?: DateTime, end?: DateTime } = {}) {
alert("Invalid shift");
return;
}
const role = schedule.value.roles.get(newShiftRoleId.value!);
if (!role) {
alert("Invalid role");
return;
}
let start;
let end;
const duration = durationFromTime(newShiftDuration.value);

View file

@ -131,10 +131,6 @@ function newShift() {
alert(`Shift ${newShiftName.value} already exists`);
return;
}
if (newShiftRoleId.value === undefined) {
alert(`Invalid role`);
return;
}
const zone = Info.normalizeZone(accountStore.activeTimezone);
const locale = accountStore.activeLocale;
const shift = ClientScheduleShift.create(

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);
}
}

View file

@ -111,7 +111,7 @@ export const apiScheduleShiftSlotSchema = z.object({
export type ApiScheduleShiftSlot = z.infer<typeof apiScheduleShiftSlotSchema>;
export const apiScheduleShiftSchema = defineApiEntity({
roleId: idSchema,
roleId: z.optional(idSchema),
name: z.string(),
description: z.optional(z.string()),
slots: z.array(apiScheduleShiftSlotSchema),

View file

@ -515,7 +515,7 @@ export class ClientScheduleRole extends ClientEntity<ApiScheduleRole> {
export class ClientScheduleShift extends ClientEntity<ApiScheduleShift> {
schedule!: ClientSchedule;
serverRoleId: Id;
serverRoleId: Id | undefined;
serverName: string;
serverDescription: string;
serverSlotIds: Set<Id>;
@ -524,7 +524,7 @@ export class ClientScheduleShift extends ClientEntity<ApiScheduleShift> {
id: Id,
updatedAt: DateTime,
deleted: boolean,
public roleId: Id,
public roleId: Id | undefined,
public name: string,
public description: string,
public slotIds: Set<Id>,
@ -572,7 +572,7 @@ export class ClientScheduleShift extends ClientEntity<ApiScheduleShift> {
static create(
schedule: ClientSchedule,
id: Id,
roleId: Id,
roleId: Id | undefined,
name: string,
description: string,
slotIds: Set<Id>,