From 61734d41521a4a305deeaf9e163157998b5a4ac4 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 13 Jun 2025 20:49:04 +0200 Subject: [PATCH] Add editing of client schedule entities Add utility methods to more easily edit the fields of a single entity in the schedule, along with a modification flag and is modified utility to check for changes having been made. --- utils/client-schedule.nuxt.test.ts | 170 +++++++++++++++-------------- utils/client-schedule.ts | 106 ++++++++++++++++++ 2 files changed, 192 insertions(+), 84 deletions(-) diff --git a/utils/client-schedule.nuxt.test.ts b/utils/client-schedule.nuxt.test.ts index f58c51d..6b92cde 100644 --- a/utils/client-schedule.nuxt.test.ts +++ b/utils/client-schedule.nuxt.test.ts @@ -1,4 +1,4 @@ -import { ClientSchedule, ClientScheduleEventSlot, ClientScheduleLocation, toIso } from "./client-schedule"; +import { ClientEntity, ClientSchedule, ClientScheduleEventSlot, ClientScheduleLocation, toIso } from "./client-schedule"; import { describe, expect, test } from "vitest"; import type { ApiSchedule } from "~/shared/types/api"; import type { Living } from "~/shared/types/common"; @@ -198,87 +198,89 @@ describe("class ClientSchedule", () => { }); } - test("create location", () => { - const schedule = fixtureClientSchedule(); - const location = new ClientScheduleLocation(3, now, false, "New location", ""); - schedule.setLocation(location); - expect(schedule.originalLocations.get(3)).toBe(undefined); - expect(schedule.locations.get(3)).toBe(location); - }); - - test("update location", () => { - const schedule = fixtureClientSchedule(); - const original = schedule.locations.get(1)!; - const copy = original.clone(); - copy.name = "Modified Location"; - schedule.setLocation(copy); - expect(schedule.originalLocations.get(1)).toBe(original); - expect(schedule.locations.get(1)).toBe(copy); - expect(schedule.events.get(1)!.slots[0].locations[0]).toBe(copy); - }); - - test("delete location in use throws", () => { - const schedule = fixtureClientSchedule(); - const original = schedule.locations.get(1)!; - const copy = original.clone(); - copy.deleted = true; - expect( - () => { schedule.setLocation(copy); } - ).toThrow(new Error('Cannot delete location, event "Up" depends on it')); - }); - - test("delete location", () => { - const schedule = fixtureClientSchedule(); - const event = schedule.events.get(1)!.clone(); - event.slots = []; - schedule.setEvent(event); - const original = schedule.locations.get(1)!; - const copy = original.clone(); - copy.deleted = true; - schedule.setLocation(copy); - expect(schedule.originalLocations.get(1)).toBe(original); - expect(schedule.locations.get(1)).toBe(copy); - }); - - test("create role", () => { - const schedule = fixtureClientSchedule(); - const role = new ClientScheduleRole(3, now, false, "New role", ""); - schedule.setRole(role); - expect(schedule.originalRoles.get(3)).toBe(undefined); - expect(schedule.roles.get(3)).toBe(role); - }); - - test("update role", () => { - const schedule = fixtureClientSchedule(); - const original = schedule.roles.get(1)!; - const copy = original.clone(); - copy.name = "Modified Role"; - schedule.setRole(copy); - expect(schedule.originalRoles.get(1)).toBe(original); - expect(schedule.roles.get(1)).toBe(copy); - expect(schedule.shifts.get(1)!.role).toBe(copy); - }); - - test("delete role in use throws", () => { - const schedule = fixtureClientSchedule(); - const original = schedule.roles.get(1)!; - const copy = original.clone(); - copy.deleted = true; - expect( - () => { schedule.setRole(copy); } - ).toThrow(new Error('Cannot delete role, shift "White" depends on it')); - }); - - test("delete role", () => { - const schedule = fixtureClientSchedule(); - const shift = schedule.shifts.get(1)!.clone(); - shift.role = schedule.roles.get(2)!; - schedule.setShift(shift); - const original = schedule.roles.get(1)!; - const copy = original.clone(); - copy.deleted = true; - schedule.setRole(copy); - expect(schedule.originalRoles.get(1)).toBe(original); - expect(schedule.roles.get(1)).toBe(copy); - }); + const entityTests: [string, (schedule: ClientSchedule) => ClientEntity][] = [ + [ + "location", + () => new ClientScheduleLocation(3, now, false, "New location", "") + ], + [ + "event", + () => new ClientScheduleEvent(3, now, false, "New location", false, "", false, "", 0, []) + ], + [ + "role", + () => new ClientScheduleRole(3, now, false, "New location", "") + ], + [ + "shift", + (schedule) => new ClientScheduleShift(3, now, false, schedule.roles.get(1)!, "New location", "", []) + ], + ] as const; + for (const [name, create] of entityTests) { + describe(name, () => { + const Name = name[0].toUpperCase() + name.slice(1); + test(`create`, () => { + const schedule = fixtureClientSchedule(); + const entity = create(schedule); + expect(schedule.modified).toBe(false); + // Create + (schedule as any)[`set${Name}`](entity); + // Check + expect(schedule.modified).toBe(true); + expect((schedule as any)[`isModified${Name}`](entity.id)).toBe(true); + expect((schedule as any)[`original${Name}s`].get(entity.id)).toBe(undefined); + expect((schedule as any)[`${name}s`].get(entity.id)).toBe(entity); + }); + test("edit", () => { + const schedule = fixtureClientSchedule(); + const original = (schedule as any)[`${name}s`].get(1); + expect(schedule.modified).toBe(false); + expect((schedule as any)[`isModified${Name}`](1)).toBe(false); + // Edit + (schedule as any)[`edit${Name}`](original, { name: `Modified ${name}` }) + // Check + expect(schedule.modified).toBe(true); + expect((schedule as any)[`isModified${Name}`](1)).toBe(true); + expect((schedule as any)[`original${Name}s`].get(1)).toBe(original); + if (name === "location") { + expect(schedule.events.get(1)!.slots[0].locations[0]).toBe(schedule.locations.get(1)); + } else if (name === "role") { + expect(schedule.shifts.get(1)!.role).toBe(schedule.roles.get(1)); + } + }); + if (name === "location") { + test("delete location in use throws", () => { + const schedule = fixtureClientSchedule(); + expect( + () => { schedule.editLocation(schedule.locations.get(1)!, { deleted: true }); } + ).toThrow(new Error('Cannot delete location, event "Up" depends on it')); + }); + } else if (name === "role") { + test("delete role in use throws", () => { + const schedule = fixtureClientSchedule(); + expect( + () => { schedule.editRole(schedule.roles.get(1)!, { deleted: true }); } + ).toThrow(new Error('Cannot delete role, shift "White" depends on it')); + }); + } + test("delete", () => { + const schedule = fixtureClientSchedule(); + const original = (schedule as any)[`${name}s`].get(1); + expect(schedule.modified).toBe(false); + expect((schedule as any)[`isModified${Name}`](1)).toBe(false); + // Delete + if (name === "location") { + schedule.editEvent(schedule.events.get(1)!, { deleted: true }); + } else if (name === "role") { + schedule.editShift(schedule.shifts.get(1)!, { deleted: true }); + } + (schedule as any)[`edit${Name}`](original, { deleted: true }) + // Check + expect(schedule.modified).toBe(true); + expect((schedule as any)[`isModified${Name}`](1)).toBe(true); + expect((schedule as any)[`original${Name}s`].get(1)).toBe(original); + expect((schedule as any)[`${name}s`].get(1).deleted).toBe(true); + }); + }); + } }); diff --git a/utils/client-schedule.ts b/utils/client-schedule.ts index bf1ecd3..bd02514 100644 --- a/utils/client-schedule.ts +++ b/utils/client-schedule.ts @@ -424,6 +424,7 @@ export class ClientSchedule extends ClientEntity { originalEvents: Map; originalRoles: Map; originalShifts: Map; + modified: boolean; constructor( id: 111, @@ -439,12 +440,33 @@ export class ClientSchedule extends ClientEntity { 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(a: Map, b: Map) { + 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) { for (const events of [this.events, this.originalEvents]) { for (const event of events.values()) { @@ -472,6 +494,10 @@ export class ClientSchedule extends ClientEntity { } } + isModifiedLocation(id: Id) { + return this.originalLocations.get(id) !== this.locations.get(id); + } + setLocation(location: ClientScheduleLocation) { if (location.deleted) { this.checkLocationRefsForDeletion(location.id); @@ -480,6 +506,22 @@ export class ClientSchedule extends ClientEntity { 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) { @@ -491,10 +533,31 @@ export class ClientSchedule extends ClientEntity { 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) { @@ -526,6 +589,10 @@ export class ClientSchedule extends ClientEntity { } } + isModifiedRole(id: Id) { + return this.originalRoles.get(id) !== this.roles.get(id); + } + setRole(role: ClientScheduleRole) { if (role.deleted) { this.checkRoleRefsForDeletion(role.id); @@ -534,6 +601,22 @@ export class ClientSchedule extends ClientEntity { 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) { @@ -545,10 +628,31 @@ export class ClientSchedule extends ClientEntity { 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) { @@ -558,6 +662,7 @@ export class ClientSchedule extends ClientEntity { } else { this.shifts.delete(id); } + this.recalcModified(); } static fromApi(api: Living, opts: { zone: Zone, locale: string }) { @@ -680,5 +785,6 @@ export class ClientSchedule extends ClientEntity { this.originalShifts, this.shifts, ); + this.recalcModified(); } }