Refactor to use ClientSchedule on client
Use the ClientSchedule data structure for deserialising and tracking edit state on the client instead of trying to directly deal with the ApiSchedule type which is not build for ease of edits or rendering.
This commit is contained in:
parent
ce9f758f84
commit
bb450fd583
15 changed files with 488 additions and 1297 deletions
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<figure class="timetable" v-if="schedule.deleted">
|
||||
<p>
|
||||
Error: Schedule is deleted.
|
||||
</p>
|
||||
</figure>
|
||||
<figure class="timetable" v-else>
|
||||
<figure class="timetable">
|
||||
<details>
|
||||
<summary>Debug</summary>
|
||||
<details>
|
||||
|
@ -61,7 +56,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="location in schedule.locations?.filter(l => !l.deleted)" :key="location.id">
|
||||
<template v-for="location in schedule.locations.values()" :key="location.id">
|
||||
<tr v-if="locationRows.has(location.id)">
|
||||
<th>{{ location.name }}</th>
|
||||
<td
|
||||
|
@ -75,12 +70,12 @@
|
|||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-if="schedule.roles">
|
||||
<template v-if="schedule.roles.size">
|
||||
<tr>
|
||||
<th>Shifts</th>
|
||||
<td :colSpan="totalColumns"></td>
|
||||
</tr>
|
||||
<template v-for="role in schedule.roles?.filter(r => !r.deleted)" :key="role.id">
|
||||
<template v-for="role in schedule.roles.values()" :key="role.id">
|
||||
<tr v-if="roleRows.has(role.id)">
|
||||
<th>{{ role.name }}</th>
|
||||
<td
|
||||
|
@ -102,7 +97,6 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { DateTime } from "luxon";
|
||||
import type { ApiSchedule, ApiScheduleEvent, ApiScheduleEventSlot, ApiScheduleLocation, ApiScheduleRole, ApiScheduleShift, ApiScheduleShiftSlot } from "~/shared/types/api";
|
||||
import type { Id } from "~/shared/types/common";
|
||||
import { pairs, setEquals } from "~/shared/utils/functions";
|
||||
|
||||
|
@ -113,8 +107,8 @@ const oneMinMs = 60 * 1000;
|
|||
|
||||
/** Point in time where a time slots starts or ends. */
|
||||
type Edge =
|
||||
| { type: "start" | "end", source: "event", slot: ApiScheduleEventSlot }
|
||||
| { type: "start" | "end", source: "shift", roleId: Id, slot: ApiScheduleShiftSlot }
|
||||
| { type: "start" | "end", source: "event", slot: ClientScheduleEventSlot }
|
||||
| { type: "start" | "end", source: "shift", roleId: Id, slot: ClientScheduleShiftSlot }
|
||||
;
|
||||
|
||||
/** Point in time where multiple edges meet. */
|
||||
|
@ -124,8 +118,8 @@ type Junction = { ts: number, edges: Edge[] };
|
|||
type Span = {
|
||||
start: Junction;
|
||||
end: Junction,
|
||||
locations: Map<number, Set<ApiScheduleEventSlot>>,
|
||||
roles: Map<number, Set<ApiScheduleShiftSlot>>,
|
||||
locations: Map<number, Set<ClientScheduleEventSlot>>,
|
||||
roles: Map<number, Set<ClientScheduleShiftSlot>>,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -140,11 +134,15 @@ type Stretch = {
|
|||
}
|
||||
|
||||
function* edgesFromEvents(
|
||||
events: Iterable<Extract<ApiScheduleEvent, { deleted?: false }>>,
|
||||
filter = (slot: ApiScheduleEventSlot) => true,
|
||||
events: Iterable<ClientScheduleEvent>,
|
||||
filter = (slot: ClientScheduleEventSlot) => true,
|
||||
): Generator<Edge> {
|
||||
for (const event of events) {
|
||||
for (const slot of event.slots.filter(filter)) {
|
||||
if (event.deleted)
|
||||
continue;
|
||||
for (const slot of event.slots.values()) {
|
||||
if (!filter(slot) || slot.deleted)
|
||||
continue;
|
||||
if (slot.start > slot.end) {
|
||||
throw new Error(`Slot ${slot.id} ends before it starts.`);
|
||||
}
|
||||
|
@ -155,16 +153,20 @@ function* edgesFromEvents(
|
|||
}
|
||||
|
||||
function* edgesFromShifts(
|
||||
shifts: Iterable<Extract<ApiScheduleShift, { deleted?: false }>>,
|
||||
filter = (slot: ApiScheduleShiftSlot) => true,
|
||||
shifts: Iterable<ClientScheduleShift>,
|
||||
filter = (slot: ClientScheduleShiftSlot) => true,
|
||||
): Generator<Edge> {
|
||||
for (const shift of shifts) {
|
||||
for (const slot of shift.slots.filter(filter)) {
|
||||
if (shift.deleted)
|
||||
continue;
|
||||
for (const slot of shift.slots.values()) {
|
||||
if (!filter(slot) || slot.deleted)
|
||||
continue;
|
||||
if (slot.start > slot.end) {
|
||||
throw new Error(`Slot ${slot.id} ends before it starts.`);
|
||||
}
|
||||
yield { type: "start", source: "shift", roleId: shift.roleId, slot };
|
||||
yield { type: "end", source: "shift", roleId: shift.roleId, slot };
|
||||
yield { type: "start", source: "shift", roleId: shift.role.id, slot };
|
||||
yield { type: "end", source: "shift", roleId: shift.role.id, slot };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +174,7 @@ function* edgesFromShifts(
|
|||
function junctionsFromEdges(edges: Iterable<Edge>) {
|
||||
const junctions = new Map<number, Junction>();
|
||||
for (const edge of edges) {
|
||||
const ts = Date.parse(edge.slot[edge.type]);
|
||||
const ts = edge.slot[edge.type].toMillis();
|
||||
const junction = junctions.get(ts);
|
||||
if (junction) {
|
||||
junction.edges.push(edge);
|
||||
|
@ -186,21 +188,21 @@ function junctionsFromEdges(edges: Iterable<Edge>) {
|
|||
|
||||
function* spansFromJunctions(
|
||||
junctions: Iterable<Junction>,
|
||||
locations: Extract<ApiScheduleLocation, { deleted?: false }>[],
|
||||
roles: Extract<ApiScheduleRole, { deleted?: false }>[],
|
||||
locations: Map<Id, ClientScheduleLocation>,
|
||||
roles: Map<Id, ClientScheduleRole>,
|
||||
): Generator<Span> {
|
||||
const activeLocations = new Map(
|
||||
locations.map(location => [location.id, new Set<ApiScheduleEventSlot>()])
|
||||
[...locations.keys()].map(id => [id, new Set<ClientScheduleEventSlot>()])
|
||||
);
|
||||
const activeRoles = new Map(
|
||||
roles?.map(role => [role.id, new Set<ApiScheduleShiftSlot>()])
|
||||
[...roles.keys()].map(id => [id, new Set<ClientScheduleShiftSlot>()])
|
||||
);
|
||||
for (const [start, end] of pairs(junctions)) {
|
||||
for (const edge of start.edges) {
|
||||
if (edge.type === "start") {
|
||||
if (edge.source === "event") {
|
||||
for (const id of edge.slot.locationIds) {
|
||||
activeLocations.get(id)?.add(edge.slot)
|
||||
for (const location of edge.slot.locations) {
|
||||
activeLocations.get(location.id)?.add(edge.slot)
|
||||
}
|
||||
} else if (edge.source === "shift") {
|
||||
activeRoles.get(edge.roleId)?.add(edge.slot)
|
||||
|
@ -224,8 +226,8 @@ function* spansFromJunctions(
|
|||
for (const edge of end.edges) {
|
||||
if (edge.type === "end") {
|
||||
if (edge.source === "event") {
|
||||
for (const id of edge.slot.locationIds) {
|
||||
activeLocations.get(id)?.delete(edge.slot)
|
||||
for (const location of edge.slot.locations) {
|
||||
activeLocations.get(location.id)?.delete(edge.slot)
|
||||
}
|
||||
} else if (edge.source === "shift") {
|
||||
activeRoles.get(edge.roleId)?.delete(edge.slot);
|
||||
|
@ -265,7 +267,7 @@ function* stretchesFromSpans(spans: Iterable<Span>, minSeparation: number): Gene
|
|||
|
||||
/** Cuts up a span by whole hours that crosses it */
|
||||
function* cutSpansByHours(span: Span, timezone: string): Generator<Span> {
|
||||
const startHour = DateTime.fromMillis(span.start.ts, { zone: timezone, locale: "en-US" })
|
||||
const startHour = DateTime.fromMillis(span.start.ts, { zone: timezone, locale: accountStore.activeLocale })
|
||||
.startOf("hour")
|
||||
;
|
||||
const end = span.end.ts;
|
||||
|
@ -308,11 +310,11 @@ function* cutSpansByHours(span: Span, timezone: string): Generator<Span> {
|
|||
|
||||
function padStretch(stretch: Stretch, timezone: string): Stretch {
|
||||
// Pad by one hour and extend it to the nearest whole hour.
|
||||
let start = DateTime.fromMillis(stretch.start, { zone: timezone, locale: "en-US" })
|
||||
let start = DateTime.fromMillis(stretch.start, { zone: timezone, locale: accountStore.activeLocale })
|
||||
.minus(oneHourMs)
|
||||
.startOf("hour")
|
||||
;
|
||||
let end = DateTime.fromMillis(stretch.end, { zone: timezone, locale: "en-US" })
|
||||
let end = DateTime.fromMillis(stretch.end, { zone: timezone, locale: accountStore.activeLocale })
|
||||
.plus(2 * oneHourMs - 1)
|
||||
.startOf("hour")
|
||||
;
|
||||
|
@ -339,24 +341,24 @@ function padStretch(stretch: Stretch, timezone: string): Stretch {
|
|||
|
||||
function tableElementsFromStretches(
|
||||
stretches: Iterable<Stretch>,
|
||||
events: Extract<ApiScheduleEvent, { deleted?: false }>[],
|
||||
locations: Extract<ApiScheduleLocation, { deleted?: false }>[],
|
||||
shifts: Extract<ApiScheduleShift, { deleted?: false }>[],
|
||||
roles: Extract<ApiScheduleRole, { deleted?: false }>[],
|
||||
events: Map<Id, ClientScheduleEvent>,
|
||||
locations: Map<Id, ClientScheduleLocation>,
|
||||
shifts: Map<Id, ClientScheduleShift>,
|
||||
roles: Map<Id, ClientScheduleRole>,
|
||||
timezone: string,
|
||||
) {
|
||||
type Col = { minutes?: number };
|
||||
type DayHead = { span: number, content?: string }
|
||||
type HourHead = { span: number, content?: string }
|
||||
type LocationCell = { span: number, slots: Set<ApiScheduleEventSlot>, title: string, crew?: boolean }
|
||||
type RoleCell = { span: number, slots: Set<ApiScheduleShiftSlot>, title: string };
|
||||
type LocationCell = { span: number, slots: Set<ClientScheduleEventSlot>, title: string, crew?: boolean }
|
||||
type RoleCell = { span: number, slots: Set<ClientScheduleShiftSlot>, title: string };
|
||||
const columnGroups: { className?: string, cols: Col[] }[] = [];
|
||||
const dayHeaders: DayHead[] = [];
|
||||
const hourHeaders: HourHead[]= [];
|
||||
const locationRows = new Map<number, LocationCell[]>(locations.map(location => [location.id, []]));
|
||||
const roleRows = new Map<number, RoleCell[]>(roles.map(role => [role.id, []]));
|
||||
const eventBySlotId = new Map(events.flatMap(event => event.slots.map(slot => [slot.id, event])));
|
||||
const shiftBySlotId = new Map(shifts?.flatMap?.(shift => shift.slots.map(slot =>[slot.id, shift])))
|
||||
const locationRows = new Map<number, LocationCell[]>([...locations.keys()].map(id => [id, []]));
|
||||
const roleRows = new Map<number, RoleCell[]>([...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(className?: string) {
|
||||
|
@ -368,7 +370,7 @@ function tableElementsFromStretches(
|
|||
function startHour(content?: string) {
|
||||
hourHeaders.push({ span: 0, content })
|
||||
}
|
||||
function startLocation(id: number, slots = new Set<ApiScheduleEventSlot>()) {
|
||||
function startLocation(id: number, slots = new Set<ClientScheduleEventSlot>()) {
|
||||
const rows = locationRows.get(id)!;
|
||||
if (rows.length) {
|
||||
const row = rows[rows.length - 1];
|
||||
|
@ -377,7 +379,7 @@ function tableElementsFromStretches(
|
|||
}
|
||||
rows.push({ span: 0, slots, title: "" });
|
||||
}
|
||||
function startRole(id: number, slots = new Set<ApiScheduleShiftSlot>()) {
|
||||
function startRole(id: number, slots = new Set<ClientScheduleShiftSlot>()) {
|
||||
const rows = roleRows.get(id)!;
|
||||
if (rows.length) {
|
||||
const row = rows[rows.length - 1];
|
||||
|
@ -390,11 +392,11 @@ function tableElementsFromStretches(
|
|||
columnGroups[columnGroups.length - 1].cols.push({ minutes })
|
||||
dayHeaders[dayHeaders.length - 1].span += 1;
|
||||
hourHeaders[hourHeaders.length - 1].span += 1;
|
||||
for(const location of locations) {
|
||||
for(const location of locations.values()) {
|
||||
const row = locationRows.get(location.id)!;
|
||||
row[row.length - 1].span += 1;
|
||||
}
|
||||
for(const role of roles ?? []) {
|
||||
for(const role of roles.values()) {
|
||||
const row = roleRows.get(role.id)!;
|
||||
row[row.length - 1].span += 1;
|
||||
}
|
||||
|
@ -403,16 +405,16 @@ function tableElementsFromStretches(
|
|||
let first = true;
|
||||
for (let stretch of stretches) {
|
||||
stretch = padStretch(stretch, timezone);
|
||||
const startDate = DateTime.fromMillis(stretch.start, { zone: timezone, locale: "en-US" });
|
||||
const startDate = DateTime.fromMillis(stretch.start, { zone: timezone, locale: accountStore.activeLocale });
|
||||
if (first) {
|
||||
first = false;
|
||||
startColumnGroup();
|
||||
startDay(startDate.toFormat("yyyy-LL-dd"));
|
||||
startHour(startDate.toFormat("HH:mm"));
|
||||
for(const location of locations) {
|
||||
for(const location of locations.values()) {
|
||||
startLocation(location.id);
|
||||
}
|
||||
for(const role of roles ?? []) {
|
||||
for(const role of roles.values()) {
|
||||
startRole(role.id);
|
||||
}
|
||||
} else {
|
||||
|
@ -423,10 +425,10 @@ function tableElementsFromStretches(
|
|||
if (!sameDay)
|
||||
startDay();
|
||||
startHour("break");
|
||||
for(const location of locations) {
|
||||
for(const location of locations.values()) {
|
||||
startLocation(location.id);
|
||||
}
|
||||
for(const role of roles ?? []) {
|
||||
for(const role of roles.values()) {
|
||||
startRole(role.id);
|
||||
}
|
||||
pushColumn();
|
||||
|
@ -435,10 +437,10 @@ function tableElementsFromStretches(
|
|||
if (!sameDay)
|
||||
startDay(dayName);
|
||||
startHour(startDate.toFormat("HH:mm"));
|
||||
for(const location of locations) {
|
||||
for(const location of locations.values()) {
|
||||
startLocation(location.id);
|
||||
}
|
||||
for(const role of roles ?? []) {
|
||||
for(const role of roles.values()) {
|
||||
startRole(role.id);
|
||||
}
|
||||
}
|
||||
|
@ -448,7 +450,7 @@ function tableElementsFromStretches(
|
|||
const end = cutSpan.end.ts;
|
||||
const durationMs = end - cutSpan.start.ts;
|
||||
|
||||
for (const location of locations) {
|
||||
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();
|
||||
|
@ -456,7 +458,7 @@ function tableElementsFromStretches(
|
|||
startLocation(location.id, slots);
|
||||
}
|
||||
}
|
||||
for (const role of roles ?? []) {
|
||||
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();
|
||||
|
@ -466,16 +468,16 @@ function tableElementsFromStretches(
|
|||
}
|
||||
|
||||
pushColumn(durationMs / oneMinMs);
|
||||
const endDate = DateTime.fromMillis(end, { zone: timezone, locale: "en-US" });
|
||||
const endDate = DateTime.fromMillis(end, { zone: timezone, locale: accountStore.activeLocale });
|
||||
if (end === endDate.startOf("day").toMillis()) {
|
||||
startDay(
|
||||
DateTime.fromMillis(cutSpan.end.ts, { zone: timezone, locale: "en-US" })
|
||||
DateTime.fromMillis(cutSpan.end.ts, { zone: timezone, locale: accountStore.activeLocale })
|
||||
.toFormat("yyyy-LL-dd")
|
||||
);
|
||||
}
|
||||
if (end === endDate.startOf("hour").toMillis()) {
|
||||
startHour(
|
||||
DateTime.fromMillis(cutSpan.end.ts, { zone: timezone, locale: "en-US" })
|
||||
DateTime.fromMillis(cutSpan.end.ts, { zone: timezone, locale: accountStore.activeLocale })
|
||||
.toFormat("HH:mm")
|
||||
);
|
||||
}
|
||||
|
@ -501,30 +503,24 @@ function tableElementsFromStretches(
|
|||
}
|
||||
|
||||
const props = defineProps<{
|
||||
schedule: ApiSchedule,
|
||||
eventSlotFilter?: (slot: ApiScheduleEventSlot) => boolean,
|
||||
shiftSlotFilter?: (slot: ApiScheduleShiftSlot) => boolean,
|
||||
schedule: ClientSchedule,
|
||||
eventSlotFilter?: (slot: ClientScheduleEventSlot) => boolean,
|
||||
shiftSlotFilter?: (slot: ClientScheduleShiftSlot) => boolean,
|
||||
}>();
|
||||
const schedule = computed(() => props.schedule);
|
||||
const junctions = computed(() => {
|
||||
if (schedule.value.deleted) {
|
||||
throw Error("Unhandled deleted schedule");
|
||||
}
|
||||
return junctionsFromEdges([
|
||||
...edgesFromEvents(schedule.value.events?.filter(e => !e.deleted) ?? [], props.eventSlotFilter),
|
||||
...edgesFromShifts(schedule.value.shifts?.filter(s => !s.deleted) ?? [], props.shiftSlotFilter),
|
||||
...edgesFromEvents(schedule.value.events.values(), props.eventSlotFilter),
|
||||
...edgesFromShifts(schedule.value.shifts.values(), props.shiftSlotFilter),
|
||||
])
|
||||
});
|
||||
const stretches = computed(() => {
|
||||
if (schedule.value.deleted) {
|
||||
throw Error("Unhandled deleted schedule");
|
||||
}
|
||||
return [
|
||||
...stretchesFromSpans(
|
||||
spansFromJunctions(
|
||||
junctions.value,
|
||||
schedule.value.locations?.filter(l => !l.deleted) ?? [],
|
||||
schedule.value.roles?.filter(r => !r.deleted) ?? [],
|
||||
schedule.value.locations,
|
||||
schedule.value.roles,
|
||||
),
|
||||
oneHourMs * 5
|
||||
)
|
||||
|
@ -538,15 +534,12 @@ const timezone = computed({
|
|||
});
|
||||
|
||||
const elements = computed(() => {
|
||||
if (schedule.value.deleted) {
|
||||
throw Error("Unhandled deleted schedule");
|
||||
}
|
||||
return tableElementsFromStretches(
|
||||
stretches.value,
|
||||
schedule.value.events?.filter(e => !e.deleted) ?? [],
|
||||
schedule.value.locations?.filter(l => !l.deleted) ?? [],
|
||||
schedule.value.shifts?.filter(s => !s.deleted) ?? [],
|
||||
schedule.value.roles?.filter(r => !r.deleted) ?? [],
|
||||
schedule.value.events,
|
||||
schedule.value.locations,
|
||||
schedule.value.shifts,
|
||||
schedule.value.roles,
|
||||
accountStore.activeTimezone
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue