Use a single mutable location, event, slot, etc, for each unique resource that keeps track of the local editable client copy and the server copy of the data contained in it. This makes it much simpler to update these data structures as I can take advantage of the v-model bindings in Vue.js and work with the system instead of against it.
988 lines
24 KiB
TypeScript
988 lines
24 KiB
TypeScript
import { DateTime, FixedOffsetZone, Zone } from "~/shared/utils/luxon";
|
|
import type {
|
|
ApiSchedule,
|
|
ApiScheduleEvent,
|
|
ApiScheduleEventSlot,
|
|
ApiScheduleLocation,
|
|
ApiScheduleRole,
|
|
ApiScheduleShift,
|
|
ApiScheduleShiftSlot
|
|
} from "~/shared/types/api";
|
|
import type { Entity, Id, Living, Tombstone } from "~/shared/types/common";
|
|
import { mapEquals, setEquals } from "~/shared/utils/functions";
|
|
import { ClientEntity } from "~/utils/client-entity";
|
|
|
|
function filterAlive<T extends Entity>(entities?: T[]) {
|
|
return (entities ?? []).filter((entity) => !entity.deleted) as Living<T>[];
|
|
}
|
|
|
|
function filterTombstone<T extends Entity>(entities?: T[]) {
|
|
return (entities ?? []).filter((entity) => entity.deleted) as Tombstone<T>[];
|
|
}
|
|
|
|
export function toIso(timestamp: DateTime) {
|
|
return timestamp.setZone(FixedOffsetZone.utcInstance).toISO();
|
|
}
|
|
|
|
function mapWith<K, V>(map: Map<K, V>, key: K, value: V) {
|
|
const copy = new Map(map);
|
|
copy.set(key, value);
|
|
return copy;
|
|
}
|
|
|
|
function mapWithout<K, V>(map: Map<K, V>, key: K) {
|
|
const copy = new Map(map);
|
|
copy.delete(key);
|
|
return copy;
|
|
}
|
|
|
|
|
|
export class ClientScheduleLocation extends ClientEntity {
|
|
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: Living<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: Living<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 {
|
|
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 {
|
|
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 this.slotIds.difference(this.serverSlotIds)) {
|
|
const slot = this.schedule.eventSlots.get(id)!;
|
|
slot.setEventId(slot.serverEventId);
|
|
}
|
|
}
|
|
|
|
discardSlot(id: Id) {
|
|
if (!this.slotIds.has(id)) {
|
|
throw new Error("ClientScheduleEvent.discardSlot: slot does not exist");
|
|
}
|
|
const slot = this.schedule.eventSlots.get(id)!;
|
|
if (slot.isNewEntity) {
|
|
this.slotIds.delete(id);
|
|
this.schedule.eventSlots.delete(id);
|
|
} else {
|
|
slot.discard();
|
|
}
|
|
}
|
|
|
|
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: Living<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: Living<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 {
|
|
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;
|
|
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,
|
|
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) {
|
|
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 {
|
|
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: Living<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: Living<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 {
|
|
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 {
|
|
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 this.slotIds.difference(this.serverSlotIds)) {
|
|
const slot = this.schedule.shiftSlots.get(id)!;
|
|
slot.setShiftId(slot.serverShiftId);
|
|
}
|
|
}
|
|
|
|
discardSlot(id: Id) {
|
|
if (!this.slotIds.has(id)) {
|
|
throw new Error("ClientScheduleShift.discardSlot: slot does not exist");
|
|
}
|
|
const slot = this.schedule.shiftSlots.get(id)!;
|
|
if (slot.isNewEntity) {
|
|
this.slotIds.delete(id);
|
|
this.schedule.shiftSlots.delete(id);
|
|
} else {
|
|
slot.discard();
|
|
}
|
|
}
|
|
|
|
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: Living<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: Living<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 {
|
|
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;
|
|
serverStart: DateTime;
|
|
serverEnd: DateTime;
|
|
serverAssigned: Set<Id>;
|
|
|
|
constructor(
|
|
public id: Id,
|
|
public isNewEntity: boolean,
|
|
public deleted: boolean,
|
|
public shiftId: Id,
|
|
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) {
|
|
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 {
|
|
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();
|
|
}
|
|
|
|
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: Living<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 {
|
|
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: Living<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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|