2025-06-12 21:45:34 +02:00
|
|
|
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";
|
2025-06-14 19:12:31 +02:00
|
|
|
import { arrayEquals, mapEquals, setEquals } from "~/shared/utils/functions";
|
2025-06-12 21:45:34 +02:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2025-06-14 19:12:31 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-06-12 21:45:34 +02:00
|
|
|
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,
|
2025-06-14 19:12:31 +02:00
|
|
|
public slots: Map<Id, ClientScheduleEventSlot>,
|
2025-06-12 21:45:34 +02:00
|
|
|
) {
|
|
|
|
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,
|
2025-06-14 19:12:31 +02:00
|
|
|
new Map(this.slots),
|
2025-06-12 21:45:34 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2025-06-14 19:12:31 +02:00
|
|
|
&& mapEquals(this.slots, other.slots, (a, b) => a.equals(b))
|
2025-06-12 21:45:34 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2025-06-14 19:12:31 +02:00
|
|
|
idMap(api.slots.map(slot => ClientScheduleEventSlot.fromApi(slot, api.id, locations, opts))),
|
2025-06-12 21:45:34 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2025-06-14 19:12:31 +02:00
|
|
|
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ClientScheduleEventSlot {
|
|
|
|
constructor(
|
|
|
|
public id: Id,
|
2025-06-14 19:12:31 +02:00
|
|
|
public deleted: boolean,
|
|
|
|
public eventId: Id,
|
2025-06-12 21:45:34 +02:00
|
|
|
public start: DateTime,
|
|
|
|
public end: DateTime,
|
|
|
|
public locations: ClientScheduleLocation[],
|
|
|
|
public assigned: Set<Id>,
|
|
|
|
public interested: number,
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
clone() {
|
|
|
|
return new ClientScheduleEventSlot(
|
|
|
|
this.id,
|
2025-06-14 19:12:31 +02:00
|
|
|
this.deleted,
|
|
|
|
this.eventId,
|
2025-06-12 21:45:34 +02:00
|
|
|
this.start,
|
|
|
|
this.end,
|
|
|
|
[...this.locations],
|
|
|
|
new Set(this.assigned),
|
|
|
|
this.interested,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
equals(other: ClientScheduleEventSlot) {
|
|
|
|
return (
|
|
|
|
this.id === other.id
|
2025-06-14 19:12:31 +02:00
|
|
|
&& this.deleted === other.deleted
|
|
|
|
&& this.eventId === other.eventId
|
2025-06-12 21:45:34 +02:00
|
|
|
&& 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,
|
2025-06-14 19:12:31 +02:00
|
|
|
eventId: Id,
|
2025-06-12 21:45:34 +02:00
|
|
|
locations: Map<Id, ClientScheduleLocation>,
|
|
|
|
opts: { zone: Zone, locale: string }
|
|
|
|
) {
|
|
|
|
return new this(
|
|
|
|
api.id,
|
2025-06-14 19:12:31 +02:00
|
|
|
false,
|
|
|
|
eventId,
|
2025-06-12 21:45:34 +02:00
|
|
|
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 {
|
2025-06-14 19:12:31 +02:00
|
|
|
if (this.deleted) {
|
|
|
|
throw new Error("ClientScheduleEventSlot.toApi: Unexpected deleted slot")
|
|
|
|
}
|
2025-06-12 21:45:34 +02:00
|
|
|
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,
|
2025-06-14 19:12:31 +02:00
|
|
|
public slots: Map<Id, ClientScheduleShiftSlot>,
|
2025-06-12 21:45:34 +02:00
|
|
|
) {
|
|
|
|
super(id, updatedAt, deleted);
|
|
|
|
}
|
|
|
|
|
|
|
|
clone() {
|
|
|
|
return new ClientScheduleShift(
|
|
|
|
this.id,
|
|
|
|
this.updatedAt,
|
|
|
|
this.deleted,
|
|
|
|
this.role,
|
|
|
|
this.name,
|
|
|
|
this.description,
|
2025-06-14 19:12:31 +02:00
|
|
|
new Map(this.slots),
|
2025-06-12 21:45:34 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2025-06-14 19:12:31 +02:00
|
|
|
&& mapEquals(this.slots, other.slots, (a, b) => a.equals(b))
|
2025-06-12 21:45:34 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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 ?? "",
|
2025-06-14 19:12:31 +02:00
|
|
|
idMap(api.slots.map(slot => ClientScheduleShiftSlot.fromApi(slot, api.id, opts))),
|
2025-06-12 21:45:34 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2025-06-14 19:12:31 +02:00
|
|
|
slots: [...this.slots.values()].filter(slot => !slot.deleted).map(slot => slot.toApi()),
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ClientScheduleShiftSlot {
|
|
|
|
constructor(
|
|
|
|
public id: Id,
|
2025-06-14 19:12:31 +02:00
|
|
|
public deleted: boolean,
|
|
|
|
public shiftId: Id,
|
2025-06-12 21:45:34 +02:00
|
|
|
public start: DateTime,
|
|
|
|
public end: DateTime,
|
|
|
|
public assigned: Set<Id>,
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
clone() {
|
|
|
|
return new ClientScheduleShiftSlot(
|
|
|
|
this.id,
|
2025-06-14 19:12:31 +02:00
|
|
|
this.deleted,
|
|
|
|
this.shiftId,
|
2025-06-12 21:45:34 +02:00
|
|
|
this.start,
|
|
|
|
this.end,
|
|
|
|
new Set(this.assigned),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
equals(other: ClientScheduleShiftSlot) {
|
|
|
|
return (
|
|
|
|
this.id === other.id
|
2025-06-14 19:12:31 +02:00
|
|
|
&& this.deleted === other.deleted
|
|
|
|
&& this.shiftId === other.shiftId
|
2025-06-12 21:45:34 +02:00
|
|
|
&& this.start.toMillis() === other.start.toMillis()
|
|
|
|
&& this.end.toMillis() === other.end.toMillis()
|
|
|
|
&& setEquals(this.assigned, other.assigned)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2025-06-14 19:12:31 +02:00
|
|
|
static fromApi(api: ApiScheduleShiftSlot, shiftId: Id, opts: { zone: Zone, locale: string }) {
|
2025-06-12 21:45:34 +02:00
|
|
|
return new this(
|
|
|
|
api.id,
|
2025-06-14 19:12:31 +02:00
|
|
|
false,
|
|
|
|
shiftId,
|
2025-06-12 21:45:34 +02:00
|
|
|
DateTime.fromISO(api.start, opts),
|
|
|
|
DateTime.fromISO(api.end, opts),
|
|
|
|
new Set(api.assigned),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
toApi(): ApiScheduleShiftSlot {
|
2025-06-14 19:12:31 +02:00
|
|
|
if (this.deleted) {
|
|
|
|
throw new Error("ClientScheduleShiftSlot.toApi: Unexpected deleted slot")
|
|
|
|
}
|
2025-06-12 21:45:34 +02:00
|
|
|
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>;
|
2025-06-14 19:12:31 +02:00
|
|
|
originalEventSlots: Map<Id, ClientScheduleEventSlot>;
|
2025-06-12 21:45:34 +02:00
|
|
|
originalRoles: Map<Id, ClientScheduleRole>;
|
|
|
|
originalShifts: Map<Id, ClientScheduleShift>;
|
2025-06-14 19:12:31 +02:00
|
|
|
originalShiftSlots: Map<Id, ClientScheduleShiftSlot>;
|
|
|
|
shiftSlots: Map<Id, ClientScheduleShiftSlot>;
|
|
|
|
eventSlots: Map<Id, ClientScheduleEventSlot>;
|
2025-06-13 20:49:04 +02:00
|
|
|
modified: boolean;
|
2025-06-14 19:22:53 +02:00
|
|
|
nextClientId = -1;
|
2025-06-12 21:45:34 +02:00
|
|
|
|
|
|
|
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);
|
2025-06-14 19:12:31 +02:00
|
|
|
// 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);
|
2025-06-13 20:49:04 +02:00
|
|
|
this.modified = false;
|
2025-06-14 19:22:53 +02:00
|
|
|
// 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;
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
equals(other: ClientSchedule): boolean {
|
|
|
|
throw new Error("ClientSchedule.equals not implemented")
|
|
|
|
}
|
|
|
|
|
2025-06-13 20:49:04 +02:00
|
|
|
private recalcModified() {
|
|
|
|
this.modified = (
|
|
|
|
!mapEquals(this.locations, this.originalLocations)
|
|
|
|
|| !mapEquals(this.events, this.originalEvents)
|
|
|
|
|| !mapEquals(this.roles, this.originalRoles)
|
|
|
|
|| !mapEquals(this.shifts, this.originalShifts)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-06-13 17:04:55 +02:00
|
|
|
private fixLocationRefs(locations: Map<Id, ClientScheduleLocation>) {
|
2025-06-12 21:45:34 +02:00
|
|
|
for (const events of [this.events, this.originalEvents]) {
|
|
|
|
for (const event of events.values()) {
|
2025-06-14 19:12:31 +02:00
|
|
|
for (const slot of event.slots.values()) {
|
2025-06-12 21:45:34 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-13 17:04:55 +02:00
|
|
|
private checkLocationRefsForDeletion(id: Id) {
|
2025-06-12 21:45:34 +02:00
|
|
|
for (const event of this.events.values()) {
|
2025-06-13 20:51:24 +02:00
|
|
|
if (event.deleted)
|
|
|
|
continue;
|
2025-06-14 19:12:31 +02:00
|
|
|
for (const slot of event.slots.values()) {
|
2025-06-12 21:45:34 +02:00
|
|
|
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`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-13 20:49:04 +02:00
|
|
|
isModifiedLocation(id: Id) {
|
|
|
|
return this.originalLocations.get(id) !== this.locations.get(id);
|
|
|
|
}
|
|
|
|
|
2025-06-12 21:45:34 +02:00
|
|
|
setLocation(location: ClientScheduleLocation) {
|
|
|
|
if (location.deleted) {
|
2025-06-13 17:04:55 +02:00
|
|
|
this.checkLocationRefsForDeletion(location.id);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
this.locations.set(location.id, location);
|
|
|
|
if (!location.deleted) {
|
2025-06-13 17:04:55 +02:00
|
|
|
this.fixLocationRefs(new Map([[location.id, location]]));
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
2025-06-13 20:49:04 +02:00
|
|
|
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);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
restoreLocation(id: Id) {
|
|
|
|
const location = this.originalLocations.get(id);
|
|
|
|
if (location) {
|
|
|
|
this.locations.set(id, location);
|
2025-06-13 17:04:55 +02:00
|
|
|
this.fixLocationRefs(new Map([[location.id, location]]));
|
2025-06-12 21:45:34 +02:00
|
|
|
} else {
|
2025-06-13 17:04:55 +02:00
|
|
|
this.checkLocationRefsForDeletion(id);
|
2025-06-12 21:45:34 +02:00
|
|
|
this.locations.delete(id);
|
|
|
|
}
|
2025-06-13 20:49:04 +02:00
|
|
|
this.recalcModified();
|
|
|
|
}
|
|
|
|
|
|
|
|
isModifiedEvent(id: Id) {
|
|
|
|
return this.originalEvents.get(id) !== this.events.get(id);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setEvent(event: ClientScheduleEvent) {
|
2025-06-14 19:12:31 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2025-06-12 21:45:34 +02:00
|
|
|
this.events.set(event.id, event);
|
2025-06-13 20:49:04 +02:00
|
|
|
this.modified = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
editEvent(
|
|
|
|
event: ClientScheduleEvent,
|
|
|
|
edits: {
|
|
|
|
deleted?: boolean,
|
|
|
|
name?: string,
|
2025-06-14 19:12:31 +02:00
|
|
|
crew?: boolean,
|
|
|
|
description?: string,
|
|
|
|
slots?: Map<Id, ClientScheduleEventSlot>,
|
2025-06-13 20:49:04 +02:00
|
|
|
},
|
|
|
|
) {
|
|
|
|
const copy = event.clone();
|
|
|
|
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
|
|
|
|
if (edits.name !== undefined) copy.name = edits.name;
|
2025-06-14 19:12:31 +02:00
|
|
|
if (edits.crew !== undefined) copy.crew = edits.crew;
|
2025-06-13 20:49:04 +02:00
|
|
|
if (edits.description !== undefined) copy.description = edits.description;
|
2025-06-14 19:12:31 +02:00
|
|
|
if (edits.slots !== undefined) copy.slots = edits.slots;
|
2025-06-13 20:49:04 +02:00
|
|
|
this.setEvent(copy);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
restoreEvent(id: Id) {
|
|
|
|
const event = this.originalEvents.get(id);
|
|
|
|
if (event) {
|
|
|
|
this.events.set(id, event);
|
|
|
|
} else {
|
|
|
|
this.events.delete(id);
|
|
|
|
}
|
2025-06-13 17:04:55 +02:00
|
|
|
this.recalcModified();
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
2025-06-14 19:12:31 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-06-13 17:04:55 +02:00
|
|
|
private fixRoleRefs(roles: Map<Id, ClientScheduleRole>) {
|
2025-06-12 21:45:34 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-13 17:04:55 +02:00
|
|
|
private checkRoleRefsForDeletion(id: Id) {
|
2025-06-12 21:45:34 +02:00
|
|
|
for (const shift of this.shifts.values()) {
|
2025-06-13 20:51:24 +02:00
|
|
|
if (shift.deleted) {
|
|
|
|
continue;
|
|
|
|
}
|
2025-06-12 21:45:34 +02:00
|
|
|
if (shift.role.id === id) {
|
|
|
|
throw new Error(`Cannot delete role, shift "${shift.name}" depends on it`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-13 20:49:04 +02:00
|
|
|
isModifiedRole(id: Id) {
|
|
|
|
return this.originalRoles.get(id) !== this.roles.get(id);
|
|
|
|
}
|
|
|
|
|
2025-06-12 21:45:34 +02:00
|
|
|
setRole(role: ClientScheduleRole) {
|
|
|
|
if (role.deleted) {
|
2025-06-13 17:04:55 +02:00
|
|
|
this.checkRoleRefsForDeletion(role.id);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
this.roles.set(role.id, role);
|
|
|
|
if (!role.deleted) {
|
2025-06-13 17:04:55 +02:00
|
|
|
this.fixRoleRefs(new Map([[role.id, role]]));
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
2025-06-13 20:49:04 +02:00
|
|
|
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);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
restoreRole(id: Id) {
|
|
|
|
const role = this.originalRoles.get(id);
|
|
|
|
if (role) {
|
|
|
|
this.roles.set(id, role);
|
2025-06-13 17:04:55 +02:00
|
|
|
this.fixRoleRefs(new Map([[role.id, role]]));
|
2025-06-12 21:45:34 +02:00
|
|
|
} else {
|
2025-06-13 17:04:55 +02:00
|
|
|
this.checkRoleRefsForDeletion(id);
|
2025-06-12 21:45:34 +02:00
|
|
|
this.roles.delete(id);
|
|
|
|
}
|
2025-06-13 20:49:04 +02:00
|
|
|
this.recalcModified();
|
|
|
|
}
|
|
|
|
|
|
|
|
isModifiedShift(id: Id) {
|
|
|
|
return this.originalShifts.get(id) !== this.shifts.get(id);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setShift(shift: ClientScheduleShift) {
|
2025-06-14 19:12:31 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2025-06-12 21:45:34 +02:00
|
|
|
this.shifts.set(shift.id, shift);
|
2025-06-13 20:49:04 +02:00
|
|
|
this.modified = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
editShift(
|
|
|
|
shift: ClientScheduleShift,
|
|
|
|
edits: {
|
|
|
|
deleted?: boolean,
|
2025-06-14 19:12:31 +02:00
|
|
|
role?: ClientScheduleRole,
|
2025-06-13 20:49:04 +02:00
|
|
|
name?: string,
|
|
|
|
description?: string
|
2025-06-14 19:12:31 +02:00
|
|
|
slots?: Map<Id, ClientScheduleShiftSlot>,
|
2025-06-13 20:49:04 +02:00
|
|
|
},
|
|
|
|
) {
|
|
|
|
const copy = shift.clone();
|
|
|
|
if (edits.deleted !== undefined) copy.deleted = edits.deleted;
|
2025-06-14 19:12:31 +02:00
|
|
|
if (edits.role !== undefined) copy.role = edits.role;
|
2025-06-13 20:49:04 +02:00
|
|
|
if (edits.name !== undefined) copy.name = edits.name;
|
|
|
|
if (edits.description !== undefined) copy.description = edits.description;
|
2025-06-14 19:12:31 +02:00
|
|
|
if (edits.slots !== undefined) copy.slots = edits.slots;
|
2025-06-13 20:49:04 +02:00
|
|
|
this.setShift(copy);
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
restoreShift(id: Id) {
|
|
|
|
const shift = this.originalShifts.get(id);
|
|
|
|
if (shift) {
|
|
|
|
this.shifts.set(id, shift);
|
|
|
|
} else {
|
|
|
|
this.shifts.delete(id);
|
|
|
|
}
|
2025-06-13 20:49:04 +02:00
|
|
|
this.recalcModified();
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
|
2025-06-14 19:12:31 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2025-06-12 21:45:34 +02:00
|
|
|
static fromApi(api: Living<ApiSchedule>, opts: { zone: Zone, locale: string }) {
|
2025-06-14 19:12:31 +02:00
|
|
|
const locations = idMap(filterAlive(api.locations).map(location => ClientScheduleLocation.fromApi(location, opts)));
|
|
|
|
const roles = idMap(filterAlive(api.roles).map(role => ClientScheduleRole.fromApi(role, opts)));
|
2025-06-12 21:45:34 +02:00
|
|
|
return new this(
|
|
|
|
api.id,
|
|
|
|
DateTime.fromISO(api.updatedAt, opts),
|
|
|
|
api.deleted ?? false,
|
|
|
|
locations,
|
2025-06-14 19:12:31 +02:00
|
|
|
idMap(filterAlive(api.events).map(event => ClientScheduleEvent.fromApi(event, locations, opts))),
|
2025-06-12 21:45:34 +02:00
|
|
|
roles,
|
2025-06-14 19:12:31 +02:00
|
|
|
idMap(filterAlive(api.shifts).map(shift => ClientScheduleShift.fromApi(shift, roles, opts))),
|
2025-06-12 21:45:34 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2025-06-14 19:12:31 +02:00
|
|
|
const setEntites = idMap(filterAlive(entityUpdates).map(entity => fromApi(entity)));
|
2025-06-12 21:45:34 +02:00
|
|
|
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,
|
|
|
|
);
|
2025-06-13 17:04:55 +02:00
|
|
|
this.fixLocationRefs(setLocations);
|
2025-06-12 21:45:34 +02:00
|
|
|
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,
|
|
|
|
);
|
2025-06-13 20:49:04 +02:00
|
|
|
this.recalcModified();
|
2025-06-12 21:45:34 +02:00
|
|
|
}
|
|
|
|
}
|