@lilaquadrat/studio 12.2.0 → 13.0.0
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/helpers/shiftPlanAllocation.d.ts +53 -0
- package/lib/src/helpers/shiftPlanAllocation.js +110 -0
- package/lib/src/helpers/shiftPlanAllocation.js.map +1 -0
- package/lib/src/helpers/shiftPlanCoverage.d.ts +2 -0
- package/lib/src/helpers/shiftPlanCoverage.js +4 -0
- package/lib/src/helpers/shiftPlanCoverage.js.map +1 -0
- package/lib/src/helpers/shiftPlanningTime.js +26 -2
- package/lib/src/helpers/shiftPlanningTime.js.map +1 -1
- package/lib/src/helpers/shiftPlanningValidation.d.ts +8 -2
- package/lib/src/helpers/shiftPlanningValidation.js +16 -0
- package/lib/src/helpers/shiftPlanningValidation.js.map +1 -1
- package/lib/src/helpers/structureMatch.js +3 -1
- package/lib/src/helpers/structureMatch.js.map +1 -1
- package/lib/src/interfaces/ListPlanningParticipant.d.ts +17 -0
- package/lib/src/interfaces/ListPlanningParticipant.js +2 -0
- package/lib/src/interfaces/ListPlanningParticipant.js.map +1 -0
- package/lib/src/interfaces/PlanCoveragePreparation.d.ts +17 -0
- package/lib/src/interfaces/PlanCoveragePreparation.js +2 -0
- package/lib/src/interfaces/PlanCoveragePreparation.js.map +1 -0
- package/lib/src/interfaces/PlanningParticipantsResult.d.ts +12 -0
- package/lib/src/interfaces/PlanningParticipantsResult.js +2 -0
- package/lib/src/interfaces/PlanningParticipantsResult.js.map +1 -0
- package/lib/src/interfaces/ShiftAutofillResult.d.ts +6 -0
- package/lib/src/interfaces/ShiftAutofillResult.js +2 -0
- package/lib/src/interfaces/ShiftAutofillResult.js.map +1 -0
- package/lib/src/interfaces/ShiftReportContextOptions.d.ts +9 -0
- package/lib/src/interfaces/ShiftReportContextOptions.js +2 -0
- package/lib/src/interfaces/ShiftReportContextOptions.js.map +1 -0
- package/lib/src/models/lists.model.js +2 -1
- package/lib/src/models/lists.model.js.map +1 -1
- package/lib/src/services/customers.service.d.ts +2 -2
- package/lib/src/services/customers.service.js +2 -2
- package/lib/src/services/customers.service.js.map +1 -1
- package/lib/src/services/listParticipants.service.d.ts +45 -8
- package/lib/src/services/listParticipants.service.js +320 -39
- package/lib/src/services/listParticipants.service.js.map +1 -1
- package/lib/src/services/lists.service.d.ts +2 -0
- package/lib/src/services/lists.service.js +28 -1
- 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 +20 -1
- package/lib/src/services/shifts.service.js +180 -26
- package/lib/src/services/shifts.service.js.map +1 -1
- package/lib/tests/listParticipantsPlanning.spec.js +406 -10
- package/lib/tests/listParticipantsPlanning.spec.js.map +1 -1
- package/lib/tests/listsServiceCreateExternalProducts.spec.js +18 -0
- package/lib/tests/listsServiceCreateExternalProducts.spec.js.map +1 -1
- package/lib/tests/shiftPlanCoverage.spec.d.ts +1 -0
- package/lib/tests/shiftPlanCoverage.spec.js +189 -0
- package/lib/tests/shiftPlanCoverage.spec.js.map +1 -0
- package/lib/tests/shiftPlanning.spec.js +430 -38
- package/lib/tests/shiftPlanning.spec.js.map +1 -1
- package/lib/tests/shiftPlanningTime.spec.d.ts +1 -0
- package/lib/tests/shiftPlanningTime.spec.js +90 -0
- package/lib/tests/shiftPlanningTime.spec.js.map +1 -0
- 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/structureMatch.spec.js +42 -0
- package/lib/tests/structureMatch.spec.js.map +1 -1
- package/package.json +2 -2
|
@@ -19,6 +19,7 @@ import ParticipantsModel from '../src/models/listsParticipants.model.js';
|
|
|
19
19
|
import CustomersModel from '../src/models/customers.model.js';
|
|
20
20
|
import StructureModel from '../src/models/structure.model.js';
|
|
21
21
|
import HistoryModel from '../src/models/history.model.js';
|
|
22
|
+
import participantsService from '../src/services/listParticipants.service.js';
|
|
22
23
|
const options = { company: 'hotel', project: 'hotel-project', user: 'planner', app: 'studio' };
|
|
23
24
|
const listId = new ObjectId();
|
|
24
25
|
const categoryId = new ObjectId();
|
|
@@ -169,6 +170,40 @@ describe('shift planning', () => {
|
|
|
169
170
|
const cleared = await plansService.changePlan(created._id, { name: created.name, timeZone: created.timeZone, settingsOverrides: {} }, options);
|
|
170
171
|
expect(cleared.list).toBeUndefined();
|
|
171
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
|
+
});
|
|
172
207
|
it('resolves assignments against the linked plan list for a shift persisted before the link', async () => {
|
|
173
208
|
await settingsService.changeSettings({ settings: policy() }, options);
|
|
174
209
|
const created = await plansService.createPlan({ name: 'Unlinked', timeZone: 'Europe/Berlin', settingsOverrides: {} }, options);
|
|
@@ -208,11 +243,46 @@ describe('shift planning', () => {
|
|
|
208
243
|
const bound = await plansService.changePlan(plan._id, planInput(listId, { workAreaCategories: [{ area, categoryIds: [categoryId] }] }), options);
|
|
209
244
|
expect(bound.workAreaCategories).toHaveLength(1);
|
|
210
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);
|
|
211
248
|
const cleared = await plansService.changePlan(plan._id, planInput(listId, { workAreaCategories: [] }), options);
|
|
212
249
|
expect(cleared.workAreaCategories).toEqual([]);
|
|
213
250
|
const untouched = await plansService.changePlan(plan._id, planInput(listId), options);
|
|
214
251
|
expect(untouched.workAreaCategories).toEqual([]);
|
|
215
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
|
+
});
|
|
216
286
|
it('stores, preserves and clears the default participant state', async () => {
|
|
217
287
|
const plan = await setupPlan();
|
|
218
288
|
const set = await plansService.changePlan(plan._id, planInput(listId, { defaultParticipantState: 'confirmed' }), options);
|
|
@@ -256,6 +326,38 @@ describe('shift planning', () => {
|
|
|
256
326
|
const history = await HistoryModel.db.findOne({ 'history.parent': shift._id });
|
|
257
327
|
expect(history).toMatchObject({ assignments: [{ customer: customerId }, { customer: secondCustomerId }], history: { model: 'shifts', reason: 'create', user: options.user } });
|
|
258
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
|
+
});
|
|
259
361
|
it('stores shift structures and people requirements and replaces them on change', async () => {
|
|
260
362
|
const plan = await setupPlan();
|
|
261
363
|
const structureId = '507f1f77bcf86cd799439011';
|
|
@@ -742,44 +844,8 @@ describe('shift planning', () => {
|
|
|
742
844
|
expect(member.data[0].area).not.toHaveProperty('company');
|
|
743
845
|
expect(member.data[0].area).not.toHaveProperty('project');
|
|
744
846
|
});
|
|
745
|
-
it('
|
|
746
|
-
await
|
|
747
|
-
await plansService.createPlan(planInput(listId, { name: 'Events [October]' }), options);
|
|
748
|
-
const matched = await plansService.listPlans({ search: 'events' }, options);
|
|
749
|
-
expect(matched.all).toBe(1);
|
|
750
|
-
expect(matched.data[0].name).toBe('Events [October]');
|
|
751
|
-
const escaped = await plansService.listPlans({ search: '[October]' }, options);
|
|
752
|
-
expect(escaped.all).toBe(1);
|
|
753
|
-
expect((await plansService.listPlans({ search: 'missing' }, options)).all).toBe(0);
|
|
754
|
-
});
|
|
755
|
-
it('lists weekly plans by planning dates and keeps both bounds on the same day', async () => {
|
|
756
|
-
const first = await setupPlan();
|
|
757
|
-
const futureCreated = await plansService.createPlan(planInput(listId), options);
|
|
758
|
-
await createDays(futureCreated._id, options, [{ date: '2026-10-12', areas: areaLinks() }]);
|
|
759
|
-
const disjointCreated = await plansService.createPlan(planInput(listId), options);
|
|
760
|
-
await createDays(disjointCreated._id, options, [{ date: '2026-10-01', areas: areaLinks() }, { date: '2026-10-20', areas: areaLinks() }]);
|
|
761
|
-
// mongo-mock has no distinct implementation; reproduce the day-date range query over the stored day documents.
|
|
762
|
-
const storedDays = await DayModel.db.find({ company: options.company, project: options.project }).toArray();
|
|
763
|
-
const distinctSpy = jest.spyOn(DayModel.db, 'distinct').mockImplementation((async (_field, filter) => {
|
|
764
|
-
const range = (filter.date ?? {});
|
|
765
|
-
const plans = storedDays
|
|
766
|
-
.filter((entry) => (!range.$gte || entry.date >= range.$gte) && (!range.$lt || entry.date < range.$lt))
|
|
767
|
-
.map((entry) => entry.plan.toHexString());
|
|
768
|
-
return [...new Set(plans)].map((hex) => new ObjectId(hex));
|
|
769
|
-
}));
|
|
770
|
-
try {
|
|
771
|
-
const filtered = await plansService.listPlans({ list: listId, from: '2026-10-10', to: '2026-10-15' }, options);
|
|
772
|
-
expect(filtered.all).toBe(1);
|
|
773
|
-
expect(filtered.data[0]._id.equals(futureCreated._id)).toBe(true);
|
|
774
|
-
const ordered = await plansService.listPlans({ list: listId, from: day, to: '2026-10-13' }, options);
|
|
775
|
-
expect(ordered.all).toBe(2);
|
|
776
|
-
expect(ordered.data[0]._id.equals(first._id)).toBe(true);
|
|
777
|
-
await expect(plansService.listPlans({ list: listId, from: day, to: day }, options)).rejects.toThrow('VALIDATION_FAILED');
|
|
778
|
-
expect(await plansService.singlePlan(new ObjectId(), options)).toBeNull();
|
|
779
|
-
}
|
|
780
|
-
finally {
|
|
781
|
-
distinctSpy.mockRestore();
|
|
782
|
-
}
|
|
847
|
+
it('returns null for a missing plan', async () => {
|
|
848
|
+
expect(await plansService.singlePlan(new ObjectId(), options)).toBeNull();
|
|
783
849
|
});
|
|
784
850
|
it('permits overlapping departments in one area while keeping shift operations bound to their plan', async () => {
|
|
785
851
|
const restaurant = await setupPlan();
|
|
@@ -831,5 +897,331 @@ describe('shift planning', () => {
|
|
|
831
897
|
}] }, options);
|
|
832
898
|
expect(await HistoryModel.db.countDocuments({ 'history.parent': shift._id })).toBe(count);
|
|
833
899
|
});
|
|
900
|
+
describe('read-only shift autofill', () => {
|
|
901
|
+
async function setupAutofill(changes = {}) {
|
|
902
|
+
const plan = await setupPlan();
|
|
903
|
+
const structureId = new ObjectId();
|
|
904
|
+
await StructureModel.db.insertOne({ _id: structureId, company: options.company, project: options.project,
|
|
905
|
+
model: 'listsParticipants', id: 's-boolean-preferred', type: 'boolean', question: 'Preferred' });
|
|
906
|
+
await ParticipantsModel.db.updateOne({ _id: secondParticipantId }, { $set: { 's-boolean-preferred': true } });
|
|
907
|
+
const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, {
|
|
908
|
+
assignments: [], requiredPeople: 1,
|
|
909
|
+
structures: [{ structure: structureId.toHexString(), grade: 'very-positive' }], ...changes,
|
|
910
|
+
}), options);
|
|
911
|
+
return { plan, shift, structureId };
|
|
912
|
+
}
|
|
913
|
+
it('ranks suggestions deterministically, batches rule reads and changes no stored documents', async () => {
|
|
914
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 2 });
|
|
915
|
+
const before = await Promise.all(models.map(async (model) => JSON.stringify(await model.db.find({}).sort({ _id: 1 }).toArray())));
|
|
916
|
+
const settingsRead = jest.spyOn(settingsService, 'singleSettings');
|
|
917
|
+
const absencesRead = jest.spyOn(absencesService, 'absencesForPlanning');
|
|
918
|
+
try {
|
|
919
|
+
const expected = { shift: shift._id, users: [secondParticipantId, participantId] };
|
|
920
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual(expected);
|
|
921
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual(expected);
|
|
922
|
+
expect(settingsRead).toHaveBeenCalledTimes(2);
|
|
923
|
+
expect(absencesRead).toHaveBeenCalledTimes(2);
|
|
924
|
+
const after = await Promise.all(models.map(async (model) => JSON.stringify(await model.db.find({}).sort({ _id: 1 }).toArray())));
|
|
925
|
+
expect(after).toEqual(before);
|
|
926
|
+
}
|
|
927
|
+
finally {
|
|
928
|
+
settingsRead.mockRestore();
|
|
929
|
+
absencesRead.mockRestore();
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
it('keeps current assignments and only fills the remaining places', async () => {
|
|
933
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 2, assignments: [{ participant: participantId }] });
|
|
934
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId, secondParticipantId] });
|
|
935
|
+
expect((await ShiftModel.db.findOne({ _id: shift._id })).assignments).toEqual(shift.assignments);
|
|
936
|
+
});
|
|
937
|
+
it('retains existing attention items without allowing new violations', async () => {
|
|
938
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 2, assignments: [{ participant: participantId }] });
|
|
939
|
+
await ParticipantsModel.db.updateOne({ _id: participantId }, { $set: { state: 'canceled' } });
|
|
940
|
+
await absencesService.createAbsence({ customer: customerId, type: 'sickness', start: `${day}T00:00:00+02:00`, end: `${nextDay}T00:00:00+02:00` }, options);
|
|
941
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId, secondParticipantId] });
|
|
942
|
+
});
|
|
943
|
+
it.each([undefined, 0, 1])('keeps an already assigned shift unchanged with target %p', async (requiredPeople) => {
|
|
944
|
+
const { plan, shift } = await setupAutofill({ requiredPeople, assignments: [{ participant: participantId }] });
|
|
945
|
+
const candidateRead = jest.spyOn(participantsService, 'planningParticipants');
|
|
946
|
+
try {
|
|
947
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
|
|
948
|
+
expect(candidateRead).not.toHaveBeenCalled();
|
|
949
|
+
}
|
|
950
|
+
finally {
|
|
951
|
+
candidateRead.mockRestore();
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
it('selects one list entry per person and returns a partial fill when the pool runs out', async () => {
|
|
955
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 3 });
|
|
956
|
+
const duplicateParticipantId = new ObjectId();
|
|
957
|
+
await ParticipantsModel.db.insertOne({ ...participant, _id: duplicateParticipantId, 's-boolean-preferred': true });
|
|
958
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId, duplicateParticipantId] });
|
|
959
|
+
});
|
|
960
|
+
it.each(['blocked', 'unchecked', 'notuseful', 'canceled'])('excludes a high-scoring %s person', async (state) => {
|
|
961
|
+
const { plan, shift } = await setupAutofill();
|
|
962
|
+
await ParticipantsModel.db.updateOne({ _id: secondParticipantId }, { $set: { state } });
|
|
963
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
|
|
964
|
+
});
|
|
965
|
+
it('ignores entries without a person in the company', async () => {
|
|
966
|
+
const { plan, shift } = await setupAutofill();
|
|
967
|
+
await CustomersModel.db.updateOne({ _id: secondCustomerId }, { $set: { company: 'foreign' } });
|
|
968
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
|
|
969
|
+
});
|
|
970
|
+
it('applies the plan default state and area-category binding together', async () => {
|
|
971
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 3 });
|
|
972
|
+
const eventsCategoryId = new ObjectId();
|
|
973
|
+
const reservedCustomerId = new ObjectId();
|
|
974
|
+
const reservedParticipantId = new ObjectId();
|
|
975
|
+
await ListsModel.db.updateOne({ _id: listId }, { $set: { categories: [
|
|
976
|
+
...list.categories, { _id: eventsCategoryId, id: 'events', name: 'Events' },
|
|
977
|
+
] } });
|
|
978
|
+
await ParticipantsModel.db.updateOne({ _id: secondParticipantId }, { $set: { state: 'reserved', category: 'events' } });
|
|
979
|
+
await CustomersModel.db.insertOne({ ...customer, _id: reservedCustomerId, id: 'reserved', accounts: [] });
|
|
980
|
+
await ParticipantsModel.db.insertOne({ ...participant, _id: reservedParticipantId, user: reservedCustomerId, state: 'reserved' });
|
|
981
|
+
await plansService.changePlan(plan._id, planInput(listId, { defaultParticipantState: 'reserved',
|
|
982
|
+
workAreaCategories: [{ area: housekeepingAreaId, categoryIds: [categoryId] }],
|
|
983
|
+
}), options);
|
|
984
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [reservedParticipantId] });
|
|
985
|
+
});
|
|
986
|
+
it('ignores matchingPeople and fills only to requiredPeople', async () => {
|
|
987
|
+
const { plan, shift, structureId } = await setupAutofill({ requiredPeople: 2, matchingPeople: 2 });
|
|
988
|
+
await ShiftModel.db.updateOne({ _id: shift._id }, { $set: { structures: [
|
|
989
|
+
{ structure: structureId.toHexString(), constraint: 'required', value: true, grade: 'very-negative' },
|
|
990
|
+
] } });
|
|
991
|
+
// matchingPeople is a coverage concern only; autofill uses the ranked pool up to requiredPeople.
|
|
992
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id,
|
|
993
|
+
users: [secondParticipantId, participantId],
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
it('counts a retained assignment outside the candidate filter', async () => {
|
|
997
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 2, assignments: [{ participant: secondParticipantId }] });
|
|
998
|
+
await ParticipantsModel.db.updateOne({ _id: participantId }, { $set: { state: 'reserved' } });
|
|
999
|
+
await plansService.changePlan(plan._id, planInput(listId, { defaultParticipantState: 'reserved' }), options);
|
|
1000
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId, participantId] });
|
|
1001
|
+
});
|
|
1002
|
+
it('does not reject matchingPeople above requiredPeople', async () => {
|
|
1003
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 1, matchingPeople: 2 });
|
|
1004
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId] });
|
|
1005
|
+
});
|
|
1006
|
+
it('skips an overlapping high-scoring person instead of double booking', async () => {
|
|
1007
|
+
const { plan, shift } = await setupAutofill();
|
|
1008
|
+
await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, { assignments: [{ participant: secondParticipantId }] }), options);
|
|
1009
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
|
|
1010
|
+
});
|
|
1011
|
+
it.each([true, false])('respects absence enforcement with plan override %p', async (absenceEnforcement) => {
|
|
1012
|
+
const { plan, shift } = await setupAutofill();
|
|
1013
|
+
await absencesService.createAbsence({ customer: secondCustomerId, type: 'sickness', start: `${day}T00:00:00+02:00`, end: `${nextDay}T00:00:00+02:00` }, options);
|
|
1014
|
+
await plansService.changePlan(plan._id, planInput(listId, { settingsOverrides: { absenceEnforcement } }), options);
|
|
1015
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id,
|
|
1016
|
+
users: [absenceEnforcement ? participantId : secondParticipantId],
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
it.each(['rest', 'dailyHours', 'consecutiveDays'])('enforces the general %s rule', async (rule) => {
|
|
1020
|
+
const { plan, shift } = await setupAutofill(rule === 'consecutiveDays'
|
|
1021
|
+
? { day: nextDay, start: `${nextDay}T08:00:00+02:00`, end: `${nextDay}T12:00:00+02:00` } : {});
|
|
1022
|
+
await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
|
|
1023
|
+
assignments: [{ participant: secondParticipantId }],
|
|
1024
|
+
...(rule === 'consecutiveDays' ? {} : { start: `${day}T00:00:00+02:00`, end: `${day}T04:00:00+02:00` }),
|
|
1025
|
+
}), options);
|
|
1026
|
+
const settings = policy();
|
|
1027
|
+
if (rule === 'rest')
|
|
1028
|
+
settings.minimumRest = { enabled: true, minutes: 660 };
|
|
1029
|
+
if (rule === 'dailyHours')
|
|
1030
|
+
settings.maximumDailyWorkMinutes = { enabled: true, minutes: 420 };
|
|
1031
|
+
if (rule === 'consecutiveDays')
|
|
1032
|
+
settings.maximumConsecutiveWorkingDays = { enabled: true, days: 1 };
|
|
1033
|
+
await settingsService.changeSettings({ settings }, options);
|
|
1034
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
|
|
1035
|
+
});
|
|
1036
|
+
it('uses a plan override to relax the general daily hours limit', async () => {
|
|
1037
|
+
const { plan, shift } = await setupAutofill();
|
|
1038
|
+
await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
|
|
1039
|
+
assignments: [{ participant: secondParticipantId }], start: `${day}T00:00:00+02:00`, end: `${day}T04:00:00+02:00`,
|
|
1040
|
+
}), options);
|
|
1041
|
+
const settings = policy();
|
|
1042
|
+
settings.maximumDailyWorkMinutes = { enabled: true, minutes: 420 };
|
|
1043
|
+
await settingsService.changeSettings({ settings }, options);
|
|
1044
|
+
await plansService.changePlan(plan._id, planInput(listId, { settingsOverrides: { maximumDailyWorkMinutes: { enabled: false, minutes: 420 } } }), options);
|
|
1045
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [secondParticipantId] });
|
|
1046
|
+
});
|
|
1047
|
+
it('protects a future shift governed by another project plan rest override', async () => {
|
|
1048
|
+
const { plan, shift } = await setupAutofill({ start: `${day}T20:00:00+02:00`, end: `${day}T23:00:00+02:00` });
|
|
1049
|
+
const otherListId = new ObjectId();
|
|
1050
|
+
const otherParticipantId = new ObjectId();
|
|
1051
|
+
const otherOptions = { ...options, project: 'other-project' };
|
|
1052
|
+
await settingsService.changeSettings({ settings: policy() }, otherOptions);
|
|
1053
|
+
await ListsModel.db.insertOne({ ...list, _id: otherListId, project: otherOptions.project });
|
|
1054
|
+
await ParticipantsModel.db.insertOne({ ...participant, _id: otherParticipantId, user: secondCustomerId, project: otherOptions.project, list: otherListId });
|
|
1055
|
+
const otherPlan = await otherProjectPlan(otherListId, otherOptions, { minimumRest: { enabled: true, minutes: 660 } });
|
|
1056
|
+
await shiftsService.createShift(otherPlan._id, shiftInput(otherPlan, planAreas(otherPlan)[0].area, {
|
|
1057
|
+
day: nextDay, assignments: [{ participant: otherParticipantId }],
|
|
1058
|
+
}), otherOptions);
|
|
1059
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
|
|
1060
|
+
});
|
|
1061
|
+
it('requires a valid shared break template when break rules demand it', async () => {
|
|
1062
|
+
const { plan, shift } = await setupAutofill({ end: `${day}T16:00:00+02:00` });
|
|
1063
|
+
const settings = policy();
|
|
1064
|
+
settings.breaks.enabled = true;
|
|
1065
|
+
await settingsService.changeSettings({ settings }, options);
|
|
1066
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [] });
|
|
1067
|
+
const breaks = [{ start: `${day}T12:00:00+02:00`, end: `${day}T12:30:00+02:00` }];
|
|
1068
|
+
const originalBreaks = JSON.stringify(breaks);
|
|
1069
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options, breaks)).toStrictEqual({ shift: shift._id, users: [secondParticipantId] });
|
|
1070
|
+
expect(JSON.stringify(breaks)).toBe(originalBreaks);
|
|
1071
|
+
expect((await ShiftModel.db.findOne({ _id: shift._id })).assignments).toEqual([]);
|
|
1072
|
+
await expect(shiftsService.autofillShift(plan._id, shift._id, options, [{ start: `${day}T07:00:00+02:00`, end: `${day}T08:00:00+02:00` }])).rejects.toThrow('VALIDATION_FAILED');
|
|
1073
|
+
});
|
|
1074
|
+
it('requires effective settings but accepts complete plan overrides without general settings', async () => {
|
|
1075
|
+
const created = await plansService.createPlan(planInput(), options);
|
|
1076
|
+
await createDays(created._id, options, [{ date: day, areas: areaLinks() }]);
|
|
1077
|
+
const plan = (await plansService.singlePlan(created._id, options));
|
|
1078
|
+
const shift = await shiftsService.createShift(plan._id, {
|
|
1079
|
+
day: dayId(plan, day), area: housekeepingAreaId, title: 'Empty', start: `${day}T08:00:00+02:00`, end: `${day}T12:00:00+02:00`,
|
|
1080
|
+
requiredPeople: 1, assignments: [],
|
|
1081
|
+
}, options);
|
|
1082
|
+
await expect(shiftsService.autofillShift(plan._id, shift._id, options)).rejects.toThrow('SHIFT_PLANNING_SETTINGS_NOT_CONFIGURED');
|
|
1083
|
+
await plansService.changePlan(plan._id, planInput(listId, { settingsOverrides: policy() }), options);
|
|
1084
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [participantId] });
|
|
1085
|
+
});
|
|
1086
|
+
it('uses the current plan list rather than a stale shift list', async () => {
|
|
1087
|
+
const { plan, shift } = await setupAutofill();
|
|
1088
|
+
await plansService.changePlan(plan._id, { ...planInput(), list: undefined }, options);
|
|
1089
|
+
expect(await shiftsService.autofillShift(plan._id, shift._id, options)).toStrictEqual({ shift: shift._id, users: [] });
|
|
1090
|
+
});
|
|
1091
|
+
it('does not read a foreign tenant or another plan shift', async () => {
|
|
1092
|
+
const { plan, shift } = await setupAutofill();
|
|
1093
|
+
await expect(shiftsService.autofillShift(plan._id, shift._id, { ...options, company: 'foreign' })).rejects.toThrow('SHIFT_PLAN_NOT_FOUND');
|
|
1094
|
+
await expect(shiftsService.autofillShift(plan._id, shift._id, { ...options, project: 'foreign' })).rejects.toThrow('SHIFT_PLAN_NOT_FOUND');
|
|
1095
|
+
await expect(shiftsService.autofillShift(plan._id, new ObjectId(), options)).rejects.toThrow('SHIFT_NOT_FOUND');
|
|
1096
|
+
const sibling = await setupPlan();
|
|
1097
|
+
await expect(shiftsService.autofillShift(sibling._id, shift._id, options)).rejects.toThrow('SHIFT_NOT_FOUND');
|
|
1098
|
+
});
|
|
1099
|
+
it('returns the resolved suggestion as a report without persisting it', async () => {
|
|
1100
|
+
const { plan, shift } = await setupAutofill({ requiredPeople: 2 });
|
|
1101
|
+
const before = await ShiftModel.db.findOne({ _id: shift._id });
|
|
1102
|
+
const report = await shiftsService.autofillShiftReport(plan._id, shift._id, options);
|
|
1103
|
+
const suggested = [secondParticipantId.toHexString(), participantId.toHexString()];
|
|
1104
|
+
expect(report._id.equals(shift._id)).toBe(true);
|
|
1105
|
+
expect((report.assignments || []).map((assignment) => assignment.participant.toHexString())).toEqual(suggested);
|
|
1106
|
+
expect(report.participants.map((participant) => participant.participant.toHexString())).toEqual(suggested);
|
|
1107
|
+
// all scheduling rules run, so a clean suggestion has no violations
|
|
1108
|
+
expect(report.violations).toEqual([]);
|
|
1109
|
+
expect(await ShiftModel.db.findOne({ _id: shift._id })).toEqual(before);
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
describe('scope shift autofill', () => {
|
|
1113
|
+
it('reuses preparation and reads the company conflict window only once', async () => {
|
|
1114
|
+
const plan = await setupPlan();
|
|
1115
|
+
await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
|
|
1116
|
+
const shiftReads = jest.spyOn(ShiftModel.db, 'find');
|
|
1117
|
+
const settingsReads = jest.spyOn(settingsService, 'singleSettings');
|
|
1118
|
+
const participantReads = jest.spyOn(ParticipantsModel.db, 'find');
|
|
1119
|
+
const customerReads = jest.spyOn(CustomersModel.db, 'find');
|
|
1120
|
+
const structureReads = jest.spyOn(StructureModel.db, 'find');
|
|
1121
|
+
const reports = await shiftsService.autofill({ level: 'plan', plan: plan._id }, options);
|
|
1122
|
+
expect(reports[0].assignments).toHaveLength(1);
|
|
1123
|
+
expect(shiftReads.mock.calls.filter(([query]) => query && 'assignments.customer' in query)).toHaveLength(1);
|
|
1124
|
+
expect(settingsReads).toHaveBeenCalledTimes(1);
|
|
1125
|
+
expect(participantReads).toHaveBeenCalledTimes(2);
|
|
1126
|
+
expect(customerReads).toHaveBeenCalledTimes(2);
|
|
1127
|
+
expect(structureReads).toHaveBeenCalledTimes(2);
|
|
1128
|
+
});
|
|
1129
|
+
it('does not expand the conflict window around distant neighbours', async () => {
|
|
1130
|
+
const plan = await setupPlan();
|
|
1131
|
+
const target = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
|
|
1132
|
+
const storedPlan = (await ShiftPlanModel.db.findOne({ _id: plan._id }));
|
|
1133
|
+
const storedShift = (await ShiftModel.db.findOne({ _id: target._id }));
|
|
1134
|
+
const neighbourPlan = new ObjectId();
|
|
1135
|
+
const distantPlan = new ObjectId();
|
|
1136
|
+
await ShiftPlanModel.db.insertMany([{ ...storedPlan, _id: neighbourPlan }, { ...storedPlan, _id: distantPlan }]);
|
|
1137
|
+
await ShiftModel.db.insertMany([
|
|
1138
|
+
{ ...storedShift, _id: new ObjectId(), plan: neighbourPlan, start: '2027-08-01T08:00:00.000Z', end: '2027-08-01T12:00:00.000Z',
|
|
1139
|
+
assignments: [{ participant: secondParticipantId, customer: secondCustomerId, breaks: [] }] },
|
|
1140
|
+
{ ...storedShift, _id: new ObjectId(), plan: distantPlan, start: '2028-05-01T08:00:00.000Z', end: '2028-05-01T12:00:00.000Z',
|
|
1141
|
+
assignments: [{ participant: secondParticipantId, customer: secondCustomerId, breaks: [] }] },
|
|
1142
|
+
]);
|
|
1143
|
+
const planReads = jest.spyOn(plansService, 'plansForCompany');
|
|
1144
|
+
const reports = await shiftsService.autofill({ level: 'plan', plan: plan._id }, options);
|
|
1145
|
+
expect(reports[0].assignments).toHaveLength(1);
|
|
1146
|
+
expect(reports[0].violations).toEqual([]);
|
|
1147
|
+
expect(planReads).toHaveBeenCalledTimes(1);
|
|
1148
|
+
expect(planReads.mock.calls[0][0].map((id) => id.toHexString())).toEqual([neighbourPlan.toHexString()]);
|
|
1149
|
+
});
|
|
1150
|
+
it('still protects a future shift governed by another project rest override', async () => {
|
|
1151
|
+
const plan = await setupPlan();
|
|
1152
|
+
const target = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, {
|
|
1153
|
+
assignments: [], requiredPeople: 2, start: `${day}T20:00:00+02:00`, end: `${day}T23:00:00+02:00`,
|
|
1154
|
+
}), options);
|
|
1155
|
+
const otherListId = new ObjectId();
|
|
1156
|
+
const otherParticipantId = new ObjectId();
|
|
1157
|
+
const otherOptions = { ...options, project: 'other-project' };
|
|
1158
|
+
await settingsService.changeSettings({ settings: policy() }, otherOptions);
|
|
1159
|
+
await ListsModel.db.insertOne({ ...list, _id: otherListId, project: otherOptions.project });
|
|
1160
|
+
await ParticipantsModel.db.insertOne({ ...participant, _id: otherParticipantId, user: secondCustomerId, project: otherOptions.project, list: otherListId });
|
|
1161
|
+
const otherPlan = await otherProjectPlan(otherListId, otherOptions, { minimumRest: { enabled: true, minutes: 660 } });
|
|
1162
|
+
await shiftsService.createShift(otherPlan._id, shiftInput(otherPlan, planAreas(otherPlan)[0].area, {
|
|
1163
|
+
day: nextDay, assignments: [{ participant: otherParticipantId }],
|
|
1164
|
+
}), otherOptions);
|
|
1165
|
+
const reports = await shiftsService.autofill({ level: 'shift', plan: plan._id, shift: target._id }, options);
|
|
1166
|
+
expect(reports[0].assignments.map((assignment) => assignment.participant.toHexString())).toEqual([participantId.toHexString()]);
|
|
1167
|
+
expect(reports[0].violations).toEqual([]);
|
|
1168
|
+
});
|
|
1169
|
+
it('fills every under-staffed shift of a plan without double booking a person', async () => {
|
|
1170
|
+
const plan = await setupPlan();
|
|
1171
|
+
const morning = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, {
|
|
1172
|
+
assignments: [], requiredPeople: 1, start: `${day}T08:00:00+02:00`, end: `${day}T16:00:00+02:00`,
|
|
1173
|
+
}), options);
|
|
1174
|
+
const evening = await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
|
|
1175
|
+
assignments: [], requiredPeople: 1, start: `${day}T12:00:00+02:00`, end: `${day}T20:00:00+02:00`,
|
|
1176
|
+
}), options);
|
|
1177
|
+
const reports = await shiftsService.autofill({ level: 'plan', plan: plan._id }, options);
|
|
1178
|
+
const byId = new Map(reports.map((report) => [report._id.toHexString(), report]));
|
|
1179
|
+
expect([...byId.keys()].sort()).toEqual([morning._id.toHexString(), evening._id.toHexString()].sort());
|
|
1180
|
+
const morningAssignee = byId.get(morning._id.toHexString()).assignments.map((assignment) => assignment.participant.toHexString());
|
|
1181
|
+
const eveningAssignee = byId.get(evening._id.toHexString()).assignments.map((assignment) => assignment.participant.toHexString());
|
|
1182
|
+
expect(morningAssignee).toHaveLength(1);
|
|
1183
|
+
expect(eveningAssignee).toHaveLength(1);
|
|
1184
|
+
// The shifts overlap, so the shared allocation cannot reuse the same person.
|
|
1185
|
+
expect(morningAssignee[0]).not.toBe(eveningAssignee[0]);
|
|
1186
|
+
});
|
|
1187
|
+
it('restricts a day scope to that day only', async () => {
|
|
1188
|
+
const plan = await setupPlan();
|
|
1189
|
+
const today = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
|
|
1190
|
+
const tomorrow = await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, {
|
|
1191
|
+
assignments: [], requiredPeople: 1, day: nextDay, start: `${nextDay}T08:00:00+02:00`, end: `${nextDay}T16:00:00+02:00`,
|
|
1192
|
+
}), options);
|
|
1193
|
+
const reports = await shiftsService.autofill({ level: 'day', plan: plan._id, day: dayId(plan, day) }, options);
|
|
1194
|
+
expect(reports.map((report) => report._id.toHexString())).toEqual([today._id.toHexString()]);
|
|
1195
|
+
expect((await ShiftModel.db.findOne({ _id: tomorrow._id })).assignments).toEqual([]);
|
|
1196
|
+
});
|
|
1197
|
+
it('restricts a work area scope to that area only', async () => {
|
|
1198
|
+
const plan = await setupPlan();
|
|
1199
|
+
const housekeeping = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
|
|
1200
|
+
const reception = await shiftsService.createShift(plan._id, shiftInput(plan, receptionAreaId, { assignments: [], requiredPeople: 1 }), options);
|
|
1201
|
+
const reports = await shiftsService.autofill({ level: 'workArea', plan: plan._id, workArea: housekeepingAreaId }, options);
|
|
1202
|
+
expect(reports.map((report) => report._id.toHexString())).toEqual([housekeeping._id.toHexString()]);
|
|
1203
|
+
expect((await ShiftModel.db.findOne({ _id: reception._id })).assignments).toEqual([]);
|
|
1204
|
+
});
|
|
1205
|
+
it('fills a single shift for a shift scope without persisting it', async () => {
|
|
1206
|
+
const plan = await setupPlan();
|
|
1207
|
+
const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
|
|
1208
|
+
const reports = await shiftsService.autofill({ level: 'shift', plan: plan._id, shift: shift._id }, options);
|
|
1209
|
+
expect(reports).toHaveLength(1);
|
|
1210
|
+
expect(reports[0]._id.equals(shift._id)).toBe(true);
|
|
1211
|
+
expect(reports[0].assignments).toHaveLength(1);
|
|
1212
|
+
expect(reports[0].violations).toEqual([]);
|
|
1213
|
+
expect((await ShiftModel.db.findOne({ _id: shift._id })).assignments).toEqual([]);
|
|
1214
|
+
});
|
|
1215
|
+
it('treats an overlapping shift in another plan as a conflict neighbour', async () => {
|
|
1216
|
+
const plan = await setupPlan();
|
|
1217
|
+
const shift = await shiftsService.createShift(plan._id, shiftInput(plan, housekeepingAreaId, { assignments: [], requiredPeople: 1 }), options);
|
|
1218
|
+
const sibling = await setupPlan();
|
|
1219
|
+
await shiftsService.createShift(sibling._id, shiftInput(sibling, housekeepingAreaId, {
|
|
1220
|
+
assignments: [{ participant: secondParticipantId }], start: `${day}T08:00:00+02:00`, end: `${day}T16:00:00+02:00`,
|
|
1221
|
+
}), options);
|
|
1222
|
+
const reports = await shiftsService.autofill({ level: 'shift', plan: plan._id, shift: shift._id }, options);
|
|
1223
|
+
expect(reports[0].assignments.map((assignment) => assignment.participant.toHexString())).toEqual([participantId.toHexString()]);
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
834
1226
|
});
|
|
835
1227
|
//# sourceMappingURL=shiftPlanning.spec.js.map
|