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>
|
<colgroup>
|
||||||
<col class="header" />
|
<col class="header" />
|
||||||
</colgroup>
|
</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}' />
|
<col v-for="col, index in group.cols" :key="index" :style='{"--minutes": col.minutes}' />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -83,16 +83,25 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="location in schedule.locations.values()" :key="location.id">
|
<template v-for="location in schedule.locations.values()" :key="location.id">
|
||||||
<tr v-if="locationRows.has(location.id)">
|
<tr
|
||||||
<th>{{ location.name }}</th>
|
v-if="locationGroups.has(location.id)"
|
||||||
<td
|
v-for="row, index in locationGroups.get(location.id)"
|
||||||
v-for="row, index in locationRows.get(location.id)"
|
:key="index"
|
||||||
:key="index"
|
>
|
||||||
:colSpan="row.span"
|
<th
|
||||||
:class='{"event": row.slots.size, "crew": row.crew }'
|
v-if="index === 0"
|
||||||
:title="row.title"
|
: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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
@ -102,16 +111,25 @@
|
||||||
<td :colSpan="totalColumns"></td>
|
<td :colSpan="totalColumns"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="role in schedule.roles.values()" :key="role.id">
|
<template v-for="role in schedule.roles.values()" :key="role.id">
|
||||||
<tr v-if="roleRows.has(role.id)">
|
<tr
|
||||||
<th>{{ role.name }}</th>
|
v-if="roleGroups.has(role.id)"
|
||||||
<td
|
v-for="row, index in roleGroups.get(role.id)"
|
||||||
v-for="row, index in roleRows.get(role.id)"
|
:key="index"
|
||||||
:key="index"
|
>
|
||||||
:colSpan="row.span"
|
<th
|
||||||
:class='{"shift": row.slots.size }'
|
v-if="index === 0"
|
||||||
:title="row.title"
|
: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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
@ -376,20 +394,22 @@ function tableElementsFromStretches(
|
||||||
type Col = { minutes?: number };
|
type Col = { minutes?: number };
|
||||||
type DayHead = { span: number, isBreak: boolean, content?: string }
|
type DayHead = { span: number, isBreak: boolean, content?: string }
|
||||||
type HourHead = { span: number, isBreak: boolean, isDayShift: 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 LocationCell = { span: number, isBreak: boolean, slot?: ClientScheduleEventSlot, event?: ClientScheduleEvent };
|
||||||
type RoleCell = { span: number, slots: Set<ClientScheduleShiftSlot>, title: string };
|
type LocationRow = LocationCell[];
|
||||||
type ColumnGroup = { start: number, end: number, width: number, className?: string, cols: Col[] };
|
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 columnGroups: ColumnGroup[] = [];
|
||||||
const dayHeaders: DayHead[] = [];
|
const dayHeaders: DayHead[] = [];
|
||||||
const hourHeaders: HourHead[]= [];
|
const hourHeaders: HourHead[]= [];
|
||||||
const locationRows = new Map<number, LocationCell[]>([...locations.keys()].map(id => [id, []]));
|
const locationGroups = new Map<number, LocationRow[]>([...locations.keys()].map(id => [id, []]));
|
||||||
const roleRows = new Map<number, RoleCell[]>([...roles.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 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])));
|
const shiftBySlotId = new Map([...shifts.values()].flatMap?.(shift => [...shift.slots.values()].map(slot =>[slot.id, shift])));
|
||||||
let totalColumns = 0;
|
let totalColumns = 0;
|
||||||
|
|
||||||
function startColumnGroup(start: number, end: number, width: number, className?: string) {
|
function startColumnGroup(start: number, end: number, width: number, isBreak: boolean) {
|
||||||
columnGroups.push({ start, end, width, className, cols: []})
|
columnGroups.push({ start, end, width, isBreak, cols: []})
|
||||||
}
|
}
|
||||||
function startDay(isBreak: boolean, content?: string) {
|
function startDay(isBreak: boolean, content?: string) {
|
||||||
dayHeaders.push({ span: 0, isBreak, content })
|
dayHeaders.push({ span: 0, isBreak, content })
|
||||||
|
@ -397,22 +417,58 @@ function tableElementsFromStretches(
|
||||||
function startHour(isBreak: boolean, content?: string, isDayShift = false) {
|
function startHour(isBreak: boolean, content?: string, isDayShift = false) {
|
||||||
hourHeaders.push({ span: 0, isBreak, isDayShift, content })
|
hourHeaders.push({ span: 0, isBreak, isDayShift, content })
|
||||||
}
|
}
|
||||||
function startLocation(id: number, slots = new Set<ClientScheduleEventSlot>()) {
|
function startLocation(id: number, isBreak: boolean, newSlots = new Set<ClientScheduleEventSlot>()) {
|
||||||
const rows = locationRows.get(id)!;
|
const group = locationGroups.get(id)!;
|
||||||
if (rows.length) {
|
// Remove all slots that are no longer in the new slots.
|
||||||
const row = rows[rows.length - 1];
|
for (const row of group) {
|
||||||
row.title = [...row.slots].map(slot => eventBySlotId.get(slot.id)!.name).join(", ");
|
const cell = row[row.length - 1];
|
||||||
row.crew = [...row.slots].every(slot => eventBySlotId.get(slot.id)!.crew);
|
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>()) {
|
function startRole(id: number, isBreak: boolean, newSlots = new Set<ClientScheduleShiftSlot>()) {
|
||||||
const rows = roleRows.get(id)!;
|
const group = roleGroups.get(id)!;
|
||||||
if (rows.length) {
|
// Remove all slots that are no longer in the new slots.
|
||||||
const row = rows[rows.length - 1];
|
for (const row of group) {
|
||||||
row.title = [...row.slots].map(slot => shiftBySlotId.get(slot.id)!.name).join(", ");
|
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) {
|
function pushColumn(minutes?: number) {
|
||||||
totalColumns += 1;
|
totalColumns += 1;
|
||||||
|
@ -420,12 +476,16 @@ function tableElementsFromStretches(
|
||||||
dayHeaders[dayHeaders.length - 1].span += 1;
|
dayHeaders[dayHeaders.length - 1].span += 1;
|
||||||
hourHeaders[hourHeaders.length - 1].span += 1;
|
hourHeaders[hourHeaders.length - 1].span += 1;
|
||||||
for(const location of locations.values()) {
|
for(const location of locations.values()) {
|
||||||
const row = locationRows.get(location.id)!;
|
const group = locationGroups.get(location.id)!;
|
||||||
row[row.length - 1].span += 1;
|
for (const row of group) {
|
||||||
|
row[row.length - 1].span += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for(const role of roles.values()) {
|
for(const role of roles.values()) {
|
||||||
const row = roleRows.get(role.id)!;
|
const group = roleGroups.get(role.id)!;
|
||||||
row[row.length - 1].span += 1;
|
for (const row of group) {
|
||||||
|
row[row.length - 1].span += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,17 +494,17 @@ function tableElementsFromStretches(
|
||||||
stretch = padStretch(stretch, timezone);
|
stretch = padStretch(stretch, timezone);
|
||||||
const startDate = DateTime.fromMillis(stretch.start, { zone: timezone, locale: accountStore.activeLocale });
|
const startDate = DateTime.fromMillis(stretch.start, { zone: timezone, locale: accountStore.activeLocale });
|
||||||
if (!lastStretch) {
|
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"));
|
startDay(false, startDate.toFormat("yyyy-LL-dd"));
|
||||||
startHour(false, startDate.toFormat("HH:mm"));
|
startHour(false, startDate.toFormat("HH:mm"));
|
||||||
for(const location of locations.values()) {
|
for (const location of locations.values()) {
|
||||||
startLocation(location.id);
|
startLocation(location.id, false);
|
||||||
}
|
}
|
||||||
for(const role of roles.values()) {
|
for (const role of roles.values()) {
|
||||||
startRole(role.id);
|
startRole(role.id, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startColumnGroup(lastStretch.end, stretch.start, 1, "break");
|
startColumnGroup(lastStretch.end, stretch.start, 1, true);
|
||||||
const dayName = startDate.toFormat("yyyy-LL-dd");
|
const dayName = startDate.toFormat("yyyy-LL-dd");
|
||||||
const lastDayHeader = dayHeaders[dayHeaders.length - 1]
|
const lastDayHeader = dayHeaders[dayHeaders.length - 1]
|
||||||
const sameDay = dayName === lastDayHeader.content && lastDayHeader.span;
|
const sameDay = dayName === lastDayHeader.content && lastDayHeader.span;
|
||||||
|
@ -452,22 +512,22 @@ function tableElementsFromStretches(
|
||||||
startDay(true);
|
startDay(true);
|
||||||
startHour(true, "break");
|
startHour(true, "break");
|
||||||
for(const location of locations.values()) {
|
for(const location of locations.values()) {
|
||||||
startLocation(location.id);
|
startLocation(location.id, true);
|
||||||
}
|
}
|
||||||
for(const role of roles.values()) {
|
for(const role of roles.values()) {
|
||||||
startRole(role.id);
|
startRole(role.id, true);
|
||||||
}
|
}
|
||||||
pushColumn();
|
pushColumn();
|
||||||
|
|
||||||
startColumnGroup(stretch.start, stretch.end, (stretch.end - stretch.start) / oneHourMs);
|
startColumnGroup(stretch.start, stretch.end, (stretch.end - stretch.start) / oneHourMs, false);
|
||||||
if (!sameDay)
|
if (!sameDay)
|
||||||
startDay(false, dayName);
|
startDay(false, dayName);
|
||||||
startHour(false, startDate.toFormat("HH:mm"));
|
startHour(false, startDate.toFormat("HH:mm"));
|
||||||
for(const location of locations.values()) {
|
for(const location of locations.values()) {
|
||||||
startLocation(location.id);
|
startLocation(location.id, false);
|
||||||
}
|
}
|
||||||
for(const role of roles.values()) {
|
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;
|
const durationMs = end - cutSpan.start.ts;
|
||||||
|
|
||||||
for (const location of locations.values()) {
|
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();
|
const slots = cutSpan.locations.get(location.id) ?? new Set();
|
||||||
if (!setEquals(slots, row.slots)) {
|
const group = locationGroups.get(location.id)!;
|
||||||
startLocation(location.id, slots);
|
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()) {
|
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();
|
const slots = cutSpan.roles.get(role.id) ?? new Set();
|
||||||
if (!setEquals(slots, row.slots)) {
|
const group = roleGroups.get(role.id)!;
|
||||||
startRole(role.id, slots);
|
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,
|
columnGroups,
|
||||||
dayHeaders: dayHeaders.filter(day => day.span),
|
dayHeaders: dayHeaders.filter(day => day.span),
|
||||||
hourHeaders: hourHeaders.filter(hour => hour.span),
|
hourHeaders: hourHeaders.filter(hour => hour.span),
|
||||||
locationRows: new Map([...locationRows]
|
locationGroups: new Map([...locationGroups]
|
||||||
.filter(([_, cells]) => cells.some(cell => cell.slots.size))
|
.filter(([_, rows]) => rows.length)
|
||||||
.map(([id, cells]) => [id, cells.filter(cell => cell.span)]))
|
.map(([id, rows]) => [
|
||||||
|
id, rows.map(row => row.filter(cell => cell.span))
|
||||||
|
]))
|
||||||
,
|
,
|
||||||
roleRows: new Map([...roleRows]
|
roleGroups: new Map([...roleGroups]
|
||||||
.filter(([_, cells]) => cells.some(cell => cell.slots.size))
|
.filter(([_, rows]) => rows.length)
|
||||||
.map(([id, cells]) => [id, cells.filter(cell => cell.span)]))
|
.map(([id, rows]) => [
|
||||||
|
id, rows.map(row => row.filter(cell => cell.span))
|
||||||
|
]))
|
||||||
,
|
,
|
||||||
eventBySlotId,
|
eventBySlotId,
|
||||||
};
|
};
|
||||||
|
@ -579,8 +643,8 @@ const totalColumns = computed(() => elements.value.totalColumns);
|
||||||
const columnGroups = computed(() => elements.value.columnGroups);
|
const columnGroups = computed(() => elements.value.columnGroups);
|
||||||
const dayHeaders = computed(() => elements.value.dayHeaders);
|
const dayHeaders = computed(() => elements.value.dayHeaders);
|
||||||
const hourHeaders = computed(() => elements.value.hourHeaders);
|
const hourHeaders = computed(() => elements.value.hourHeaders);
|
||||||
const locationRows = computed(() => elements.value.locationRows);
|
const locationGroups = computed(() => elements.value.locationGroups);
|
||||||
const roleRows = computed(() => elements.value.roleRows);
|
const roleGroups = computed(() => elements.value.roleGroups);
|
||||||
|
|
||||||
const now = useState(() => Math.round(Date.now() / oneMinMs) * oneMinMs);
|
const now = useState(() => Math.round(Date.now() / oneMinMs) * oneMinMs);
|
||||||
const interval = ref<any>();
|
const interval = ref<any>();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue