Render overlapping events/shifts in separate rows
Instead of merging overlapping events and shifts when displaying them in the timetable which causes a very confusing display, add new rows when events overlap so that each event can be fully displayed without any overlapping in the table.
This commit is contained in:
parent
5662b890de
commit
016930f933
1 changed files with 134 additions and 70 deletions
|
@ -38,7 +38,7 @@
|
|||
<colgroup>
|
||||
<col class="header" />
|
||||
</colgroup>
|
||||
<colgroup v-for="group, groupIndex in columnGroups" :key="groupIndex" :class="group.className">
|
||||
<colgroup v-for="group, groupIndex in columnGroups" :key="groupIndex" :class="{ break: group.isBreak }">
|
||||
<col v-for="col, index in group.cols" :key="index" :style='{"--minutes": col.minutes}' />
|
||||
</colgroup>
|
||||
<thead>
|
||||
|
@ -83,16 +83,25 @@
|
|||
</td>
|
||||
</tr>
|
||||
<template v-for="location in schedule.locations.values()" :key="location.id">
|
||||
<tr v-if="locationRows.has(location.id)">
|
||||
<th>{{ location.name }}</th>
|
||||
<td
|
||||
v-for="row, index in locationRows.get(location.id)"
|
||||
:key="index"
|
||||
:colSpan="row.span"
|
||||
:class='{"event": row.slots.size, "crew": row.crew }'
|
||||
:title="row.title"
|
||||
<tr
|
||||
v-if="locationGroups.has(location.id)"
|
||||
v-for="row, index in locationGroups.get(location.id)"
|
||||
:key="index"
|
||||
>
|
||||
<th
|
||||
v-if="index === 0"
|
||||
:rowSpan="locationGroups.get(location.id)!.length"
|
||||
>
|
||||
{{ row.title }}
|
||||
{{ location.name }}
|
||||
</th>
|
||||
<td
|
||||
v-for="cell, index in row"
|
||||
:key="index"
|
||||
:colSpan="cell.span"
|
||||
:class='{"event": cell.slot, "crew": cell.event?.crew }'
|
||||
:title="cell.event?.name"
|
||||
>
|
||||
{{ cell.event?.name }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
@ -102,16 +111,25 @@
|
|||
<td :colSpan="totalColumns"></td>
|
||||
</tr>
|
||||
<template v-for="role in schedule.roles.values()" :key="role.id">
|
||||
<tr v-if="roleRows.has(role.id)">
|
||||
<th>{{ role.name }}</th>
|
||||
<td
|
||||
v-for="row, index in roleRows.get(role.id)"
|
||||
:key="index"
|
||||
:colSpan="row.span"
|
||||
:class='{"shift": row.slots.size }'
|
||||
:title="row.title"
|
||||
<tr
|
||||
v-if="roleGroups.has(role.id)"
|
||||
v-for="row, index in roleGroups.get(role.id)"
|
||||
:key="index"
|
||||
>
|
||||
<th
|
||||
v-if="index === 0"
|
||||
:rowSpan="roleGroups.get(role.id)!.length"
|
||||
>
|
||||
{{ row.title }}
|
||||
{{ role.name }}
|
||||
</th>
|
||||
<td
|
||||
v-for="cell, index in row"
|
||||
:key="index"
|
||||
:colSpan="cell.span"
|
||||
:class='{"shift": cell.slot }'
|
||||
:title="cell.shift?.name"
|
||||
>
|
||||
{{ cell.shift?.name }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
@ -376,20 +394,22 @@ function tableElementsFromStretches(
|
|||
type Col = { minutes?: number };
|
||||
type DayHead = { span: number, isBreak: boolean, content?: string }
|
||||
type HourHead = { span: number, isBreak: boolean, isDayShift: boolean, content?: string }
|
||||
type LocationCell = { span: number, slots: Set<ClientScheduleEventSlot>, title: string, crew?: boolean }
|
||||
type RoleCell = { span: number, slots: Set<ClientScheduleShiftSlot>, title: string };
|
||||
type ColumnGroup = { start: number, end: number, width: number, className?: string, cols: Col[] };
|
||||
type LocationCell = { span: number, isBreak: boolean, slot?: ClientScheduleEventSlot, event?: ClientScheduleEvent };
|
||||
type LocationRow = LocationCell[];
|
||||
type RoleCell = { span: number, isBreak: boolean, slot?: ClientScheduleShiftSlot, shift?: ClientScheduleShift };
|
||||
type RoleRow = RoleCell[];
|
||||
type ColumnGroup = { start: number, end: number, width: number, isBreak: boolean, cols: Col[] };
|
||||
const columnGroups: ColumnGroup[] = [];
|
||||
const dayHeaders: DayHead[] = [];
|
||||
const hourHeaders: HourHead[]= [];
|
||||
const locationRows = new Map<number, LocationCell[]>([...locations.keys()].map(id => [id, []]));
|
||||
const roleRows = new Map<number, RoleCell[]>([...roles.keys()].map(id => [id, []]));
|
||||
const locationGroups = new Map<number, LocationRow[]>([...locations.keys()].map(id => [id, []]));
|
||||
const roleGroups = new Map<number, RoleRow[]>([...roles.keys()].map(id => [id, []]));
|
||||
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;
|
||||
|
||||
function startColumnGroup(start: number, end: number, width: number, className?: string) {
|
||||
columnGroups.push({ start, end, width, className, cols: []})
|
||||
function startColumnGroup(start: number, end: number, width: number, isBreak: boolean) {
|
||||
columnGroups.push({ start, end, width, isBreak, cols: []})
|
||||
}
|
||||
function startDay(isBreak: boolean, content?: string) {
|
||||
dayHeaders.push({ span: 0, isBreak, content })
|
||||
|
@ -397,22 +417,58 @@ function tableElementsFromStretches(
|
|||
function startHour(isBreak: boolean, content?: string, isDayShift = false) {
|
||||
hourHeaders.push({ span: 0, isBreak, isDayShift, content })
|
||||
}
|
||||
function startLocation(id: number, slots = new Set<ClientScheduleEventSlot>()) {
|
||||
const rows = locationRows.get(id)!;
|
||||
if (rows.length) {
|
||||
const row = rows[rows.length - 1];
|
||||
row.title = [...row.slots].map(slot => eventBySlotId.get(slot.id)!.name).join(", ");
|
||||
row.crew = [...row.slots].every(slot => eventBySlotId.get(slot.id)!.crew);
|
||||
function startLocation(id: number, isBreak: boolean, newSlots = new Set<ClientScheduleEventSlot>()) {
|
||||
const group = locationGroups.get(id)!;
|
||||
// Remove all slots that are no longer in the new slots.
|
||||
for (const row of group) {
|
||||
const cell = row[row.length - 1];
|
||||
if (cell.isBreak !== isBreak || cell.slot && !newSlots.has(cell.slot))
|
||||
row.push({ span: 0, isBreak, slot: undefined, event: undefined });
|
||||
}
|
||||
const existingSlots = new Set(group.map(row => row[row.length - 1].slot).filter(slot => slot));
|
||||
// Add all new slots that do not already exist.
|
||||
for (const slot of newSlots.difference(existingSlots)) {
|
||||
let row = group.find(row => !row[row.length - 1].slot);
|
||||
if (!row) {
|
||||
row = columnGroups.map(
|
||||
colGroup => ({
|
||||
span: colGroup.cols.length,
|
||||
isBreak: colGroup.isBreak,
|
||||
slot: undefined,
|
||||
event: undefined
|
||||
})
|
||||
);
|
||||
group.push(row);
|
||||
}
|
||||
row.push({ span: 0, isBreak, slot, event: eventBySlotId.get(slot.id) });
|
||||
}
|
||||
rows.push({ span: 0, slots, title: "" });
|
||||
}
|
||||
function startRole(id: number, slots = new Set<ClientScheduleShiftSlot>()) {
|
||||
const rows = roleRows.get(id)!;
|
||||
if (rows.length) {
|
||||
const row = rows[rows.length - 1];
|
||||
row.title = [...row.slots].map(slot => shiftBySlotId.get(slot.id)!.name).join(", ");
|
||||
function startRole(id: number, 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) {
|
||||
const cell = row[row.length - 1];
|
||||
if (cell.isBreak !== isBreak || cell.slot && !newSlots.has(cell.slot)) {
|
||||
row.push({ span: 0, isBreak, slot: undefined, shift: undefined });
|
||||
}
|
||||
}
|
||||
const existingSlots = new Set(group.map(row => row[row.length - 1].slot).filter(slot => slot));
|
||||
// Add all new slots that do not already exist.
|
||||
for (const slot of newSlots.difference(existingSlots)) {
|
||||
let row = group.find(row => !row[row.length - 1].slot);
|
||||
if (!row) {
|
||||
row = columnGroups.map(
|
||||
colGroup => ({
|
||||
span: colGroup.cols.length,
|
||||
isBreak: colGroup.isBreak,
|
||||
slot: undefined,
|
||||
shift: undefined
|
||||
})
|
||||
);
|
||||
group.push(row);
|
||||
}
|
||||
row.push({ span: 0, isBreak, slot, shift: shiftBySlotId.get(slot.id) });
|
||||
}
|
||||
rows.push({ span: 0, slots, title: "" });
|
||||
}
|
||||
function pushColumn(minutes?: number) {
|
||||
totalColumns += 1;
|
||||
|
@ -420,12 +476,16 @@ function tableElementsFromStretches(
|
|||
dayHeaders[dayHeaders.length - 1].span += 1;
|
||||
hourHeaders[hourHeaders.length - 1].span += 1;
|
||||
for(const location of locations.values()) {
|
||||
const row = locationRows.get(location.id)!;
|
||||
row[row.length - 1].span += 1;
|
||||
const group = locationGroups.get(location.id)!;
|
||||
for (const row of group) {
|
||||
row[row.length - 1].span += 1;
|
||||
}
|
||||
}
|
||||
for(const role of roles.values()) {
|
||||
const row = roleRows.get(role.id)!;
|
||||
row[row.length - 1].span += 1;
|
||||
const group = roleGroups.get(role.id)!;
|
||||
for (const row of group) {
|
||||
row[row.length - 1].span += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,17 +494,17 @@ function tableElementsFromStretches(
|
|||
stretch = padStretch(stretch, timezone);
|
||||
const startDate = DateTime.fromMillis(stretch.start, { zone: timezone, locale: accountStore.activeLocale });
|
||||
if (!lastStretch) {
|
||||
startColumnGroup(stretch.start, stretch.end, (stretch.end - stretch.start) / oneHourMs);
|
||||
startColumnGroup(stretch.start, stretch.end, (stretch.end - stretch.start) / oneHourMs, false);
|
||||
startDay(false, startDate.toFormat("yyyy-LL-dd"));
|
||||
startHour(false, startDate.toFormat("HH:mm"));
|
||||
for(const location of locations.values()) {
|
||||
startLocation(location.id);
|
||||
for (const location of locations.values()) {
|
||||
startLocation(location.id, false);
|
||||
}
|
||||
for(const role of roles.values()) {
|
||||
startRole(role.id);
|
||||
for (const role of roles.values()) {
|
||||
startRole(role.id, false);
|
||||
}
|
||||
} else {
|
||||
startColumnGroup(lastStretch.end, stretch.start, 1, "break");
|
||||
startColumnGroup(lastStretch.end, stretch.start, 1, true);
|
||||
const dayName = startDate.toFormat("yyyy-LL-dd");
|
||||
const lastDayHeader = dayHeaders[dayHeaders.length - 1]
|
||||
const sameDay = dayName === lastDayHeader.content && lastDayHeader.span;
|
||||
|
@ -452,22 +512,22 @@ function tableElementsFromStretches(
|
|||
startDay(true);
|
||||
startHour(true, "break");
|
||||
for(const location of locations.values()) {
|
||||
startLocation(location.id);
|
||||
startLocation(location.id, true);
|
||||
}
|
||||
for(const role of roles.values()) {
|
||||
startRole(role.id);
|
||||
startRole(role.id, true);
|
||||
}
|
||||
pushColumn();
|
||||
|
||||
startColumnGroup(stretch.start, stretch.end, (stretch.end - stretch.start) / oneHourMs);
|
||||
startColumnGroup(stretch.start, stretch.end, (stretch.end - stretch.start) / oneHourMs, false);
|
||||
if (!sameDay)
|
||||
startDay(false, dayName);
|
||||
startHour(false, startDate.toFormat("HH:mm"));
|
||||
for(const location of locations.values()) {
|
||||
startLocation(location.id);
|
||||
startLocation(location.id, false);
|
||||
}
|
||||
for(const role of roles.values()) {
|
||||
startRole(role.id);
|
||||
startRole(role.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -477,19 +537,19 @@ function tableElementsFromStretches(
|
|||
const durationMs = end - cutSpan.start.ts;
|
||||
|
||||
for (const location of locations.values()) {
|
||||
const rows = locationRows.get(location.id)!;
|
||||
const row = rows[rows.length - 1];
|
||||
const slots = cutSpan.locations.get(location.id) ?? new Set();
|
||||
if (!setEquals(slots, row.slots)) {
|
||||
startLocation(location.id, slots);
|
||||
const group = locationGroups.get(location.id)!;
|
||||
const existing = new Set(group.map(row => row[row.length - 1].slot).filter(slot => slot));
|
||||
if (!setEquals(slots, existing)) {
|
||||
startLocation(location.id, false, slots)
|
||||
}
|
||||
}
|
||||
for (const role of roles.values()) {
|
||||
const rows = roleRows.get(role.id)!;
|
||||
const row = rows[rows.length - 1];
|
||||
const slots = cutSpan.roles.get(role.id) ?? new Set();
|
||||
if (!setEquals(slots, row.slots)) {
|
||||
startRole(role.id, slots);
|
||||
const group = roleGroups.get(role.id)!;
|
||||
const existing = new Set(group.map(row => row[row.length - 1].slot).filter(slot => slot));
|
||||
if (!setEquals(slots, existing)) {
|
||||
startRole(role.id, false, slots);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -522,13 +582,17 @@ function tableElementsFromStretches(
|
|||
columnGroups,
|
||||
dayHeaders: dayHeaders.filter(day => day.span),
|
||||
hourHeaders: hourHeaders.filter(hour => hour.span),
|
||||
locationRows: new Map([...locationRows]
|
||||
.filter(([_, cells]) => cells.some(cell => cell.slots.size))
|
||||
.map(([id, cells]) => [id, cells.filter(cell => cell.span)]))
|
||||
locationGroups: new Map([...locationGroups]
|
||||
.filter(([_, rows]) => rows.length)
|
||||
.map(([id, rows]) => [
|
||||
id, rows.map(row => row.filter(cell => cell.span))
|
||||
]))
|
||||
,
|
||||
roleRows: new Map([...roleRows]
|
||||
.filter(([_, cells]) => cells.some(cell => cell.slots.size))
|
||||
.map(([id, cells]) => [id, cells.filter(cell => cell.span)]))
|
||||
roleGroups: new Map([...roleGroups]
|
||||
.filter(([_, rows]) => rows.length)
|
||||
.map(([id, rows]) => [
|
||||
id, rows.map(row => row.filter(cell => cell.span))
|
||||
]))
|
||||
,
|
||||
eventBySlotId,
|
||||
};
|
||||
|
@ -579,8 +643,8 @@ const totalColumns = computed(() => elements.value.totalColumns);
|
|||
const columnGroups = computed(() => elements.value.columnGroups);
|
||||
const dayHeaders = computed(() => elements.value.dayHeaders);
|
||||
const hourHeaders = computed(() => elements.value.hourHeaders);
|
||||
const locationRows = computed(() => elements.value.locationRows);
|
||||
const roleRows = computed(() => elements.value.roleRows);
|
||||
const locationGroups = computed(() => elements.value.locationGroups);
|
||||
const roleGroups = computed(() => elements.value.roleGroups);
|
||||
|
||||
const now = useState(() => Math.round(Date.now() / oneMinMs) * oneMinMs);
|
||||
const interval = ref<any>();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue