To determine if the entity received from the server is the same as the entity the client has the equals method is use. To avoid problems with clients that have incorrect clocks the timestamp is overriden with the server's time when the entities are saved on the server. This means that the entities received back from the server when the client saves will have different timestamps than what the client set. Ignore the updatedAt timestamp when comparing entities for equality so that the update logic correctly replaces entities that only differ by the update timestamp.
791 lines
19 KiB
TypeScript
791 lines
19 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, 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>[];
|
|
}
|
|
|
|
function entityMap<T extends { id: Id }>(entities: T[]) {
|
|
return new Map(entities.map(entity => [entity.id, entity]));
|
|
}
|
|
|
|
export function toIso(timestamp: DateTime) {
|
|
return timestamp.setZone(FixedOffsetZone.utcInstance).toISO();
|
|
}
|
|
|
|
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: 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,
|
|
this.slots.map(slot => slot.clone()),
|
|
);
|
|
}
|
|
|
|
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
|
|
&& arrayEquals(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,
|
|
api.slots.map(slot => ClientScheduleEventSlot.fromApi(slot, 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.map(slot => slot.toApi()),
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ClientScheduleEventSlot {
|
|
constructor(
|
|
public id: Id,
|
|
public start: DateTime,
|
|
public end: DateTime,
|
|
public locations: ClientScheduleLocation[],
|
|
public assigned: Set<Id>,
|
|
public interested: number,
|
|
) {
|
|
}
|
|
|
|
clone() {
|
|
return new ClientScheduleEventSlot(
|
|
this.id,
|
|
this.start,
|
|
this.end,
|
|
[...this.locations],
|
|
new Set(this.assigned),
|
|
this.interested,
|
|
);
|
|
}
|
|
|
|
equals(other: ClientScheduleEventSlot) {
|
|
return (
|
|
this.id === other.id
|
|
&& 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,
|
|
locations: Map<Id, ClientScheduleLocation>,
|
|
opts: { zone: Zone, locale: string }
|
|
) {
|
|
return new this(
|
|
api.id,
|
|
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 {
|
|
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: ClientScheduleShiftSlot[],
|
|
) {
|
|
super(id, updatedAt, deleted);
|
|
}
|
|
|
|
clone() {
|
|
return new ClientScheduleShift(
|
|
this.id,
|
|
this.updatedAt,
|
|
this.deleted,
|
|
this.role,
|
|
this.name,
|
|
this.description,
|
|
this.slots.map(slot => slot.clone()),
|
|
)
|
|
}
|
|
|
|
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
|
|
&& arrayEquals(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 ?? "",
|
|
api.slots.map(slot => ClientScheduleShiftSlot.fromApi(slot, 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.map(slot => slot.toApi()),
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ClientScheduleShiftSlot {
|
|
constructor(
|
|
public id: Id,
|
|
public start: DateTime,
|
|
public end: DateTime,
|
|
public assigned: Set<Id>,
|
|
) {
|
|
}
|
|
|
|
clone() {
|
|
return new ClientScheduleShiftSlot(
|
|
this.id,
|
|
this.start,
|
|
this.end,
|
|
new Set(this.assigned),
|
|
)
|
|
}
|
|
|
|
equals(other: ClientScheduleShiftSlot) {
|
|
return (
|
|
this.id === other.id
|
|
&& this.start.toMillis() === other.start.toMillis()
|
|
&& this.end.toMillis() === other.end.toMillis()
|
|
&& setEquals(this.assigned, other.assigned)
|
|
)
|
|
}
|
|
|
|
static fromApi(api: ApiScheduleShiftSlot, opts: { zone: Zone, locale: string }) {
|
|
return new this(
|
|
api.id,
|
|
DateTime.fromISO(api.start, opts),
|
|
DateTime.fromISO(api.end, opts),
|
|
new Set(api.assigned),
|
|
);
|
|
}
|
|
|
|
toApi(): ApiScheduleShiftSlot {
|
|
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>;
|
|
originalRoles: Map<Id, ClientScheduleRole>;
|
|
originalShifts: Map<Id, ClientScheduleShift>;
|
|
modified: boolean;
|
|
|
|
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);
|
|
this.modified = false;
|
|
}
|
|
|
|
equals(other: ClientSchedule): boolean {
|
|
throw new Error("ClientSchedule.equals not implemented")
|
|
}
|
|
|
|
private recalcModified() {
|
|
function mapEquals<K, V>(a: Map<K, V>, b: Map<K, V>) {
|
|
if (a.size !== b.size) {
|
|
return false;
|
|
}
|
|
for (const [key, value] of a) {
|
|
if (!b.has(key) || b.get(key) !== value) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
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) {
|
|
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) {
|
|
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) {
|
|
this.events.set(event.id, event);
|
|
this.modified = true;
|
|
}
|
|
|
|
editEvent(
|
|
event: ClientScheduleEvent,
|
|
edits: {
|
|
deleted?: boolean,
|
|
name?: string,
|
|
description?: string
|
|
},
|
|
) {
|
|
const copy = event.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.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();
|
|
}
|
|
|
|
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) {
|
|
this.shifts.set(shift.id, shift);
|
|
this.modified = true;
|
|
}
|
|
|
|
editShift(
|
|
shift: ClientScheduleShift,
|
|
edits: {
|
|
deleted?: boolean,
|
|
name?: string,
|
|
description?: string
|
|
},
|
|
) {
|
|
const copy = shift.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.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();
|
|
}
|
|
|
|
static fromApi(api: Living<ApiSchedule>, opts: { zone: Zone, locale: string }) {
|
|
const locations = entityMap(filterAlive(api.locations).map(location => ClientScheduleLocation.fromApi(location, opts)));
|
|
const roles = entityMap(filterAlive(api.roles).map(role => ClientScheduleRole.fromApi(role, opts)));
|
|
return new this(
|
|
api.id,
|
|
DateTime.fromISO(api.updatedAt, opts),
|
|
api.deleted ?? false,
|
|
locations,
|
|
entityMap(filterAlive(api.events).map(event => ClientScheduleEvent.fromApi(event, locations, opts))),
|
|
roles,
|
|
entityMap(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 = entityMap(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();
|
|
}
|
|
}
|