Vue's proxy handling interferres with the new Set methods causing them to throw a TypeError when used on Set objects that are replaced by proxies. Workaround by dropping the proxy when using these. See https://github.com/vuejs/core/issues/11398
997 lines
25 KiB
TypeScript
997 lines
25 KiB
TypeScript
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.description;
|
|
}
|
|
|
|
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.description;
|
|
}
|
|
|
|
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;
|
|
serverName: string;
|
|
serverDescription: string;
|
|
serverSlotIds: Set<Id>;
|
|
|
|
constructor(
|
|
id: Id,
|
|
updatedAt: DateTime,
|
|
deleted: boolean,
|
|
public roleId: Id,
|
|
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,
|
|
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.roleId = 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|