owltide/utils/client-schedule.ts

1002 lines
25 KiB
TypeScript
Raw Normal View History

/*
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { DateTime, FixedOffsetZone, Zone } from "~/shared/utils/luxon";
import type {
ApiEntity,
ApiSchedule,
ApiScheduleEvent,
ApiScheduleEventSlot,
ApiScheduleLocation,
ApiScheduleRole,
ApiScheduleShift,
ApiScheduleShiftSlot,
ApiTombstone
} from "~/shared/types/api";
import type { Id } from "~/shared/types/common";
import { setEquals } from "~/shared/utils/functions";
import { ClientEntity } from "~/utils/client-entity";
function filterEntity<T extends ApiEntity>(entities?: (T | ApiTombstone)[]) {
return (entities ?? []).filter((entity) => !entity.deleted) as T[];
}
function filterTombstone<T extends ApiEntity>(entities?: (T | ApiTombstone)[]) {
return (entities ?? []).filter((entity) => entity.deleted) as ApiTombstone[];
}
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 class ClientScheduleLocation extends ClientEntity<ApiScheduleLocation> {
serverName: string;
serverDescription: string;
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public description: string,
) {
super(id, updatedAt, deleted);
this.serverName = name;
this.serverDescription = description;
}
override isModified() {
return (
super.isModified()
|| this.name !== this.serverName
|| this.description !== this.serverDescription
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleLocation.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;
this.deleted = this.serverDeleted;
this.name = this.serverName;
this.description = this.serverDescription;
}
static create(
id: Id,
name: string,
description: string,
opts: { zone: Zone, locale: string },
) {
return new ClientScheduleLocation(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
name,
description,
);
}
static fromApi(api: ApiScheduleLocation, opts: { zone: Zone, locale: string }) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.description ?? "",
);
}
override apiUpdate(api: ApiScheduleLocation, opts: { zone: Zone, locale: string }) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.serverName = api.name;
this.serverDescription = api.description || "";
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleLocation | ApiTombstone {
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<ApiScheduleEvent> {
schedule!: ClientSchedule;
serverName: string;
serverCrew: boolean;
serverHost: string;
serverCancelled: boolean;
serverDescription: string;
serverInterested: number;
serverSlotIds: Set<Id>;
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 slotIds: Set<Id>,
) {
super(id, updatedAt, deleted);
this.serverName = name;
this.serverCrew = crew;
this.serverHost = host;
this.serverCancelled = cancelled;
this.serverDescription = description;
this.serverInterested = interested;
this.serverSlotIds = new Set(slotIds);
}
get slots(): ReadonlyMap<Id, ClientScheduleEventSlot> {
return new Map([...this.slotIds].map(id => [id, this.schedule.eventSlots.get(id)!]));
}
override isModified() {
return (
super.isModified()
|| this.name !== this.serverName
|| this.crew !== this.serverCrew
|| this.host !== this.serverHost
|| this.cancelled !== this.serverCancelled
|| this.description !== this.serverDescription
|| this.interested !== this.serverInterested
|| !setEquals(this.slotIds, this.serverSlotIds)
|| [...this.slotIds].some(id => this.schedule.eventSlots.get(id)!.isModified())
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleEvent.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;;
this.deleted = this.serverDeleted;;
this.name = this.serverName;
this.crew = this.serverCrew;
this.host = this.serverHost;
this.cancelled = this.serverCancelled;
this.description = this.serverDescription;
this.interested = this.serverInterested;
for (const id of this.serverSlotIds) {
this.schedule.eventSlots.get(id)!.discard();
}
for (const id of toRaw(this.slotIds).difference(this.serverSlotIds)) {
const slot = this.schedule.eventSlots.get(id)!;
slot.setEventId(slot.serverEventId);
}
}
static create(
schedule: ClientSchedule,
id: Id,
name: string,
crew: boolean,
host: string,
cancelled: boolean,
description: string,
interested: number,
slotIds: Set<Id>,
opts: { zone: Zone, locale: string },
) {
const event = new ClientScheduleEvent(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
name,
crew,
host,
cancelled,
description,
interested,
slotIds,
);
event.schedule = schedule;
return event;
}
static fromApi(
api: ApiScheduleEvent,
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,
new Set(api.slots.map(slot => slot.id)),
);
}
override apiUpdate(
api: ApiScheduleEvent,
opts: { zone: Zone, locale: string },
) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.serverName = api.name;
this.serverCrew = api.crew ?? false;
this.serverHost = api.host ?? "";
this.serverCancelled = api.cancelled ?? false;
this.serverDescription = api.description ?? "";
this.serverInterested = api.interested ?? 0;
this.serverSlotIds = new Set(api.slots.map(slot => slot.id));
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleEvent | ApiTombstone {
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 {
schedule!: ClientSchedule;
serverDeleted: boolean;
serverEventId: Id | undefined;
serverStart: DateTime;
serverEnd: DateTime;
serverLocationIds: Set<Id>;
serverAssigned: Set<Id>;
serverInterested: number;
constructor(
public id: Id,
public isNewEntity: boolean,
public deleted: boolean,
public eventId: Id | undefined,
public start: DateTime,
public end: DateTime,
public locationIds: Set<Id>,
public assigned: Set<Id>,
public interested: number,
) {
this.serverDeleted = deleted;
this.serverEventId = eventId;
this.serverStart = start;
this.serverEnd = end;
this.serverLocationIds = new Set(locationIds);
this.serverAssigned = new Set(assigned);
this.serverInterested = interested;
}
isModified() {
return (
this.isNewEntity
|| this.deleted
|| this.serverDeleted
|| this.eventId !== this.serverEventId
|| this.start.toMillis() !== this.serverStart.toMillis()
|| this.end.toMillis() !== this.serverEnd.toMillis()
|| !setEquals(this.locationIds, this.serverLocationIds)
|| !setEquals(this.assigned, this.serverAssigned)
|| this.interested !== this.serverInterested
);
}
setEventId(newEventId: Id | undefined) {
if (this.eventId === newEventId)
return;
this.schedule.events.get(newEventId!)?.slotIds.add(this.id);
this.schedule.events.get(this.eventId!)?.slotIds.delete(this.id);
this.eventId = newEventId;
}
discard() {
if (this.isNewEntity) {
throw new Error("ClientScheduleEventSlot.discard: Cannot discard new slot");
}
this.deleted = this.serverDeleted;
this.setEventId(this.serverEventId);
this.start = this.serverStart;
this.end = this.serverEnd;
this.locationIds = new Set(this.serverLocationIds);
this.assigned = new Set(this.serverAssigned);
this.interested = this.serverInterested;
}
static create(
schedule: ClientSchedule,
id: Id,
eventId: Id,
start: DateTime,
end: DateTime,
locationIds: Set<Id>,
assigned: Set<Id>,
interested: number,
) {
const slot = new ClientScheduleEventSlot(
id,
true,
false,
eventId,
start,
end,
locationIds,
assigned,
interested,
);
slot.schedule = schedule;
return slot;
}
static fromApi(
api: ApiScheduleEventSlot,
eventId: Id,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
false,
false,
eventId,
DateTime.fromISO(api.start, opts),
DateTime.fromISO(api.end, opts),
new Set(api.locationIds),
new Set(api.assigned),
api.interested ?? 0,
);
}
apiUpdate(
api: ApiScheduleEventSlot,
eventId: Id,
opts: { zone: Zone, locale: string }
) {
const wasModified = this.isModified();
this.isNewEntity = false;
this.serverDeleted = false;
this.serverEventId = eventId;
this.serverStart = DateTime.fromISO(api.start, opts);
this.serverEnd = DateTime.fromISO(api.end, opts);
this.serverLocationIds = new Set(api.locationIds);
this.serverAssigned = new Set(api.assigned);
this.serverInterested = api.interested ?? 0;
if (!wasModified || !this.isModified()) {
this.discard();
}
}
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.locationIds],
assigned: this.assigned.size ? [...this.assigned] : undefined,
interested: this.interested || undefined,
}
}
}
export class ClientScheduleRole extends ClientEntity<ApiScheduleRole> {
serverName: string;
serverDescription: string;
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public name: string,
public description: string,
) {
super(id, updatedAt, deleted);
this.serverName = name;
this.serverDescription = description;
}
override isModified() {
return (
super.isModified()
|| this.name !== this.serverName
|| this.description !== this.serverDescription
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleRole.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;
this.deleted = this.serverDeleted;
this.name = this.serverName;
this.description = this.serverDescription;
}
static create(
id: Id,
name: string,
description: string,
opts: { zone: Zone, locale: string },
) {
return new ClientScheduleRole(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
name,
description,
);
}
static fromApi(api: ApiScheduleRole, opts: { zone: Zone, locale: string }) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.name,
api.description ?? "",
);
}
override apiUpdate(api: ApiScheduleRole, opts: { zone: Zone, locale: string }) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.serverName = api.name;
this.serverDescription = api.description || "";
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleRole | ApiTombstone {
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<ApiScheduleShift> {
schedule!: ClientSchedule;
serverRoleId: Id | undefined;
serverName: string;
serverDescription: string;
serverSlotIds: Set<Id>;
constructor(
id: Id,
updatedAt: DateTime,
deleted: boolean,
public roleId: Id | undefined,
public name: string,
public description: string,
public slotIds: Set<Id>,
) {
super(id, updatedAt, deleted);
this.serverRoleId = roleId;
this.serverName = name;
this.serverDescription = description;
this.serverSlotIds = new Set(slotIds);
}
get slots(): ReadonlyMap<Id, ClientScheduleShiftSlot> {
return new Map([...this.slotIds].map(id => [id, this.schedule.shiftSlots.get(id)!]));
}
override isModified() {
return (
super.isModified()
|| this.roleId !== this.serverRoleId
|| this.name !== this.serverName
|| this.description !== this.serverDescription
|| !setEquals(this.slotIds, this.serverSlotIds)
|| [...this.slotIds].some(id => this.schedule.shiftSlots.get(id)!.isModified())
);
}
override discard() {
if (this.isNew()) {
throw new Error("ClientScheduleShift.discard: Cannot discard new entity.")
}
this.updatedAt = this.serverUpdatedAt;;
this.deleted = this.serverDeleted;;
this.roleId = this.serverRoleId;
this.name = this.serverName;
this.description = this.serverDescription;
for (const id of this.serverSlotIds) {
this.schedule.shiftSlots.get(id)!.discard();
}
for (const id of toRaw(this.slotIds).difference(this.serverSlotIds)) {
const slot = this.schedule.shiftSlots.get(id)!;
slot.setShiftId(slot.serverShiftId);
}
}
static create(
schedule: ClientSchedule,
id: Id,
roleId: Id | undefined,
name: string,
description: string,
slotIds: Set<Id>,
opts: { zone: Zone, locale: string },
) {
const shift = new ClientScheduleShift(
id,
DateTime.fromMillis(ClientEntity.newEntityMillis, opts),
false,
roleId,
name,
description,
slotIds,
);
shift.schedule = schedule;
return shift;
}
static fromApi(
api: ApiScheduleShift,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
api.roleId,
api.name,
api.description ?? "",
new Set(api.slots.map(slot => slot.id)),
);
}
override apiUpdate(
api: ApiScheduleShift,
opts: { zone: Zone, locale: string },
) {
const wasModified = this.isModified();
this.serverUpdatedAt = DateTime.fromISO(api.updatedAt, opts);
this.serverDeleted = false;
this.serverRoleId = api.roleId;
this.serverName = api.name;
this.serverDescription = api.description ?? "";
this.serverSlotIds = new Set(api.slots.map(slot => slot.id));
if (!wasModified || !this.isModified()) {
this.discard();
}
}
toApi(): ApiScheduleShift | ApiTombstone {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
roleId: this.roleId,
name: this.name,
description: this.description || undefined,
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
}
}
}
export class ClientScheduleShiftSlot {
schedule!: ClientSchedule;
serverDeleted: boolean;
serverShiftId: Id | undefined;
serverStart: DateTime;
serverEnd: DateTime;
serverAssigned: Set<Id>;
constructor(
public id: Id,
public isNewEntity: boolean,
public deleted: boolean,
public shiftId: Id | undefined,
public start: DateTime,
public end: DateTime,
public assigned: Set<Id>,
) {
this.serverDeleted = deleted;
this.serverShiftId = shiftId;
this.serverStart = start;
this.serverEnd = end;
this.serverAssigned = new Set(assigned);
}
isModified() {
return (
this.isNewEntity
|| this.deleted
|| this.serverDeleted
|| this.shiftId !== this.serverShiftId
|| this.start.toMillis() !== this.serverStart.toMillis()
|| this.end.toMillis() !== this.serverEnd.toMillis()
|| !setEquals(this.assigned, this.serverAssigned)
);
}
setShiftId(newShiftId: Id | undefined) {
if (this.shiftId === newShiftId)
return;
this.schedule.shifts.get(newShiftId!)?.slotIds.add(this.id);
this.schedule.shifts.get(this.shiftId!)?.slotIds.delete(this.id);
this.shiftId = newShiftId;
}
discard() {
if (this.isNewEntity) {
throw new Error("ClientScheduleShiftSlot.discard: Cannot discard new slot");
}
this.deleted = this.serverDeleted;
this.setShiftId(this.serverShiftId);
this.start = this.serverStart;
this.end = this.serverEnd;
this.assigned = new Set(this.serverAssigned);
}
static create(
schedule: ClientSchedule,
id: Id,
shiftId: Id,
start: DateTime,
end: DateTime,
assigned: Set<Id>,
) {
const slot = new ClientScheduleShiftSlot(
id,
true,
false,
shiftId,
start,
end,
assigned,
);
slot.schedule = schedule;
return slot;
}
static fromApi(
api: ApiScheduleShiftSlot,
shiftId: Id,
opts: { zone: Zone, locale: string },
) {
return new this(
api.id,
false,
false,
shiftId,
DateTime.fromISO(api.start, opts),
DateTime.fromISO(api.end, opts),
new Set(api.assigned),
);
}
apiUpdate(
api: ApiScheduleShiftSlot,
shiftId: Id,
opts: { zone: Zone, locale: string },
) {
const wasModified = this.isModified();
this.isNewEntity = false;
this.serverDeleted = false;
this.serverShiftId = shiftId;
this.serverStart = DateTime.fromISO(api.start, opts);
this.serverEnd = DateTime.fromISO(api.end, opts);
this.serverAssigned = new Set(api.assigned);
if (!wasModified || !this.isModified()) {
this.discard();
}
}
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<ApiSchedule> {
nextClientId = -1;
constructor(
id: 111,
updatedAt: DateTime,
deleted: boolean,
public locations: ClientMap<ClientScheduleLocation>,
public events: ClientMap<ClientScheduleEvent>,
public eventSlots: Map<Id, ClientScheduleEventSlot>,
public roles: ClientMap<ClientScheduleRole>,
public shifts: ClientMap<ClientScheduleShift>,
public shiftSlots: Map<Id, ClientScheduleShiftSlot>,
) {
super(id, updatedAt, deleted);
// XXX For now the prototype server is assigning client ids instead of remapping them.
this.nextClientId = Math.min(
0,
...locations.keys(),
...events.keys(),
...eventSlots.keys(),
...roles.keys(),
...shifts.keys(),
...shiftSlots.keys(),
) - 1;
}
discard() {
this.locations.discard();
this.events.discard();
this.roles.discard();
this.shifts.discard();
}
discardEventSlot(id: Id) {
const eventSlot = this.eventSlots.get(id);
if (!eventSlot) {
throw new Error("ClientSchedule.discardEventSlot: slot does not exist");
}
if (eventSlot.isNewEntity) {
this.eventSlots.delete(id);
if (eventSlot.eventId !== undefined)
this.events.get(eventSlot.eventId)?.slotIds.delete(id);
if (eventSlot.serverEventId !== undefined && eventSlot.serverEventId !== eventSlot.eventId)
this.events.get(eventSlot.serverEventId)?.schedule.eventSlots.delete(id);
} else {
eventSlot.discard();
}
}
discardShiftSlot(id: Id) {
const shiftSlot = this.shiftSlots.get(id);
if (!shiftSlot) {
throw new Error("ClientSchedule.discardShiftSlot: slot does not exist");
}
if (shiftSlot.isNewEntity) {
this.shiftSlots.delete(id);
if (shiftSlot.shiftId !== undefined)
this.shifts.get(shiftSlot.shiftId)?.slotIds.delete(id);
if (shiftSlot.serverShiftId !== undefined && shiftSlot.serverShiftId !== shiftSlot.shiftId)
this.shifts.get(shiftSlot.serverShiftId)?.schedule.shiftSlots.delete(id);
} else {
shiftSlot.discard();
}
}
override isModified() {
return (
super.isModified()
|| [...this.locations.values()].some(location => location.isModified())
|| [...this.events.values()].some(event => event.isModified())
|| [...this.roles.values()].some(role => role.isModified())
|| [...this.shifts.values()].some(shift => shift.isModified())
);
}
private checkLocationRefsForDeletion(id: Id) {
for (const event of this.events.values()) {
if (event.deleted)
continue;
for (const slot of event.slots.values()) {
if (slot.locationIds.has(id)) {
throw new Error(`Cannot delete location, event "${event.name}" depends on it`);
}
}
}
}
private checkRoleRefsForDeletion(id: Id) {
for (const shift of this.shifts.values()) {
if (shift.deleted) {
continue;
}
if (shift.roleId === id) {
throw new Error(`Cannot delete role, shift "${shift.name}" depends on it`);
}
}
}
static fromApi(api: ApiSchedule, opts: { zone: Zone, locale: string }) {
const eventSlots = idMap((api.events ?? [])
.filter(event => !event.deleted)
.flatMap(event => event.slots.map(slot => ClientScheduleEventSlot.fromApi(slot, event.id, opts)))
)
const shiftSlots = idMap((api.shifts ?? [])
.filter(shift => !shift.deleted)
.flatMap(shift => shift.slots.map(slot => ClientScheduleShiftSlot.fromApi(slot, shift.id, opts)))
)
const schedule = new this(
api.id,
DateTime.fromISO(api.updatedAt, opts),
api.deleted ?? false,
ClientMap.fromApi(ClientScheduleLocation, api.locations ?? [], opts),
ClientMap.fromApi(ClientScheduleEvent, api.events ?? [], opts),
eventSlots,
ClientMap.fromApi(ClientScheduleRole, api.roles ?? [], opts),
ClientMap.fromApi(ClientScheduleShift, api.shifts ?? [], opts),
shiftSlots,
);
for (const event of schedule.events.values()) {
event.schedule = schedule;
}
for (const eventSlot of schedule.eventSlots.values()) {
eventSlot.schedule = schedule;
}
for (const shift of schedule.shifts.values()) {
shift.schedule = schedule;
}
for (const shiftSlot of schedule.shiftSlots.values()) {
shiftSlot.schedule = schedule;
}
return schedule;
}
toApi(diff = false): ApiSchedule | ApiTombstone {
if (this.deleted) {
return {
id: this.id,
updatedAt: toIso(this.updatedAt),
deleted: true,
}
}
return {
id: this.id as 111,
updatedAt: toIso(this.updatedAt),
locations: this.locations.size ? this.locations.toApi(diff) : undefined,
events: this.events.size ? this.events.toApi(diff) : undefined,
roles: this.roles.size ? this.roles.toApi(diff) : undefined,
shifts: this.shifts.size ? this.shifts.toApi(diff) : undefined,
}
}
override apiUpdate(update: ApiSchedule, opts: { zone: Zone, locale: string }) {
if (update.deleted)
throw new Error("ClientSchedule.apiUpdate: Unexpected deletion");
if (update.id !== this.id)
throw new Error("ClientSchedule.apiUpdate: id mismatch");
this.updatedAt = DateTime.fromISO(update.updatedAt, opts);
if (update.locations)
this.locations.apiUpdate(update.locations, opts);
if (update.events) {
for (const apiEvent of update.events) {
if (!apiEvent.deleted) {
for (const apiSlot of apiEvent.slots) {
let slot = this.eventSlots.get(apiSlot.id);
if (slot) {
slot.apiUpdate(apiSlot, apiEvent.id, opts);
} else {
slot = ClientScheduleEventSlot.fromApi(apiSlot, apiEvent.id, opts);
slot.schedule = this;
this.eventSlots.set(slot.id, slot);
}
}
}
}
this.events.apiUpdate(update.events, opts);
const serverSlotIds = new Set([...this.events.values()].flatMap(event => [...event.serverSlotIds]));
for (const event of this.events.values()) {
for (const slotId of event.slotIds) {
if (!serverSlotIds.has(slotId)) {
const slot = this.eventSlots.get(slotId)!;
if (!slot.isNewEntity) {
if (slot.deleted) {
event.slotIds.delete(slotId);
this.eventSlots.delete(slotId);
} else {
slot.serverDeleted = true;
}
}
}
}
}
}
if (update.roles)
this.roles.apiUpdate(update.roles, opts);
if (update.shifts) {
for (const apiShift of update.shifts) {
if (!apiShift.deleted) {
for (const apiSlot of apiShift.slots) {
let slot = this.shiftSlots.get(apiSlot.id);
if (slot) {
slot.apiUpdate(apiSlot, apiShift.id, opts);
} else {
slot = ClientScheduleShiftSlot.fromApi(apiSlot, apiShift.id, opts);
slot.schedule = this;
this.shiftSlots.set(slot.id, slot);
}
}
}
}
this.shifts.apiUpdate(update.shifts, opts);
const serverSlotIds = new Set([...this.shifts.values()].flatMap(shift => [...shift.serverSlotIds]));
for (const shift of this.shifts.values()) {
for (const slotId of shift.slotIds) {
if (!serverSlotIds.has(slotId)) {
const slot = this.shiftSlots.get(slotId)!;
if (!slot.isNewEntity) {
if (slot.deleted) {
shift.slotIds.delete(slotId);
this.shiftSlots.delete(slotId);
} else {
slot.serverDeleted = true;
}
}
}
}
}
}
}
}