@lilaquadrat/studio 12.3.0 → 13.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/src/models/storage.model.js +2 -1
- package/lib/src/models/storage.model.js.map +1 -1
- package/lib/src/services/lists.service.d.ts +2 -0
- package/lib/src/services/lists.service.js +4 -0
- package/lib/src/services/lists.service.js.map +1 -1
- package/lib/src/services/shiftPlanDays.service.d.ts +2 -0
- package/lib/src/services/shiftPlanDays.service.js +12 -0
- package/lib/src/services/shiftPlanDays.service.js.map +1 -1
- package/lib/src/services/shiftPlans.service.d.ts +2 -2
- package/lib/src/services/shiftPlans.service.js +67 -9
- package/lib/src/services/shiftPlans.service.js.map +1 -1
- package/lib/src/services/shifts.service.d.ts +2 -0
- package/lib/src/services/shifts.service.js +9 -0
- package/lib/src/services/shifts.service.js.map +1 -1
- package/lib/tests/shiftPlanning.spec.js +103 -38
- package/lib/tests/shiftPlanning.spec.js.map +1 -1
- package/lib/tests/shiftPlansList.spec.d.ts +1 -0
- package/lib/tests/shiftPlansList.spec.js +161 -0
- package/lib/tests/shiftPlansList.spec.js.map +1 -0
- package/lib/tests/storageModel.spec.d.ts +1 -0
- package/lib/tests/storageModel.spec.js +12 -0
- package/lib/tests/storageModel.spec.js.map +1 -0
- package/package.json +2 -2
|
@@ -170,6 +170,40 @@ describe('shift planning', () => {
|
|
|
170
170
|
const cleared = await plansService.changePlan(created._id, { name: created.name, timeZone: created.timeZone, settingsOverrides: {} }, options);
|
|
171
171
|
expect(cleared.list).toBeUndefined();
|
|
172
172
|
});
|
|
173
|
+
it.each(['replace', 'remove'])('refuses to %s the employee list until existing assignments are cleared', async (change) => {
|
|
174
|
+
const plan = await setupPlan();
|
|
175
|
+
const otherListId = new ObjectId();
|
|
176
|
+
await ListsModel.db.insertOne({ ...list, _id: otherListId, name: 'Events' });
|
|
177
|
+
await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [] }), options);
|
|
178
|
+
const shift = await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId), options);
|
|
179
|
+
const previousPlan = await ShiftPlanModel.db.findOne({ _id: plan._id });
|
|
180
|
+
const previousShift = await ShiftModel.db.findOne({ _id: shift._id });
|
|
181
|
+
const historyCount = await HistoryModel.db.countDocuments({});
|
|
182
|
+
const input = change === 'replace' ? planInput(otherListId) : planInput(listId, { list: undefined });
|
|
183
|
+
await expect(plansService.changePlan(plan._id, input, options)).rejects.toThrow('SHIFT_PLAN_REFERENCES_IN_USE');
|
|
184
|
+
expect(await ShiftPlanModel.db.findOne({ _id: plan._id })).toEqual(previousPlan);
|
|
185
|
+
expect(await ShiftModel.db.findOne({ _id: shift._id })).toEqual(previousShift);
|
|
186
|
+
expect(await HistoryModel.db.countDocuments({})).toBe(historyCount);
|
|
187
|
+
const cleared = await shiftsService.replaceAssignments(plan._id, shift._id, { assignments: [] }, options);
|
|
188
|
+
expect(cleared.assignments).toEqual([]);
|
|
189
|
+
const changed = await plansService.changePlan(plan._id, input, options);
|
|
190
|
+
expect(changed.list?.toHexString()).toBe(input.list?.toHexString());
|
|
191
|
+
});
|
|
192
|
+
it('does not let assignments in another plan, project or company prevent an employee-list change', async () => {
|
|
193
|
+
const plan = await setupPlan();
|
|
194
|
+
const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [] }), options);
|
|
195
|
+
const document = (await ShiftModel.db.findOne({ _id: shift._id }));
|
|
196
|
+
const assignments = [{ participant: participantId, customer: customerId, breaks: [] }];
|
|
197
|
+
await ShiftModel.db.insertMany([
|
|
198
|
+
{ ...document, _id: new ObjectId(), plan: new ObjectId(), assignments },
|
|
199
|
+
{ ...document, _id: new ObjectId(), project: 'foreign', assignments },
|
|
200
|
+
{ ...document, _id: new ObjectId(), company: 'foreign', assignments },
|
|
201
|
+
]);
|
|
202
|
+
const changed = await plansService.changePlan(plan._id, planInput(listId, { list: undefined }), options);
|
|
203
|
+
expect(changed.list).toBeUndefined();
|
|
204
|
+
expect(changed.shifts).toHaveLength(1);
|
|
205
|
+
expect(changed.shifts[0]._id.equals(shift._id)).toBe(true);
|
|
206
|
+
});
|
|
173
207
|
it('resolves assignments against the linked plan list for a shift persisted before the link', async () => {
|
|
174
208
|
await settingsService.changeSettings({ settings: policy() }, options);
|
|
175
209
|
const created = await plansService.createPlan({ name: 'Unlinked', timeZone: 'Europe/Berlin', settingsOverrides: {} }, options);
|
|
@@ -209,11 +243,46 @@ describe('shift planning', () => {
|
|
|
209
243
|
const bound = await plansService.changePlan(plan._id, planInput(listId, { workAreaCategories: [{ area, categoryIds: [categoryId] }] }), options);
|
|
210
244
|
expect(bound.workAreaCategories).toHaveLength(1);
|
|
211
245
|
expect(bound.workAreaCategories[0].categoryIds[0].equals(categoryId)).toBe(true);
|
|
246
|
+
const kept = await plansService.changePlan(plan._id, planInput(new ObjectId(listId.toHexString()), { name: 'Renamed' }), options);
|
|
247
|
+
expect(kept.workAreaCategories).toEqual(bound.workAreaCategories);
|
|
212
248
|
const cleared = await plansService.changePlan(plan._id, planInput(listId, { workAreaCategories: [] }), options);
|
|
213
249
|
expect(cleared.workAreaCategories).toEqual([]);
|
|
214
250
|
const untouched = await plansService.changePlan(plan._id, planInput(listId), options);
|
|
215
251
|
expect(untouched.workAreaCategories).toEqual([]);
|
|
216
252
|
});
|
|
253
|
+
it.each(['replace', 'remove'])('clears omitted category bindings when an unassigned plan changes its list through %s', async (change) => {
|
|
254
|
+
const plan = await setupPlan();
|
|
255
|
+
await plansService.changePlan(plan._id, planInput(listId, {
|
|
256
|
+
workAreaCategories: [{ area: housekeepingAreaId, categoryIds: [categoryId] }],
|
|
257
|
+
}), options);
|
|
258
|
+
await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [] }), options);
|
|
259
|
+
const otherListId = new ObjectId();
|
|
260
|
+
const otherCategoryId = new ObjectId();
|
|
261
|
+
await ListsModel.db.insertOne({ ...list, _id: otherListId, categories: [{ _id: otherCategoryId, id: 'events', name: 'Events' }] });
|
|
262
|
+
const input = change === 'replace' ? planInput(otherListId) : planInput(listId, { list: undefined });
|
|
263
|
+
const changed = await plansService.changePlan(plan._id, input, options);
|
|
264
|
+
expect(changed.list?.toHexString()).toBe(input.list?.toHexString());
|
|
265
|
+
expect(changed.workAreaCategories).toEqual([]);
|
|
266
|
+
expect(changed.days).toEqual(plan.days);
|
|
267
|
+
expect(changed.workAreas).toEqual(plan.workAreas);
|
|
268
|
+
expect(changed.shifts).toHaveLength(1);
|
|
269
|
+
});
|
|
270
|
+
it('validates explicit replacement bindings against the new employee list before changing the plan', async () => {
|
|
271
|
+
const plan = await setupPlan();
|
|
272
|
+
const bindings = [{ area: housekeepingAreaId, categoryIds: [categoryId] }];
|
|
273
|
+
await plansService.changePlan(plan._id, planInput(listId, { workAreaCategories: bindings }), options);
|
|
274
|
+
const otherListId = new ObjectId();
|
|
275
|
+
const otherCategoryId = new ObjectId();
|
|
276
|
+
await ListsModel.db.insertOne({ ...list, _id: otherListId, categories: [{ _id: otherCategoryId, id: 'events', name: 'Events' }] });
|
|
277
|
+
const previous = await ShiftPlanModel.db.findOne({ _id: plan._id });
|
|
278
|
+
await expect(plansService.changePlan(plan._id, planInput(otherListId, { workAreaCategories: bindings }), options)).rejects.toThrow('VALIDATION_FAILED');
|
|
279
|
+
await expect(plansService.changePlan(plan._id, planInput(listId, { list: undefined, workAreaCategories: bindings }), options)).rejects.toThrow('VALIDATION_FAILED');
|
|
280
|
+
expect(await ShiftPlanModel.db.findOne({ _id: plan._id })).toEqual(previous);
|
|
281
|
+
const replacement = [{ area: housekeepingAreaId, categoryIds: [otherCategoryId] }];
|
|
282
|
+
const changed = await plansService.changePlan(plan._id, planInput(otherListId, { workAreaCategories: replacement }), options);
|
|
283
|
+
expect(changed.list.equals(otherListId)).toBe(true);
|
|
284
|
+
expect(changed.workAreaCategories).toEqual(replacement);
|
|
285
|
+
});
|
|
217
286
|
it('stores, preserves and clears the default participant state', async () => {
|
|
218
287
|
const plan = await setupPlan();
|
|
219
288
|
const set = await plansService.changePlan(plan._id, planInput(listId, { defaultParticipantState: 'confirmed' }), options);
|
|
@@ -257,6 +326,38 @@ describe('shift planning', () => {
|
|
|
257
326
|
const history = await HistoryModel.db.findOne({ 'history.parent': shift._id });
|
|
258
327
|
expect(history).toMatchObject({ assignments: [{ customer: customerId }, { customer: secondCustomerId }], history: { model: 'shifts', reason: 'create', user: options.user } });
|
|
259
328
|
});
|
|
329
|
+
it('returns the complete scoped schedule in plan details while keeping standalone shift lists paginated', async () => {
|
|
330
|
+
const plan = await setupPlan();
|
|
331
|
+
const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, {
|
|
332
|
+
start: `${day}T16:00:00+02:00`, end: `${day}T18:00:00+02:00`,
|
|
333
|
+
}), options);
|
|
334
|
+
const document = (await ShiftModel.db.findOne({ _id: shift._id }));
|
|
335
|
+
const additionalShifts = Array.from({ length: 30 }, (_, index) => ({
|
|
336
|
+
...document, _id: new ObjectId(), assignments: [],
|
|
337
|
+
start: new Date(Date.parse(`${day}T06:00:00Z`) + index * 60000).toISOString(),
|
|
338
|
+
end: new Date(Date.parse(`${day}T07:00:00Z`) + index * 60000).toISOString(),
|
|
339
|
+
}));
|
|
340
|
+
await ShiftModel.db.insertMany([
|
|
341
|
+
...additionalShifts,
|
|
342
|
+
{ ...document, _id: new ObjectId(), project: 'foreign', assignments: [] },
|
|
343
|
+
{ ...document, _id: new ObjectId(), company: 'foreign', assignments: [] },
|
|
344
|
+
{ ...document, _id: new ObjectId(), plan: new ObjectId() },
|
|
345
|
+
]);
|
|
346
|
+
const detail = (await plansService.singlePlan(plan._id, options));
|
|
347
|
+
expect(detail.shifts.map((entry) => entry._id)).toEqual([...additionalShifts.map((entry) => entry._id), shift._id]);
|
|
348
|
+
expect(detail.shifts[30]).toMatchObject({
|
|
349
|
+
_id: shift._id, participants: [{ participant: participantId, customer: customerId }],
|
|
350
|
+
violations: [], workingTime: [{ customer: customerId.toHexString(), daytimeMinutes: 120, nighttimeMinutes: 0, breakMinutes: 0 }],
|
|
351
|
+
});
|
|
352
|
+
const firstPage = await shiftsService.listShifts(plan._id, {}, options);
|
|
353
|
+
const secondPage = await shiftsService.listShifts(plan._id, { site: 2 }, options);
|
|
354
|
+
expect(firstPage).toMatchObject({ all: 31, count: 30 });
|
|
355
|
+
expect(secondPage).toMatchObject({ all: 31, count: 1 });
|
|
356
|
+
expect(secondPage.data[0]._id.equals(shift._id)).toBe(true);
|
|
357
|
+
await expect(plansService.changePlan(plan._id, planInput(listId, { list: undefined }), options)).rejects.toThrow('SHIFT_PLAN_REFERENCES_IN_USE');
|
|
358
|
+
expect(await plansService.singlePlan(plan._id, { ...options, company: 'foreign' })).toBeNull();
|
|
359
|
+
expect(await plansService.singlePlan(plan._id, { ...options, project: 'foreign' })).toBeNull();
|
|
360
|
+
});
|
|
260
361
|
it('stores shift structures and people requirements and replaces them on change', async () => {
|
|
261
362
|
const plan = await setupPlan();
|
|
262
363
|
const structureId = '507f1f77bcf86cd799439011';
|
|
@@ -743,44 +844,8 @@ describe('shift planning', () => {
|
|
|
743
844
|
expect(member.data[0].area).not.toHaveProperty('company');
|
|
744
845
|
expect(member.data[0].area).not.toHaveProperty('project');
|
|
745
846
|
});
|
|
746
|
-
it('
|
|
747
|
-
await
|
|
748
|
-
await plansService.createPlan(planInput(listId, { name: 'Events [October]' }), options);
|
|
749
|
-
const matched = await plansService.listPlans({ search: 'events' }, options);
|
|
750
|
-
expect(matched.all).toBe(1);
|
|
751
|
-
expect(matched.data[0].name).toBe('Events [October]');
|
|
752
|
-
const escaped = await plansService.listPlans({ search: '[October]' }, options);
|
|
753
|
-
expect(escaped.all).toBe(1);
|
|
754
|
-
expect((await plansService.listPlans({ search: 'missing' }, options)).all).toBe(0);
|
|
755
|
-
});
|
|
756
|
-
it('lists weekly plans by planning dates and keeps both bounds on the same day', async () => {
|
|
757
|
-
const first = await setupPlan();
|
|
758
|
-
const futureCreated = await plansService.createPlan(planInput(listId), options);
|
|
759
|
-
await createDays(futureCreated._id, options, [{ date: '2026-10-12', areas: areaLinks() }]);
|
|
760
|
-
const disjointCreated = await plansService.createPlan(planInput(listId), options);
|
|
761
|
-
await createDays(disjointCreated._id, options, [{ date: '2026-10-01', areas: areaLinks() }, { date: '2026-10-20', areas: areaLinks() }]);
|
|
762
|
-
// mongo-mock has no distinct implementation; reproduce the day-date range query over the stored day documents.
|
|
763
|
-
const storedDays = await DayModel.db.find({ company: options.company, project: options.project }).toArray();
|
|
764
|
-
const distinctSpy = jest.spyOn(DayModel.db, 'distinct').mockImplementation((async (_field, filter) => {
|
|
765
|
-
const range = (filter.date ?? {});
|
|
766
|
-
const plans = storedDays
|
|
767
|
-
.filter((entry) => (!range.$gte || entry.date >= range.$gte) && (!range.$lt || entry.date < range.$lt))
|
|
768
|
-
.map((entry) => entry.plan.toHexString());
|
|
769
|
-
return [...new Set(plans)].map((hex) => new ObjectId(hex));
|
|
770
|
-
}));
|
|
771
|
-
try {
|
|
772
|
-
const filtered = await plansService.listPlans({ list: listId, from: '2026-10-10', to: '2026-10-15' }, options);
|
|
773
|
-
expect(filtered.all).toBe(1);
|
|
774
|
-
expect(filtered.data[0]._id.equals(futureCreated._id)).toBe(true);
|
|
775
|
-
const ordered = await plansService.listPlans({ list: listId, from: day, to: '2026-10-13' }, options);
|
|
776
|
-
expect(ordered.all).toBe(2);
|
|
777
|
-
expect(ordered.data[0]._id.equals(first._id)).toBe(true);
|
|
778
|
-
await expect(plansService.listPlans({ list: listId, from: day, to: day }, options)).rejects.toThrow('VALIDATION_FAILED');
|
|
779
|
-
expect(await plansService.singlePlan(new ObjectId(), options)).toBeNull();
|
|
780
|
-
}
|
|
781
|
-
finally {
|
|
782
|
-
distinctSpy.mockRestore();
|
|
783
|
-
}
|
|
847
|
+
it('returns null for a missing plan', async () => {
|
|
848
|
+
expect(await plansService.singlePlan(new ObjectId(), options)).toBeNull();
|
|
784
849
|
});
|
|
785
850
|
it('permits overlapping departments in one area while keeping shift operations bound to their plan', async () => {
|
|
786
851
|
const restaurant = await setupPlan();
|