owltide/utils/client-schedule.ts
Hornwitser bb450fd583 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.
2025-06-14 19:22:53 +02:00

965 lines
24 KiB
TypeScript

import { DateTime, FixedOffsetZone, Zone } from "~/shared/utils/luxon";
import type {
ApiSchedule,
ApiScheduleEvent,
ApiScheduleEventSlot,
ApiScheduleLocation,
ApiScheduleRole,
ApiScheduleShift,
ApiScheduleShiftSlot
} from "~/shared/types/api";
import type { Entity, Id, Living, Tombstone } from "~/shared/types/common";
import { arrayEquals, mapEquals, setEquals } from "~/shared/utils/functions";
function filterAlive<T extends Entity>(entities?: T[]) {
return (entities ?? []).filter((entity) => !entity.deleted) as Living<T>[];
}
function filterTombstone<T extends Entity>(entities?: T[]) {
return (entities ?? []).filter((entity) => entity.deleted) as Tombstone<T>[];
}
export function toIso(timestamp: DateTime) {
return timestamp.setZone(FixedOffsetZone.utcInstance).toISO();
}
function mapWith<K, V>(map: Map<K, V>, key: K, value: V) {
const copy = new Map(map);
copy.set(key, value);
return copy;
}
function mapWithout<K, V>(map: Map<K, V>, key: K) {
const copy = new Map(map);
copy.delete(key);
return copy;
}
export abstract class ClientEntity {
constructor(
public id: Id,
public updatedAt: DateTime,
public deleted: boolean,
) {
}
abstract equals(other: this): boolean;
}
export class ClientScheduleLocation extends ClientEntity {
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public description: string,
) {
super(id, updatedAt, deleted);
}
clone() {
return new ClientScheduleLocation(
this.id,
this.updatedAt,
this.deleted,
this.name,
this.description,
);
}
equals(other: ClientScheduleLocation) {
return (
this.id === other.id
&& this.deleted === other.deleted
&& this.name === other.name
&& this.description === other.description
)
}
static fromApi(api: Living<ApiScheduleLocation>, opts: { zone: Zone, locale: string }) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.description ?? "",
);
}
toApi(): ApiScheduleLocation {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
name: this.name,
description: this.description || undefined,
}
}
}
export class ClientScheduleEvent extends ClientEntity {
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public crew: boolean,
public host: string,
public cancelled: boolean,
public description: string,
public interested: number,
public slots: Map<Id, ClientScheduleEventSlot>,
) {
super(id, updatedAt, deleted);
}
clone() {
return new ClientScheduleEvent(
this.id,
this.updatedAt,
this.deleted,
this.name,
this.crew,
this.host,
this.cancelled,
this.description,
this.interested,
new Map(this.slots),
);
}
equals(other: ClientScheduleEvent) {
return (
this.id === other.id
&& this.deleted === other.deleted
&& this.name === other.name
&& this.crew === other.crew
&& this.host === other.host
&& this.cancelled === other.cancelled
&& this.description === other.description
&& this.interested === other.interested
&& mapEquals(this.slots, other.slots, (a, b) => a.equals(b))
)
}
static fromApi(
api: Living<ApiScheduleEvent>,
locations: Map<Id, ClientScheduleLocation>,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.crew ?? false,
api.host ?? "",
api.cancelled ?? false,
api.description ?? "",
api.interested ?? 0,
idMap(api.slots.map(slot => ClientScheduleEventSlot.fromApi(slot, api.id, locations, opts))),
);
}
toApi(): ApiScheduleEvent {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
name: this.name,
crew: this.crew || undefined,
host: this.host || undefined,
cancelled: this.cancelled || undefined,
description: this.description || undefined,
interested: this.interested || undefined,
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
}
}
}
export class ClientScheduleEventSlot {
constructor(
public id: Id,
public deleted: boolean,
public eventId: Id,
public start: DateTime,
public end: DateTime,
public locations: ClientScheduleLocation[],
public assigned: Set<Id>,
public interested: number,
) {
}
clone() {
return new ClientScheduleEventSlot(
this.id,
this.deleted,
this.eventId,
this.start,
this.end,
[...this.locations],
new Set(this.assigned),
this.interested,
);
}
equals(other: ClientScheduleEventSlot) {
return (
this.id === other.id
&& this.deleted === other.deleted
&& this.eventId === other.eventId
&& this.start.toMillis() === other.start.toMillis()
&& this.end.toMillis() === other.end.toMillis()
&& arrayEquals(this.locations, other.locations)
&& setEquals(this.assigned, other.assigned)
&& this.interested === other.interested
)
}
static fromApi(
api: ApiScheduleEventSlot,
eventId: Id,
locations: Map<Id, ClientScheduleLocation>,
opts: { zone: Zone, locale: string }
) {
return new this(
api.id,
false,
eventId,
DateTime.fromISO(api.start, opts),
DateTime.fromISO(api.end, opts),
api.locationIds.map(id => locations.get(id)!),
new Set(api.assigned),
api.interested ?? 0,
);
}
toApi(): ApiScheduleEventSlot {
if (this.deleted) {
throw new Error("ClientScheduleEventSlot.toApi: Unexpected deleted slot")
}
return {
id: this.id,
start: toIso(this.start),
end: toIso(this.end),
locationIds: this.locations.map(location => location.id),
assigned: this.assigned.size ? [...this.assigned] : undefined,
interested: this.interested || undefined,
}
}
}
export class ClientScheduleRole extends ClientEntity {
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public description: string,
) {
super(id, updatedAt, deleted);
}
clone() {
return new ClientScheduleRole(
this.id,
this.updatedAt,
this.deleted,
this.name,
this.description,
);
}
equals(other: ClientScheduleRole) {
return (
this.id === other.id
&& this.deleted === other.deleted
&& this.name === other.name
&& this.description === other.description
)
}
static fromApi(api: Living<ApiScheduleRole>, opts: { zone: Zone, locale: string }) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.description ?? "",
);
}
toApi(): ApiScheduleRole {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
name: this.name,
description: this.description || undefined,
}
}
}
export class ClientScheduleShift extends ClientEntity {
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public role: ClientScheduleRole,
public name: string,
public description: string,
public slots: Map<Id, ClientScheduleShiftSlot>,
) {
super(id, updatedAt, deleted);
}
clone() {
return new ClientScheduleShift(
this.id,
this.updatedAt,
this.deleted,
this.role,
this.name,
this.description,
new Map(this.slots),
)
}
equals(other: ClientScheduleShift) {
return (
this.id === other.id
&& this.deleted === other.deleted
&& this.role.id === other.role.id
&& this.name === other.name
&& this.description === other.description
&& mapEquals(this.slots, other.slots, (a, b) => a.equals(b))
)
}
static fromApi(
api: Living<ApiScheduleShift>,
roles: Map<Id, ClientScheduleRole>,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
roles.get(api.roleId)!,
api.name,
api.description ?? "",
idMap(api.slots.map(slot => ClientScheduleShiftSlot.fromApi(slot, api.id, opts))),
);
}
toApi(): ApiScheduleShift {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
roleId: this.role.id,
name: this.name,
description: this.description || undefined,
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
}
}
}
export class ClientScheduleShiftSlot {
constructor(
public id: Id,
public deleted: boolean,
public shiftId: Id,
public start: DateTime,
public end: DateTime,
public assigned: Set<Id>,
) {
}
clone() {
return new ClientScheduleShiftSlot(
this.id,
this.deleted,
this.shiftId,
this.start,
this.end,
new Set(this.assigned),
)
}
equals(other: ClientScheduleShiftSlot) {
return (
this.id === other.id
&& this.deleted === other.deleted
&& this.shiftId === other.shiftId
&& this.start.toMillis() === other.start.toMillis()
&& this.end.toMillis() === other.end.toMillis()
&& setEquals(this.assigned, other.assigned)
)
}
static fromApi(api: ApiScheduleShiftSlot, shiftId: Id, opts: { zone: Zone, locale: string }) {
return new this(
api.id,
false,
shiftId,
DateTime.fromISO(api.start, opts),
DateTime.fromISO(api.end, opts),
new Set(api.assigned),
);
}
toApi(): ApiScheduleShiftSlot {
if (this.deleted) {
throw new Error("ClientScheduleShiftSlot.toApi: Unexpected deleted slot")
}
return {
id: this.id,
start: toIso(this.start),
end: toIso(this.end),
assigned: this.assigned.size ? [...this.assigned] : undefined,
}
}
}
export class ClientSchedule extends ClientEntity {
originalLocations: Map<Id, ClientScheduleLocation>;
originalEvents: Map<Id, ClientScheduleEvent>;
originalEventSlots: Map<Id, ClientScheduleEventSlot>;
originalRoles: Map<Id, ClientScheduleRole>;
originalShifts: Map<Id, ClientScheduleShift>;
originalShiftSlots: Map<Id, ClientScheduleShiftSlot>;
shiftSlots: Map<Id, ClientScheduleShiftSlot>;
eventSlots: Map<Id, ClientScheduleEventSlot>;
modified: boolean;
nextClientId = -1;
constructor(
id: 111,
updatedAt: DateTime,
deleted: boolean,
public locations: Map<Id, ClientScheduleLocation>,
public events: Map<Id, ClientScheduleEvent>,
public roles: Map<Id, ClientScheduleRole>,
public shifts: Map<Id, ClientScheduleShift>,
) {
super(id, updatedAt, deleted);
this.originalLocations = new Map(locations);
this.originalEvents = new Map(events);
this.originalRoles = new Map(roles);
this.originalShifts = new Map(shifts);
// Slot tracking
this.eventSlots = idMap([...events.values()].flatMap(event => [...event.slots.values()]));
this.originalEventSlots = new Map(this.eventSlots);
this.shiftSlots = idMap([...shifts.values()].flatMap(shift => [...shift.slots.values()]));
this.originalShiftSlots = new Map(this.shiftSlots);
this.modified = false;
// XXX For now the prototype server is assigning client ids instead of remapping them.
this.nextClientId = Math.min(
0,
...locations.keys(),
...events.keys(),
...roles.keys(),
...shifts.keys(),
) - 1;
}
equals(other: ClientSchedule): boolean {
throw new Error("ClientSchedule.equals not implemented")
}
private recalcModified() {
this.modified = (
!mapEquals(this.locations, this.originalLocations)
|| !mapEquals(this.events, this.originalEvents)
|| !mapEquals(this.roles, this.originalRoles)
|| !mapEquals(this.shifts, this.originalShifts)
);
}
private fixLocationRefs(locations: Map<Id, ClientScheduleLocation>) {
for (const events of [this.events, this.originalEvents]) {
for (const event of events.values()) {
for (const slot of event.slots.values()) {
for (let i = 0; i < slot.locations.length; i++) {
const location = locations.get(slot.locations[i].id);
if (location && slot.locations[i] !== location) {
slot.locations[i] = location;
}
}
}
}
}
}
private checkLocationRefsForDeletion(id: Id) {
for (const event of this.events.values()) {
if (event.deleted)
continue;
for (const slot of event.slots.values()) {
for (let i = 0; i < slot.locations.length; i++) {
if (slot.locations[i].id === id) {
throw new Error(`Cannot delete location, event "${event.name}" depends on it`);
}
}
}
}
}
isModifiedLocation(id: Id) {
return this.originalLocations.get(id) !== this.locations.get(id);
}
setLocation(location: ClientScheduleLocation) {
if (location.deleted) {
this.checkLocationRefsForDeletion(location.id);
}
this.locations.set(location.id, location);
if (!location.deleted) {
this.fixLocationRefs(new Map([[location.id, location]]));
}
this.modified = true;
}
editLocation(
location: ClientScheduleLocation,
edits: {
deleted?: boolean,
name?: string,
description?: string
},
) {
const copy = location.clone();
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
if (edits.name !== undefined) copy.name = edits.name;
if (edits.description !== undefined) copy.description = edits.description;
this.setLocation(copy);
}
restoreLocation(id: Id) {
const location = this.originalLocations.get(id);
if (location) {
this.locations.set(id, location);
this.fixLocationRefs(new Map([[location.id, location]]));
} else {
this.checkLocationRefsForDeletion(id);
this.locations.delete(id);
}
this.recalcModified();
}
isModifiedEvent(id: Id) {
return this.originalEvents.get(id) !== this.events.get(id);
}
setEvent(event: ClientScheduleEvent) {
const previous = this.events.get(event.id);
if (previous) {
for (const id of previous.slots.keys()) {
if (!event.slots.has(id)) {
this.eventSlots.delete(id);
}
}
for (const [id, slot] of event.slots) {
this.eventSlots.set(id, slot);
}
}
this.events.set(event.id, event);
this.modified = true;
}
editEvent(
event: ClientScheduleEvent,
edits: {
deleted?: boolean,
name?: string,
crew?: boolean,
description?: string,
slots?: Map<Id, ClientScheduleEventSlot>,
},
) {
const copy = event.clone();
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
if (edits.name !== undefined) copy.name = edits.name;
if (edits.crew !== undefined) copy.crew = edits.crew;
if (edits.description !== undefined) copy.description = edits.description;
if (edits.slots !== undefined) copy.slots = edits.slots;
this.setEvent(copy);
}
restoreEvent(id: Id) {
const event = this.originalEvents.get(id);
if (event) {
this.events.set(id, event);
} else {
this.events.delete(id);
}
this.recalcModified();
}
isModifiedEventSlot(id: Id) {
return this.originalEventSlots.get(id) !== this.eventSlots.get(id);
}
setEventSlot(eventSlot: ClientScheduleEventSlot) {
const currentEvent = this.events.get(eventSlot.eventId);
if (!currentEvent) {
throw new Error(`Event with id ${eventSlot.eventId} does not exist`);
}
const previous = this.eventSlots.get(eventSlot.id);
if (previous && previous.eventId !== eventSlot.eventId) {
const previousEvent = this.events.get(previous.eventId)!;
this.editEvent(previousEvent, {
slots: mapWithout(previousEvent.slots, eventSlot.id),
});
}
this.editEvent(currentEvent, {
slots: mapWith(currentEvent.slots, eventSlot.id, eventSlot),
});
this.modified = true;
}
editEventSlot(
event: ClientScheduleEventSlot,
edits: {
deleted?: boolean,
eventId?: Id,
start?: DateTime,
end?: DateTime,
locations?: ClientScheduleLocation[],
assigned?: Set<Id>,
},
) {
const copy = event.clone();
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
if (edits.eventId !== undefined) copy.eventId = edits.eventId;
if (edits.start !== undefined) copy.start = edits.start;
if (edits.end !== undefined) copy.end = edits.end;
if (edits.locations !== undefined) copy.locations = edits.locations;
if (edits.assigned !== undefined) copy.assigned = edits.assigned;
this.setEventSlot(copy);
}
restoreEventSlot(id: Id) {
const originalSlot = this.originalEventSlots.get(id);
if (!originalSlot) {
this.eventSlots.delete(id);
} else {
this.setEventSlot(originalSlot);
}
this.recalcModified();
}
private fixRoleRefs(roles: Map<Id, ClientScheduleRole>) {
for (const shifts of [this.shifts, this.originalShifts]) {
for (const shift of shifts.values()) {
const role = roles.get(shift.role.id);
if (role && shift.role !== role) {
shift.role = role;
}
}
}
}
private checkRoleRefsForDeletion(id: Id) {
for (const shift of this.shifts.values()) {
if (shift.deleted) {
continue;
}
if (shift.role.id === id) {
throw new Error(`Cannot delete role, shift "${shift.name}" depends on it`);
}
}
}
isModifiedRole(id: Id) {
return this.originalRoles.get(id) !== this.roles.get(id);
}
setRole(role: ClientScheduleRole) {
if (role.deleted) {
this.checkRoleRefsForDeletion(role.id);
}
this.roles.set(role.id, role);
if (!role.deleted) {
this.fixRoleRefs(new Map([[role.id, role]]));
}
this.modified = true;
}
editRole(
role: ClientScheduleRole,
edits: {
deleted?: boolean,
name?: string,
description?: string
},
) {
const copy = role.clone();
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
if (edits.name !== undefined) copy.name = edits.name;
if (edits.description !== undefined) copy.description = edits.description;
this.setRole(copy);
}
restoreRole(id: Id) {
const role = this.originalRoles.get(id);
if (role) {
this.roles.set(id, role);
this.fixRoleRefs(new Map([[role.id, role]]));
} else {
this.checkRoleRefsForDeletion(id);
this.roles.delete(id);
}
this.recalcModified();
}
isModifiedShift(id: Id) {
return this.originalShifts.get(id) !== this.shifts.get(id);
}
setShift(shift: ClientScheduleShift) {
const previous = this.shifts.get(shift.id);
if (previous) {
for (const id of previous.slots.keys()) {
if (!shift.slots.has(id)) {
this.shiftSlots.delete(id);
}
}
for (const [id, slot] of shift.slots) {
this.shiftSlots.set(id, slot);
}
}
this.shifts.set(shift.id, shift);
this.modified = true;
}
editShift(
shift: ClientScheduleShift,
edits: {
deleted?: boolean,
role?: ClientScheduleRole,
name?: string,
description?: string
slots?: Map<Id, ClientScheduleShiftSlot>,
},
) {
const copy = shift.clone();
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
if (edits.role !== undefined) copy.role = edits.role;
if (edits.name !== undefined) copy.name = edits.name;
if (edits.description !== undefined) copy.description = edits.description;
if (edits.slots !== undefined) copy.slots = edits.slots;
this.setShift(copy);
}
restoreShift(id: Id) {
const shift = this.originalShifts.get(id);
if (shift) {
this.shifts.set(id, shift);
} else {
this.shifts.delete(id);
}
this.recalcModified();
}
isModifiedShiftSlot(id: Id) {
return this.originalShiftSlots.get(id) !== this.shiftSlots.get(id);
}
setShiftSlot(shiftSlot: ClientScheduleShiftSlot) {
const currentShift = this.shifts.get(shiftSlot.shiftId);
if (!currentShift) {
throw new Error(`Shift with id ${shiftSlot.shiftId} does not exist`);
}
const previous = this.shiftSlots.get(shiftSlot.id);
if (previous && previous.shiftId !== shiftSlot.shiftId) {
const previousShift = this.shifts.get(previous.shiftId)!;
this.editShift(previousShift, {
slots: mapWithout(previousShift.slots, shiftSlot.id),
});
}
this.editShift(currentShift, {
slots: mapWith(currentShift.slots, shiftSlot.id, shiftSlot),
});
this.modified = true;
}
editShiftSlot(
shift: ClientScheduleShiftSlot,
edits: {
deleted?: boolean,
shiftId?: Id,
start?: DateTime,
end?: DateTime,
assigned?: Set<Id>,
},
) {
const copy = shift.clone();
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
if (edits.shiftId !== undefined) copy.shiftId = edits.shiftId;
if (edits.start !== undefined) copy.start = edits.start;
if (edits.end !== undefined) copy.end = edits.end;
if (edits.assigned !== undefined) copy.assigned = edits.assigned;
this.setShiftSlot(copy);
}
restoreShiftSlot(id: Id) {
const originalSlot = this.originalShiftSlots.get(id);
if (!originalSlot) {
this.shiftSlots.delete(id);
} else {
this.setShiftSlot(originalSlot);
}
this.recalcModified();
}
static fromApi(api: Living<ApiSchedule>, opts: { zone: Zone, locale: string }) {
const locations = idMap(filterAlive(api.locations).map(location => ClientScheduleLocation.fromApi(location, opts)));
const roles = idMap(filterAlive(api.roles).map(role => ClientScheduleRole.fromApi(role, opts)));
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
locations,
idMap(filterAlive(api.events).map(event => ClientScheduleEvent.fromApi(event, locations, opts))),
roles,
idMap(filterAlive(api.shifts).map(shift => ClientScheduleShift.fromApi(shift, roles, opts))),
);
}
toApi(diff: boolean): ApiSchedule {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
if (!diff) {
return {
id: this.id as 111,
updatedAt: toIso(this.updatedAt),
locations: this.locations.size ? [...this.locations.values()].map(location => location.toApi()) : undefined,
events: this.events.size ? [...this.events.values()].map(event => event.toApi()) : undefined,
roles: this.roles.size ? [...this.roles.values()].map(role => role.toApi()) : undefined,
shifts: this.shifts.size ? [...this.shifts.values()].map(shift => shift.toApi()) : undefined,
}
}
const locations: ApiScheduleLocation[] = [];
for (const [id, location] of this.locations)
if (location !== this.originalLocations.get(id)) locations.push(location.toApi());
const events: ApiScheduleEvent[] = [];
for (const [id, event] of this.events)
if (event !== this.originalEvents.get(id)) events.push(event.toApi());
const roles: ApiScheduleRole[] = [];
for (const [id, role] of this.roles)
if (role !== this.originalRoles.get(id)) roles.push(role.toApi());
const shifts: ApiScheduleShift[] = [];
for (const [id, shift] of this.shifts)
if (shift !== this.originalShifts.get(id)) shifts.push(shift.toApi());
return {
id: this.id as 111,
updatedAt: toIso(this.updatedAt),
locations: locations.length ? locations : undefined,
events: events.length ? events : undefined,
roles: roles.length ? roles : undefined,
shifts: shifts.length ? shifts : undefined,
}
}
applyUpdate(update: ApiSchedule, opts: { zone: Zone, locale: string }) {
if (update.deleted)
throw new Error("ClientSchedule.applyUpdate: Unexpected deletion");
if (update.id !== this.id)
throw new Error("ClientSchedule.applyUpdate: id mismatch");
this.updatedAt = DateTime.fromISO(update.updatedAt, opts);
function applyEntityUpdates<T extends Entity, U extends ClientEntity>(
entityUpdates: T[] | undefined,
fromApi: (api: Living<T>) => U,
originalEntities: Map<Id, U>,
entities: Map<Id, U>,
) {
if (!entityUpdates)
return new Map();
const setEntites = idMap(filterAlive(entityUpdates).map(entity => fromApi(entity)));
for (const [id, updatedLocation] of setEntites) {
const modifiedLocation = entities.get(id);
if (
originalEntities.get(id) === modifiedLocation
|| modifiedLocation?.equals(updatedLocation)
) {
entities.set(id, updatedLocation);
}
originalEntities.set(id, updatedLocation);
}
const deletedLocations = filterTombstone(entityUpdates).map(location => location.id);
for (const id of deletedLocations) {
const modifiedLocation = entities.get(id);
if (
originalEntities.get(id) === modifiedLocation
|| entities.get(id)?.deleted
) {
entities.delete(id);
}
originalEntities.delete(id);
}
return setEntites;
}
const setLocations = applyEntityUpdates(
update.locations,
api => ClientScheduleLocation.fromApi(api, opts),
this.originalLocations,
this.locations,
);
this.fixLocationRefs(setLocations);
applyEntityUpdates(
update.events,
api => ClientScheduleEvent.fromApi(api, this.locations, opts),
this.originalEvents,
this.events,
);
applyEntityUpdates(
update.roles,
api => ClientScheduleRole.fromApi(api, opts),
this.originalRoles,
this.roles,
);
applyEntityUpdates(
update.shifts,
api => ClientScheduleShift.fromApi(api, this.roles, opts),
this.originalShifts,
this.shifts,
);
this.recalcModified();
}
}