@lilaquadrat/studio 12.1.0 → 12.3.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/models.d.ts +8 -1
- package/lib/models.js +8 -1
- package/lib/models.js.map +1 -1
- package/lib/services.d.ts +8 -1
- package/lib/services.js +8 -1
- package/lib/services.js.map +1 -1
- package/lib/src/Immutable.class.js +10 -4
- package/lib/src/Immutable.class.js.map +1 -1
- package/lib/src/aggegations/structures.aggregation.d.ts +6 -8
- package/lib/src/aggegations/structures.aggregation.js +3 -2
- package/lib/src/aggegations/structures.aggregation.js.map +1 -1
- package/lib/src/classes/paymentProviders/PaymentProviderFactory.class.d.ts +4 -0
- package/lib/src/classes/paymentProviders/PaymentProviderFactory.class.js +8 -0
- package/lib/src/classes/paymentProviders/PaymentProviderFactory.class.js.map +1 -0
- package/lib/src/classes/paymentProviders/StripePaymentProvider.class.d.ts +9 -0
- package/lib/src/classes/paymentProviders/StripePaymentProvider.class.js +49 -0
- package/lib/src/classes/paymentProviders/StripePaymentProvider.class.js.map +1 -0
- package/lib/src/classes/shiftPlanningModel.class.d.ts +6 -0
- package/lib/src/classes/shiftPlanningModel.class.js +26 -0
- package/lib/src/classes/shiftPlanningModel.class.js.map +1 -0
- package/lib/src/helpers/buildProductName.d.ts +7 -0
- package/lib/src/helpers/buildProductName.js +20 -0
- package/lib/src/helpers/buildProductName.js.map +1 -0
- 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/shiftPlanningReports.d.ts +7 -0
- package/lib/src/helpers/shiftPlanningReports.js +135 -0
- package/lib/src/helpers/shiftPlanningReports.js.map +1 -0
- package/lib/src/helpers/shiftPlanningSchemas.d.ts +143 -0
- package/lib/src/helpers/shiftPlanningSchemas.js +87 -0
- package/lib/src/helpers/shiftPlanningSchemas.js.map +1 -0
- package/lib/src/helpers/shiftPlanningTime.d.ts +24 -0
- package/lib/src/helpers/shiftPlanningTime.js +215 -0
- package/lib/src/helpers/shiftPlanningTime.js.map +1 -0
- package/lib/src/helpers/shiftPlanningValidation.d.ts +32 -0
- package/lib/src/helpers/shiftPlanningValidation.js +69 -0
- package/lib/src/helpers/shiftPlanningValidation.js.map +1 -0
- package/lib/src/helpers/structureMatch.d.ts +15 -0
- package/lib/src/helpers/structureMatch.js +72 -0
- package/lib/src/helpers/structureMatch.js.map +1 -0
- 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/PaymentProviderBase.d.ts +14 -0
- package/lib/src/interfaces/PaymentProviderBase.js +2 -0
- package/lib/src/interfaces/PaymentProviderBase.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/customers.model.js +10 -0
- package/lib/src/models/customers.model.js.map +1 -1
- package/lib/src/models/employeeAbsence.model.d.ts +4 -0
- package/lib/src/models/employeeAbsence.model.js +11 -0
- package/lib/src/models/employeeAbsence.model.js.map +1 -0
- package/lib/src/models/historySeen.model.d.ts +4 -0
- package/lib/src/models/historySeen.model.js +30 -0
- package/lib/src/models/historySeen.model.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/models/listsParticipants.model.js +11 -0
- package/lib/src/models/listsParticipants.model.js.map +1 -1
- package/lib/src/models/permissions.model.js +28 -0
- package/lib/src/models/permissions.model.js.map +1 -1
- package/lib/src/models/shift.model.d.ts +4 -0
- package/lib/src/models/shift.model.js +17 -0
- package/lib/src/models/shift.model.js.map +1 -0
- package/lib/src/models/shiftPlan.model.d.ts +4 -0
- package/lib/src/models/shiftPlan.model.js +14 -0
- package/lib/src/models/shiftPlan.model.js.map +1 -0
- package/lib/src/models/shiftPlanDays.model.d.ts +4 -0
- package/lib/src/models/shiftPlanDays.model.js +15 -0
- package/lib/src/models/shiftPlanDays.model.js.map +1 -0
- package/lib/src/models/shiftPlanningSettings.model.d.ts +4 -0
- package/lib/src/models/shiftPlanningSettings.model.js +10 -0
- package/lib/src/models/shiftPlanningSettings.model.js.map +1 -0
- package/lib/src/models/workAreas.model.d.ts +4 -0
- package/lib/src/models/workAreas.model.js +12 -0
- package/lib/src/models/workAreas.model.js.map +1 -0
- package/lib/src/services/customers.service.d.ts +12 -1
- package/lib/src/services/customers.service.js +27 -0
- package/lib/src/services/customers.service.js.map +1 -1
- package/lib/src/services/employeeAbsences.service.d.ts +18 -0
- package/lib/src/services/employeeAbsences.service.js +57 -0
- package/lib/src/services/employeeAbsences.service.js.map +1 -0
- package/lib/src/services/history.service.d.ts +15 -0
- package/lib/src/services/history.service.js +36 -0
- package/lib/src/services/history.service.js.map +1 -1
- package/lib/src/services/historySeen.service.d.ts +32 -0
- package/lib/src/services/historySeen.service.js +66 -0
- package/lib/src/services/historySeen.service.js.map +1 -0
- package/lib/src/services/listParticipants.service.d.ts +180 -12
- package/lib/src/services/listParticipants.service.js +426 -6
- package/lib/src/services/listParticipants.service.js.map +1 -1
- package/lib/src/services/lists.service.d.ts +9 -0
- package/lib/src/services/lists.service.js +51 -0
- package/lib/src/services/lists.service.js.map +1 -1
- package/lib/src/services/paymentProviders.service.d.ts +5 -0
- package/lib/src/services/paymentProviders.service.js +10 -0
- package/lib/src/services/paymentProviders.service.js.map +1 -1
- package/lib/src/services/shiftPlanDays.service.d.ts +22 -0
- package/lib/src/services/shiftPlanDays.service.js +80 -0
- package/lib/src/services/shiftPlanDays.service.js.map +1 -0
- package/lib/src/services/shiftPlanningSettings.service.d.ts +11 -0
- package/lib/src/services/shiftPlanningSettings.service.js +27 -0
- package/lib/src/services/shiftPlanningSettings.service.js.map +1 -0
- package/lib/src/services/shiftPlans.service.d.ts +24 -0
- package/lib/src/services/shiftPlans.service.js +139 -0
- package/lib/src/services/shiftPlans.service.js.map +1 -0
- package/lib/src/services/shifts.service.d.ts +46 -0
- package/lib/src/services/shifts.service.js +405 -0
- package/lib/src/services/shifts.service.js.map +1 -0
- package/lib/src/services/structures.service.d.ts +2 -0
- package/lib/src/services/structures.service.js +7 -0
- package/lib/src/services/structures.service.js.map +1 -1
- package/lib/src/services/workAreas.service.d.ts +18 -0
- package/lib/src/services/workAreas.service.js +67 -0
- package/lib/src/services/workAreas.service.js.map +1 -0
- package/lib/tests/buildProductName.spec.d.ts +1 -0
- package/lib/tests/buildProductName.spec.js +27 -0
- package/lib/tests/buildProductName.spec.js.map +1 -0
- package/lib/tests/customersServiceStructures.spec.d.ts +1 -0
- package/lib/tests/customersServiceStructures.spec.js +146 -0
- package/lib/tests/customersServiceStructures.spec.js.map +1 -0
- package/lib/tests/historySeenService.spec.d.ts +1 -0
- package/lib/tests/historySeenService.spec.js +159 -0
- package/lib/tests/historySeenService.spec.js.map +1 -0
- package/lib/tests/listParticipantsPlanning.spec.d.ts +1 -0
- package/lib/tests/listParticipantsPlanning.spec.js +479 -0
- package/lib/tests/listParticipantsPlanning.spec.js.map +1 -0
- package/lib/tests/listParticipantsServiceStructures.spec.d.ts +1 -0
- package/lib/tests/listParticipantsServiceStructures.spec.js +176 -0
- package/lib/tests/listParticipantsServiceStructures.spec.js.map +1 -0
- package/lib/tests/listsServiceCreateExternalProducts.spec.d.ts +1 -0
- package/lib/tests/listsServiceCreateExternalProducts.spec.js +91 -0
- package/lib/tests/listsServiceCreateExternalProducts.spec.js.map +1 -0
- package/lib/tests/paymentProviders.spec.d.ts +1 -0
- package/lib/tests/paymentProviders.spec.js +119 -0
- package/lib/tests/paymentProviders.spec.js.map +1 -0
- package/lib/tests/paymentProvidersServiceGetProduct.spec.d.ts +1 -0
- package/lib/tests/paymentProvidersServiceGetProduct.spec.js +33 -0
- package/lib/tests/paymentProvidersServiceGetProduct.spec.js.map +1 -0
- 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.d.ts +1 -0
- package/lib/tests/shiftPlanning.spec.js +1162 -0
- package/lib/tests/shiftPlanning.spec.js.map +1 -0
- 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/structureMatch.spec.d.ts +1 -0
- package/lib/tests/structureMatch.spec.js +106 -0
- package/lib/tests/structureMatch.spec.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ObjectId, WithId } from 'mongodb';
|
|
2
|
+
import type { ShiftPlan, ShiftPlanChangeInput, ShiftPlanCreateInput, ShiftPlanFilters, ShiftPlanResponse, ShiftPlanningPage, ShiftPlanningProjectOptions, ShiftPlanningCompanyOptions } from '@lilaquadrat/interfaces';
|
|
3
|
+
import Immutable from '../Immutable.class.js';
|
|
4
|
+
/** A plan is the top-level scheduling unit; its employee list is an optional, reusable reference. Days are separate documents. */
|
|
5
|
+
export declare class ShiftPlansService extends Immutable<ShiftPlan> {
|
|
6
|
+
model: import("../classes/shiftPlanningModel.class.js").default<ShiftPlan>;
|
|
7
|
+
private employeeList;
|
|
8
|
+
/** One binding per work area, each category must belong to the linked employee list. */
|
|
9
|
+
private workAreaCategories;
|
|
10
|
+
private configuration;
|
|
11
|
+
/** Name/date overlap is intentional: departments and successive weeks may reuse the same pool. */
|
|
12
|
+
createPlan(input: ShiftPlanCreateInput, options: ShiftPlanningProjectOptions): Promise<ShiftPlanResponse>;
|
|
13
|
+
singlePlan(plan: ObjectId, options: ShiftPlanningProjectOptions): Promise<ShiftPlanResponse | null>;
|
|
14
|
+
changePlan(plan: ObjectId, input: ShiftPlanChangeInput, options: ShiftPlanningProjectOptions): Promise<ShiftPlanResponse>;
|
|
15
|
+
listPlans(filters: ShiftPlanFilters, options: ShiftPlanningProjectOptions): Promise<ShiftPlanningPage<ShiftPlanResponse>>;
|
|
16
|
+
planForScheduling(plan: ObjectId, options: ShiftPlanningProjectOptions): Promise<WithId<ShiftPlan>>;
|
|
17
|
+
planForScheduling(plan: ObjectId, options: ShiftPlanningProjectOptions, required: false): Promise<WithId<ShiftPlan> | null>;
|
|
18
|
+
plansForProject(options: ShiftPlanningProjectOptions, list?: ObjectId): Promise<WithId<ShiftPlan>[]>;
|
|
19
|
+
/** Labor checks may span projects; callers return only diagnostics, never these other plans' details. */
|
|
20
|
+
plansForCompany(plans: ObjectId[], options: ShiftPlanningCompanyOptions): Promise<WithId<ShiftPlan>[]>;
|
|
21
|
+
private responses;
|
|
22
|
+
}
|
|
23
|
+
declare const _default: ShiftPlansService;
|
|
24
|
+
export default _default;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import Immutable from '../Immutable.class.js';
|
|
2
|
+
import ImmutableError from '../classes/errors/Immutable.error.js';
|
|
3
|
+
import PlanModel from '../models/shiftPlan.model.js';
|
|
4
|
+
import ListsService from './lists.service.js';
|
|
5
|
+
import WorkAreasService from './workAreas.service.js';
|
|
6
|
+
import ShiftPlanDaysService from './shiftPlanDays.service.js';
|
|
7
|
+
import ShiftsService from './shifts.service.js';
|
|
8
|
+
import SettingsService from './shiftPlanningSettings.service.js';
|
|
9
|
+
import { effectivePolicy, localFormatter, normalizeDate, planningValidation, validatePolicy } from '../helpers/shiftPlanningTime.js';
|
|
10
|
+
import { assertUniquePlanningIds, planningPage, planningPagination, planningSearch } from '../helpers/shiftPlanningValidation.js';
|
|
11
|
+
/** A plan is the top-level scheduling unit; its employee list is an optional, reusable reference. Days are separate documents. */
|
|
12
|
+
export class ShiftPlansService extends Immutable {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.model = PlanModel;
|
|
16
|
+
}
|
|
17
|
+
async employeeList(list, options) {
|
|
18
|
+
if (!list)
|
|
19
|
+
return undefined;
|
|
20
|
+
const result = await ListsService.singleForPlanning(list, options);
|
|
21
|
+
if (!result)
|
|
22
|
+
throw new ImmutableError('LIST_NOT_FOUND');
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
/** One binding per work area, each category must belong to the linked employee list. */
|
|
26
|
+
async workAreaCategories(input, list, options) {
|
|
27
|
+
if (input.workAreaCategories === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const entries = input.workAreaCategories.filter((entry) => (entry.categoryIds ?? []).length);
|
|
30
|
+
if (!entries.length)
|
|
31
|
+
return [];
|
|
32
|
+
const areaIds = entries.map((entry) => entry.area);
|
|
33
|
+
assertUniquePlanningIds(areaIds.map((area) => area.toHexString()), '/workAreaCategories');
|
|
34
|
+
const areas = await WorkAreasService.areasForPlanning(areaIds, options);
|
|
35
|
+
if (areas.length !== new Set(areaIds.map((area) => area.toHexString())).size)
|
|
36
|
+
planningValidation('/workAreaCategories', 'work areas must exist in this project');
|
|
37
|
+
const listCategoryIds = new Set((list?.categories ?? []).map((category) => category._id.toHexString()));
|
|
38
|
+
entries.forEach((entry) => {
|
|
39
|
+
const ids = entry.categoryIds ?? [];
|
|
40
|
+
assertUniquePlanningIds(ids.map((category) => category.toHexString()), '/workAreaCategories/categoryIds');
|
|
41
|
+
if (!list)
|
|
42
|
+
planningValidation('/workAreaCategories/categoryIds', 'categories require a linked employee list');
|
|
43
|
+
if (ids.some((category) => !listCategoryIds.has(category.toHexString())))
|
|
44
|
+
planningValidation('/workAreaCategories/categoryIds', 'categories must belong to this employee list');
|
|
45
|
+
});
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
async configuration(input, list, options) {
|
|
49
|
+
localFormatter(input.timeZone);
|
|
50
|
+
const settings = await SettingsService.singleSettings(options);
|
|
51
|
+
const overrides = input.settingsOverrides ?? {};
|
|
52
|
+
validatePolicy({ ...settings?.settings, ...overrides });
|
|
53
|
+
const workAreaCategories = await this.workAreaCategories(input, list, options);
|
|
54
|
+
if (input.workHours !== undefined && input.workHours.start >= input.workHours.end)
|
|
55
|
+
planningValidation('/workHours/end', 'end must be after start');
|
|
56
|
+
return {
|
|
57
|
+
name: input.name, timeZone: input.timeZone, settingsOverrides: overrides,
|
|
58
|
+
...(workAreaCategories !== undefined ? { workAreaCategories } : {}),
|
|
59
|
+
...(input.defaultParticipantState !== undefined ? { defaultParticipantState: input.defaultParticipantState } : {}),
|
|
60
|
+
...(input.workHours !== undefined ? { workHours: input.workHours } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Name/date overlap is intentional: departments and successive weeks may reuse the same pool. */
|
|
64
|
+
async createPlan(input, options) {
|
|
65
|
+
const { list } = input;
|
|
66
|
+
const listDocument = await this.employeeList(list, options);
|
|
67
|
+
const configuration = await this.configuration(input, listDocument, options);
|
|
68
|
+
const created = await this.create({ company: options.company, project: options.project, ...(list ? { list } : {}), ...configuration }, options.user, options.app);
|
|
69
|
+
return (await this.singlePlan(created._id, options));
|
|
70
|
+
}
|
|
71
|
+
async singlePlan(plan, options) {
|
|
72
|
+
const document = await this.planForScheduling(plan, options, false);
|
|
73
|
+
if (!document)
|
|
74
|
+
return null;
|
|
75
|
+
const response = (await this.responses([document], options))[0];
|
|
76
|
+
// The plan detail is edited as a whole, so one request carries its schedule instead of a separate shifts list.
|
|
77
|
+
const shifts = await ShiftsService.listShifts(plan, {}, options);
|
|
78
|
+
return { ...response, shifts: shifts.data };
|
|
79
|
+
}
|
|
80
|
+
async changePlan(plan, input, options) {
|
|
81
|
+
await this.planForScheduling(plan, options);
|
|
82
|
+
const { list } = input;
|
|
83
|
+
const listDocument = await this.employeeList(list, options);
|
|
84
|
+
const configuration = await this.configuration(input, listDocument, options);
|
|
85
|
+
await ShiftsService.assertPlanTimeZone(plan, configuration.timeZone, options);
|
|
86
|
+
await this.immutable({ _id: plan, company: options.company, project: options.project }, { ...configuration, ...(list ? { list } : {}) }, options.user, 'change', options.app);
|
|
87
|
+
return (await this.singlePlan(plan, options));
|
|
88
|
+
}
|
|
89
|
+
async listPlans(filters, options) {
|
|
90
|
+
const from = filters.from ? normalizeDate(filters.from) : undefined;
|
|
91
|
+
const to = filters.to ? normalizeDate(filters.to) : undefined;
|
|
92
|
+
if (from && to && from >= to)
|
|
93
|
+
planningValidation('/to', 'must be after from');
|
|
94
|
+
const query = { company: options.company, project: options.project };
|
|
95
|
+
if (filters.list)
|
|
96
|
+
query.list = filters.list;
|
|
97
|
+
const search = planningSearch(filters.search);
|
|
98
|
+
if (search)
|
|
99
|
+
query.name = { $regex: search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' };
|
|
100
|
+
if (from || to) {
|
|
101
|
+
const plans = await ShiftPlanDaysService.plansInRange(from, to, options);
|
|
102
|
+
query._id = { $in: plans };
|
|
103
|
+
}
|
|
104
|
+
const { site, limit, skip } = planningPagination(filters);
|
|
105
|
+
const all = await this.model.db.countDocuments(query);
|
|
106
|
+
const plans = await this.model.db.find(query).sort({ name: 1, _id: 1 }).skip(skip).limit(limit).toArray();
|
|
107
|
+
return planningPage(await this.responses(plans, options), all, site, limit);
|
|
108
|
+
}
|
|
109
|
+
async planForScheduling(plan, options, required = true) {
|
|
110
|
+
const document = await this.model.db.findOne({ _id: plan, company: options.company, project: options.project });
|
|
111
|
+
if (!document && required)
|
|
112
|
+
throw new ImmutableError('SHIFT_PLAN_NOT_FOUND');
|
|
113
|
+
return document;
|
|
114
|
+
}
|
|
115
|
+
plansForProject(options, list) {
|
|
116
|
+
return this.model.db.find({ company: options.company, project: options.project, ...(list ? { list } : {}) }).toArray();
|
|
117
|
+
}
|
|
118
|
+
/** Labor checks may span projects; callers return only diagnostics, never these other plans' details. */
|
|
119
|
+
plansForCompany(plans, options) {
|
|
120
|
+
return this.model.db.find({ company: options.company, _id: { $in: plans } }).toArray();
|
|
121
|
+
}
|
|
122
|
+
async responses(plans, options) {
|
|
123
|
+
const settings = await SettingsService.singleSettings(options);
|
|
124
|
+
const days = await ShiftPlanDaysService.daysForPlans(plans.map((plan) => plan._id), options);
|
|
125
|
+
const areas = await WorkAreasService.areasForPlanning(days.flatMap((day) => (day.areas ?? []).map((link) => link.area)), options);
|
|
126
|
+
const listIds = plans.map((plan) => plan.list).filter((list) => !!list);
|
|
127
|
+
const lists = listIds.length ? await ListsService.listsForPlanning(listIds, options) : [];
|
|
128
|
+
return plans.map((plan) => {
|
|
129
|
+
const planDays = days.filter((day) => day.plan.equals(plan._id));
|
|
130
|
+
const list = plan.list ? lists.find((document) => document._id.equals(plan.list)) : undefined;
|
|
131
|
+
return { ...plan, days: planDays, effectiveSettings: effectivePolicy(settings?.settings, plan.settingsOverrides),
|
|
132
|
+
workAreas: areas.filter((area) => planDays.some((day) => (day.areas ?? []).some((link) => link.area.equals(area._id)))),
|
|
133
|
+
...(list?.categories?.length ? { categories: list.categories } : {}),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export default new ShiftPlansService();
|
|
139
|
+
//# sourceMappingURL=shiftPlans.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shiftPlans.service.js","sourceRoot":"","sources":["../../../src/services/shiftPlans.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ObjectId, WithId } from 'mongodb';
|
|
2
|
+
import type { MemberShift, MemberShiftFilters, PlanningScope, Shift, ShiftAssignmentBreak, ShiftAssignmentsInput, ShiftCreateInput, ShiftInput, ShiftPlanDay, ShiftPlanningFilters, ShiftPlanningPage, ShiftResponse, ShiftPlanningProjectOptions } from '@lilaquadrat/interfaces';
|
|
3
|
+
import Immutable from '../Immutable.class.js';
|
|
4
|
+
import type { ShiftAutofillResult } from '../interfaces/ShiftAutofillResult.js';
|
|
5
|
+
/** Scheduling owns employee allocation; work areas and plans only supply reusable identity/configuration. */
|
|
6
|
+
export declare class ShiftsService extends Immutable<Shift> {
|
|
7
|
+
model: import("../classes/shiftPlanningModel.class.js").default<Shift>;
|
|
8
|
+
private normalizedTime;
|
|
9
|
+
private document;
|
|
10
|
+
private resolvedAssignments;
|
|
11
|
+
createShift(plan: ObjectId, input: ShiftCreateInput, options: ShiftPlanningProjectOptions): Promise<ShiftResponse>;
|
|
12
|
+
singleShift(plan: ObjectId, shift: ObjectId, options: ShiftPlanningProjectOptions): Promise<ShiftResponse | null>;
|
|
13
|
+
listShifts(plan: ObjectId, filters: ShiftPlanningFilters, options: ShiftPlanningProjectOptions): Promise<ShiftPlanningPage<ShiftResponse>>;
|
|
14
|
+
/** Absolute break times survive edits; moving a shift outside them must be resolved explicitly by the planner. */
|
|
15
|
+
changeShift(plan: ObjectId, shift: ObjectId, input: ShiftInput, options: ShiftPlanningProjectOptions): Promise<ShiftResponse>;
|
|
16
|
+
removeShift(plan: ObjectId, shift: ObjectId, options: ShiftPlanningProjectOptions): Promise<void>;
|
|
17
|
+
replaceAssignments(plan: ObjectId, shift: ObjectId, input: ShiftAssignmentsInput, options: ShiftPlanningProjectOptions): Promise<ShiftResponse>;
|
|
18
|
+
/** Suggestions use the manual assignment rules without applying the proposed allocation. */
|
|
19
|
+
autofillShift(plan: ObjectId, shift: ObjectId, options: ShiftPlanningProjectOptions, breaks?: ShiftAssignmentBreak[]): Promise<ShiftAutofillResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Same allocation and rules as autofillShift, but resolves the suggestion into assignments and
|
|
22
|
+
* returns the recomputed shift, so a caller can apply it without a second round trip. Nothing is
|
|
23
|
+
* persisted; applying the result still goes through the normal assignment validation.
|
|
24
|
+
*/
|
|
25
|
+
autofillShiftReport(plan: ObjectId, shift: ObjectId, options: ShiftPlanningProjectOptions, breaks?: ShiftAssignmentBreak[]): Promise<ShiftResponse>;
|
|
26
|
+
/**
|
|
27
|
+
* Rule-checked suggestions for every under-staffed shift in a plan, day, work area or single shift.
|
|
28
|
+
* Shifts are filled greedily in start order against one shared allocation, so a person already
|
|
29
|
+
* picked for a colliding shift is not picked again; other plans act only as conflict neighbours.
|
|
30
|
+
* Nothing is persisted; applying a result still goes through the normal assignment validation.
|
|
31
|
+
*/
|
|
32
|
+
autofill(scope: PlanningScope, options: ShiftPlanningProjectOptions, breaks?: ShiftAssignmentBreak[]): Promise<ShiftResponse[]>;
|
|
33
|
+
hasWorkAreaAssignments(area: ObjectId, options: ShiftPlanningProjectOptions): Promise<boolean>;
|
|
34
|
+
/** A plan cannot change its time zone while existing shifts would no longer match their day's local date. */
|
|
35
|
+
assertPlanTimeZone(plan: ObjectId, timeZone: string, options: ShiftPlanningProjectOptions): Promise<void>;
|
|
36
|
+
hasDayShifts(day: ObjectId, options: ShiftPlanningProjectOptions): Promise<boolean>;
|
|
37
|
+
/** A day cannot drop an area or change its date while shifts still reference it. */
|
|
38
|
+
assertDayReferences(previous: WithId<ShiftPlanDay>, configuration: Pick<ShiftPlanDay, 'date' | 'areas'>, options: ShiftPlanningProjectOptions): Promise<void>;
|
|
39
|
+
myShifts(customer: ObjectId, filters: MemberShiftFilters, options: ShiftPlanningProjectOptions): Promise<ShiftPlanningPage<MemberShift>>;
|
|
40
|
+
private reportContext;
|
|
41
|
+
private reports;
|
|
42
|
+
private scheduleNeighbors;
|
|
43
|
+
private validateSchedule;
|
|
44
|
+
}
|
|
45
|
+
declare const _default: ShiftsService;
|
|
46
|
+
export default _default;
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb';
|
|
2
|
+
import Immutable from '../Immutable.class.js';
|
|
3
|
+
import ImmutableError from '../classes/errors/Immutable.error.js';
|
|
4
|
+
import ShiftModel from '../models/shift.model.js';
|
|
5
|
+
import ShiftPlansService from './shiftPlans.service.js';
|
|
6
|
+
import ShiftPlanDaysService from './shiftPlanDays.service.js';
|
|
7
|
+
import WorkAreasService from './workAreas.service.js';
|
|
8
|
+
import SettingsService from './shiftPlanningSettings.service.js';
|
|
9
|
+
import AbsencesService from './employeeAbsences.service.js';
|
|
10
|
+
import ListsService from './lists.service.js';
|
|
11
|
+
import ParticipantsService from './listParticipants.service.js';
|
|
12
|
+
import CustomersService from './customers.service.js';
|
|
13
|
+
import { effectivePolicy, interval, localDate, maximumShiftMilliseconds, normalizeBreaks, planningValidation } from '../helpers/shiftPlanningTime.js';
|
|
14
|
+
import { assertUniquePlanningIds, equalAssignmentSets, equalBreaks, newScheduleViolations, planningIntervalFilter, planningPage, planningPagination, } from '../helpers/shiftPlanningValidation.js';
|
|
15
|
+
import { allocateShiftFills } from '../helpers/shiftPlanAllocation.js';
|
|
16
|
+
import { boundedPlanningInstant, reportShift, scheduleWindowMilliseconds, unavailableParticipantStates } from '../helpers/shiftPlanningReports.js';
|
|
17
|
+
/** Scheduling owns employee allocation; work areas and plans only supply reusable identity/configuration. */
|
|
18
|
+
export class ShiftsService extends Immutable {
|
|
19
|
+
constructor() {
|
|
20
|
+
super(...arguments);
|
|
21
|
+
this.model = ShiftModel;
|
|
22
|
+
}
|
|
23
|
+
normalizedTime(input, plan, day) {
|
|
24
|
+
const normalized = { ...input, ...interval(input.start, input.end) };
|
|
25
|
+
if (Date.parse(normalized.end) - Date.parse(normalized.start) > maximumShiftMilliseconds)
|
|
26
|
+
planningValidation('/end', 'a shift must not exceed seven days');
|
|
27
|
+
if (localDate(normalized.start, plan.timeZone) !== day.date) {
|
|
28
|
+
planningValidation('/day', 'must match the local start date of its day');
|
|
29
|
+
}
|
|
30
|
+
if (!(day.areas ?? []).some((link) => link.area.equals(input.area))) {
|
|
31
|
+
planningValidation('/area', 'must be a work area linked to this day');
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
async document(plan, shift, options) {
|
|
36
|
+
await ShiftPlansService.planForScheduling(plan, options);
|
|
37
|
+
const document = await this.model.db.findOne({ _id: shift, company: options.company, project: options.project, plan });
|
|
38
|
+
if (!document)
|
|
39
|
+
throw new ImmutableError('SHIFT_NOT_FOUND');
|
|
40
|
+
return document;
|
|
41
|
+
}
|
|
42
|
+
async resolvedAssignments(inputs, shift, options, previous) {
|
|
43
|
+
assertUniquePlanningIds(inputs.map((entry) => entry.participant.toHexString()), '/assignments');
|
|
44
|
+
const { list } = shift;
|
|
45
|
+
if (inputs.length && !list)
|
|
46
|
+
planningValidation('/assignments', 'assignments require a linked employee list');
|
|
47
|
+
const assignments = [];
|
|
48
|
+
for (const input of inputs) {
|
|
49
|
+
const breaks = (input.breaks ?? []).map((entry) => interval(entry.start, entry.end));
|
|
50
|
+
const existing = previous?.assignments.find((entry) => entry.participant.equals(input.participant));
|
|
51
|
+
// Retain the original customer anchor when the list entry has since been canceled or removed.
|
|
52
|
+
if (existing && equalBreaks(existing.breaks, breaks)) {
|
|
53
|
+
assignments.push(normalizeBreaks(shift, existing));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const participant = await ParticipantsService.singleForPlanning(list, input.participant, options);
|
|
57
|
+
if (!participant || unavailableParticipantStates.has(participant.state))
|
|
58
|
+
planningValidation('/assignments/participant', 'participant is not available in this list');
|
|
59
|
+
const customer = (await CustomersService.peopleForPlanning([participant.user], options.company))[0];
|
|
60
|
+
if (!customer)
|
|
61
|
+
planningValidation('/assignments/participant', 'participant must identify a person in this company');
|
|
62
|
+
assignments.push(normalizeBreaks(shift, { participant: input.participant, customer: customer._id, breaks }));
|
|
63
|
+
}
|
|
64
|
+
// Several list entries can represent one person; participant identity alone cannot prevent double allocation.
|
|
65
|
+
assertUniquePlanningIds(assignments.map((entry) => entry.customer.toHexString()), '/assignments');
|
|
66
|
+
return assignments;
|
|
67
|
+
}
|
|
68
|
+
async createShift(plan, input, options) {
|
|
69
|
+
const configuration = await ShiftPlansService.planForScheduling(plan, options);
|
|
70
|
+
const day = await ShiftPlanDaysService.dayForScheduling(plan, input.day, options);
|
|
71
|
+
const list = configuration.list;
|
|
72
|
+
const { assignments, ...time } = input;
|
|
73
|
+
const candidate = { ...this.normalizedTime(time, configuration, day), _id: new ObjectId(),
|
|
74
|
+
company: options.company, project: options.project, plan, ...(list ? { list } : {}), assignments: [],
|
|
75
|
+
};
|
|
76
|
+
candidate.assignments = await this.resolvedAssignments(assignments ?? [], candidate, options);
|
|
77
|
+
await this.validateSchedule(candidate, options);
|
|
78
|
+
const { _id, ...data } = candidate;
|
|
79
|
+
const created = await this.create(data, options.user, options.app);
|
|
80
|
+
return (await this.singleShift(plan, created._id, options));
|
|
81
|
+
}
|
|
82
|
+
async singleShift(plan, shift, options) {
|
|
83
|
+
await ShiftPlansService.planForScheduling(plan, options);
|
|
84
|
+
const document = await this.model.db.findOne({ _id: shift, company: options.company, project: options.project, plan });
|
|
85
|
+
return document ? (await this.reports([document], options, undefined, undefined, plan))[0] : null;
|
|
86
|
+
}
|
|
87
|
+
async listShifts(plan, filters, options) {
|
|
88
|
+
await ShiftPlansService.planForScheduling(plan, options);
|
|
89
|
+
const query = { company: options.company, project: options.project, plan, ...planningIntervalFilter(filters) };
|
|
90
|
+
if (filters.area)
|
|
91
|
+
query.area = filters.area;
|
|
92
|
+
const { site, limit, skip } = planningPagination(filters);
|
|
93
|
+
const all = await this.model.db.countDocuments(query);
|
|
94
|
+
const documents = await this.model.db.find(query).sort({ start: 1, _id: 1 }).skip(skip).limit(limit).toArray();
|
|
95
|
+
return planningPage(await this.reports(documents, options, undefined, undefined, plan), all, site, limit);
|
|
96
|
+
}
|
|
97
|
+
/** Absolute break times survive edits; moving a shift outside them must be resolved explicitly by the planner. */
|
|
98
|
+
async changeShift(plan, shift, input, options) {
|
|
99
|
+
const previous = await this.document(plan, shift, options);
|
|
100
|
+
const configuration = await ShiftPlansService.planForScheduling(plan, options);
|
|
101
|
+
const day = await ShiftPlanDaysService.dayForScheduling(plan, input.day, options);
|
|
102
|
+
const time = this.normalizedTime(input, configuration, day);
|
|
103
|
+
const candidate = { ...previous, ...time };
|
|
104
|
+
candidate.assignments = candidate.assignments.map((assignment) => normalizeBreaks(candidate, assignment));
|
|
105
|
+
await this.validateSchedule(candidate, options, previous);
|
|
106
|
+
const list = configuration.list;
|
|
107
|
+
await this.immutable({ _id: shift, company: options.company, project: options.project, plan }, { ...time, ...(list ? { list } : {}) }, options.user, 'change', options.app);
|
|
108
|
+
return (await this.singleShift(plan, shift, options));
|
|
109
|
+
}
|
|
110
|
+
async removeShift(plan, shift, options) {
|
|
111
|
+
await this.document(plan, shift, options);
|
|
112
|
+
await this.delete(shift, options.user, options.app);
|
|
113
|
+
}
|
|
114
|
+
async replaceAssignments(plan, shift, input, options) {
|
|
115
|
+
const previous = await this.document(plan, shift, options);
|
|
116
|
+
const configuration = await ShiftPlansService.planForScheduling(plan, options);
|
|
117
|
+
const list = configuration.list;
|
|
118
|
+
// The plan owns the employee list; a shift persisted before the plan was linked must resolve against it.
|
|
119
|
+
const target = { ...previous, ...(list ? { list } : {}) };
|
|
120
|
+
const assignments = await this.resolvedAssignments(input.assignments, target, options, previous);
|
|
121
|
+
if (!equalAssignmentSets(assignments, previous.assignments)) {
|
|
122
|
+
await this.validateSchedule({ ...target, assignments }, options, previous);
|
|
123
|
+
await this.immutable({ _id: shift, company: options.company, project: options.project, plan }, { assignments, ...(list ? { list } : {}) }, options.user, 'change', options.app, undefined, undefined, 'assignments');
|
|
124
|
+
}
|
|
125
|
+
return (await this.singleShift(plan, shift, options));
|
|
126
|
+
}
|
|
127
|
+
/** Suggestions use the manual assignment rules without applying the proposed allocation. */
|
|
128
|
+
async autofillShift(plan, shift, options, breaks = []) {
|
|
129
|
+
const configuration = await ShiftPlansService.planForScheduling(plan, options);
|
|
130
|
+
const previous = await this.model.db.findOne({ _id: shift, company: options.company, project: options.project, plan });
|
|
131
|
+
if (!previous)
|
|
132
|
+
throw new ImmutableError('SHIFT_NOT_FOUND');
|
|
133
|
+
// Existing attention items belong to the planner; autofill must not silently remove their assignments.
|
|
134
|
+
const result = { shift: previous._id, users: previous.assignments.map((assignment) => assignment.participant) };
|
|
135
|
+
// An absent staffing target is not permission to fill the shift with the entire employee pool.
|
|
136
|
+
const requiredPeople = previous.requiredPeople ?? 0;
|
|
137
|
+
if (requiredPeople <= previous.assignments.length)
|
|
138
|
+
return result;
|
|
139
|
+
// A plan may have changed lists since this shift was saved; its current configuration is authoritative.
|
|
140
|
+
const list = configuration.list;
|
|
141
|
+
if (!list)
|
|
142
|
+
return result;
|
|
143
|
+
const target = { ...previous, list };
|
|
144
|
+
const categories = configuration.workAreaCategories?.find((binding) => binding.area.equals(target.area))?.categoryIds;
|
|
145
|
+
const state = configuration.defaultParticipantState ? [configuration.defaultParticipantState] : undefined;
|
|
146
|
+
// The pool is ranked by qualification, then raw score; normalized stages would merge different scores.
|
|
147
|
+
// Inspect every candidate because scheduling conflicts can rule out an entire page of higher-ranked people.
|
|
148
|
+
const pool = await ParticipantsService.planningParticipants(list, { level: 'shift', shift: target._id, workArea: target.area }, state, categories, undefined, {
|
|
149
|
+
...options, skip: 0, limit: Number.MAX_SAFE_INTEGER, sort: { _id: 1 },
|
|
150
|
+
});
|
|
151
|
+
// Multiple list entries can identify the same person, including someone already assigned to this shift.
|
|
152
|
+
const seenCustomers = new Set(previous.assignments.map((assignment) => assignment.customer.toHexString()));
|
|
153
|
+
const candidates = [];
|
|
154
|
+
for (const candidate of pool.data) {
|
|
155
|
+
const customer = candidate.user?._id;
|
|
156
|
+
if (!customer || unavailableParticipantStates.has(candidate.state) || seenCustomers.has(customer.toHexString()))
|
|
157
|
+
continue;
|
|
158
|
+
seenCustomers.add(customer.toHexString());
|
|
159
|
+
// Only supplied breaks are considered; inventing break times would leave callers unable to apply an ID-only result.
|
|
160
|
+
candidates.push(normalizeBreaks(target, { participant: candidate._id, customer, breaks }));
|
|
161
|
+
}
|
|
162
|
+
if (!candidates.length)
|
|
163
|
+
return result;
|
|
164
|
+
// The probe expands the shared read scope to all possible picks; it is never an actual allocation.
|
|
165
|
+
// Loading this context once avoids database queries inside the candidate loop.
|
|
166
|
+
const probe = { ...target, assignments: [...previous.assignments, ...candidates] };
|
|
167
|
+
const neighbors = await this.scheduleNeighbors(probe, options, previous);
|
|
168
|
+
const otherNeighbors = neighbors.filter((neighbor) => !neighbor._id.equals(target._id));
|
|
169
|
+
const context = await this.reportContext([...otherNeighbors, probe], options);
|
|
170
|
+
// Missing or incompatible settings must not silently disable scheduling checks.
|
|
171
|
+
if (!effectivePolicy(context.settings, configuration.settingsOverrides))
|
|
172
|
+
throw new ImmutableError('SHIFT_PLANNING_SETTINGS_NOT_CONFIGURED');
|
|
173
|
+
// Persisted violation severities are the baseline, so existing attention items need not block safe additions.
|
|
174
|
+
// Working-time totals are unnecessary here; only rule violations influence selection.
|
|
175
|
+
const baseline = new Map();
|
|
176
|
+
for (const neighbor of neighbors)
|
|
177
|
+
reportShift(neighbor, context, false, baseline);
|
|
178
|
+
const otherSchedules = context.schedules.filter((scheduled) => !scheduled._id.equals(target._id));
|
|
179
|
+
const assignments = [...previous.assignments];
|
|
180
|
+
for (const candidate of candidates) {
|
|
181
|
+
if (assignments.length >= requiredPeople)
|
|
182
|
+
break;
|
|
183
|
+
const proposed = { ...target, assignments: [...assignments, candidate] };
|
|
184
|
+
// Replace the shift only in memory so neighboring plans can reveal new rest or hours violations.
|
|
185
|
+
const proposedContext = { ...context, schedules: [...otherSchedules, proposed] };
|
|
186
|
+
const updatedEvidence = new Map();
|
|
187
|
+
const updated = [...otherNeighbors, proposed]
|
|
188
|
+
.map((document) => reportShift(document, proposedContext, false, updatedEvidence));
|
|
189
|
+
// Availability may have changed between the pool query and the later context reads.
|
|
190
|
+
if (updated[updated.length - 1].unavailableParticipantIds.includes(candidate.participant.toHexString()))
|
|
191
|
+
continue;
|
|
192
|
+
if (newScheduleViolations(proposed, previous, updated, baseline, updatedEvidence).length)
|
|
193
|
+
continue;
|
|
194
|
+
assignments.push(candidate);
|
|
195
|
+
result.users.push(candidate.participant);
|
|
196
|
+
}
|
|
197
|
+
// A partial result is intentional, not a reservation; applying it later still requires assignment validation.
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Same allocation and rules as autofillShift, but resolves the suggestion into assignments and
|
|
202
|
+
* returns the recomputed shift, so a caller can apply it without a second round trip. Nothing is
|
|
203
|
+
* persisted; applying the result still goes through the normal assignment validation.
|
|
204
|
+
*/
|
|
205
|
+
async autofillShiftReport(plan, shift, options, breaks = []) {
|
|
206
|
+
const result = await this.autofillShift(plan, shift, options, breaks);
|
|
207
|
+
const previous = await this.document(plan, shift, options);
|
|
208
|
+
const configuration = await ShiftPlansService.planForScheduling(plan, options);
|
|
209
|
+
// The plan owns the employee list; a shift persisted before the plan was linked must resolve against it.
|
|
210
|
+
const target = { ...previous, ...(configuration.list ? { list: configuration.list } : {}) };
|
|
211
|
+
const assignments = await this.resolvedAssignments(result.users.map((participant) => ({ participant, breaks })), target, options, previous);
|
|
212
|
+
const proposal = { ...target, assignments };
|
|
213
|
+
return (await this.reports([proposal], options, proposal))[0];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Rule-checked suggestions for every under-staffed shift in a plan, day, work area or single shift.
|
|
217
|
+
* Shifts are filled greedily in start order against one shared allocation, so a person already
|
|
218
|
+
* picked for a colliding shift is not picked again; other plans act only as conflict neighbours.
|
|
219
|
+
* Nothing is persisted; applying a result still goes through the normal assignment validation.
|
|
220
|
+
*/
|
|
221
|
+
async autofill(scope, options, breaks = []) {
|
|
222
|
+
if (!scope.plan)
|
|
223
|
+
return [];
|
|
224
|
+
const configuration = await ShiftPlansService.planForScheduling(scope.plan, options);
|
|
225
|
+
const list = configuration.list;
|
|
226
|
+
if (!list)
|
|
227
|
+
return [];
|
|
228
|
+
const preparation = await ParticipantsService.prepareShiftPlanning(list, scope, options);
|
|
229
|
+
if (!preparation || !preparation.targets.length)
|
|
230
|
+
return [];
|
|
231
|
+
// The pool arrives ranked; carrying breaks and keeping one entry per person matches a manual pick.
|
|
232
|
+
const targets = preparation.targets.map((target) => {
|
|
233
|
+
const shift = { ...target.shift, list };
|
|
234
|
+
const seen = new Set(shift.assignments.map((assignment) => assignment.customer.toHexString()));
|
|
235
|
+
const candidates = [];
|
|
236
|
+
for (const candidate of target.candidates) {
|
|
237
|
+
const customer = candidate.customer.toHexString();
|
|
238
|
+
if (seen.has(customer))
|
|
239
|
+
continue;
|
|
240
|
+
seen.add(customer);
|
|
241
|
+
candidates.push({ ...candidate, breaks: normalizeBreaks(shift, { participant: candidate.participant, customer: candidate.customer, breaks }).breaks });
|
|
242
|
+
}
|
|
243
|
+
return { ...target, shift, candidates };
|
|
244
|
+
});
|
|
245
|
+
// The probe widens the shared context read to every possible pick; it is never an allocation.
|
|
246
|
+
const probes = targets.map((target) => ({
|
|
247
|
+
...target.shift,
|
|
248
|
+
assignments: [...target.shift.assignments, ...target.candidates.map((candidate) => ({ participant: candidate.participant, customer: candidate.customer, breaks: [] }))],
|
|
249
|
+
}));
|
|
250
|
+
const customerIds = [...new Map(probes.flatMap((probe) => probe.assignments.map((assignment) => [assignment.customer.toHexString(), assignment.customer]))).values()];
|
|
251
|
+
const starts = probes.map((probe) => Date.parse(probe.start));
|
|
252
|
+
const ends = probes.map((probe) => Date.parse(probe.end));
|
|
253
|
+
// Only this scope's shifts may be filled, but conflicts in every plan are compared, as manual edits do.
|
|
254
|
+
const neighbours = await this.model.db.find({ company: options.company, 'assignments.customer': { $in: customerIds },
|
|
255
|
+
end: { $gt: boundedPlanningInstant(Math.min(...starts) - scheduleWindowMilliseconds) },
|
|
256
|
+
start: { $lt: boundedPlanningInstant(Math.max(...ends) + scheduleWindowMilliseconds) },
|
|
257
|
+
}).toArray();
|
|
258
|
+
// The neighbours already cover the complete rule window; expanding around them would grow it again.
|
|
259
|
+
const context = await this.reportContext([...neighbours, ...probes], options, { schedules: neighbours, preparation });
|
|
260
|
+
// Missing or incompatible settings must not silently disable scheduling checks.
|
|
261
|
+
if (!effectivePolicy(context.settings, configuration.settingsOverrides))
|
|
262
|
+
throw new ImmutableError('SHIFT_PLANNING_SETTINGS_NOT_CONFIGURED');
|
|
263
|
+
const schedules = [...new Map([...context.schedules, ...targets.map((target) => target.shift)].map((shift) => [shift._id.toHexString(), shift])).values()];
|
|
264
|
+
const allocation = allocateShiftFills(schedules, targets, context, { matchingGate: false, breaksBlocking: true, stopOnUnfillable: false });
|
|
265
|
+
// Report against the finished allocation so overlaps between the suggested shifts are visible.
|
|
266
|
+
const filled = [...new Map([...context.schedules, ...allocation.shifts].map((shift) => [shift._id.toHexString(), shift])).values()];
|
|
267
|
+
return allocation.shifts.map((shift) => reportShift(shift, { ...context, schedules: filled }));
|
|
268
|
+
}
|
|
269
|
+
async hasWorkAreaAssignments(area, options) {
|
|
270
|
+
return await this.model.db.countDocuments({ company: options.company, project: options.project, area }) > 0;
|
|
271
|
+
}
|
|
272
|
+
/** A plan cannot change its time zone while existing shifts would no longer match their day's local date. */
|
|
273
|
+
async assertPlanTimeZone(plan, timeZone, options) {
|
|
274
|
+
const shifts = await this.model.db.find({ company: options.company, project: options.project, plan }).toArray();
|
|
275
|
+
if (!shifts.length)
|
|
276
|
+
return;
|
|
277
|
+
const days = await ShiftPlanDaysService.daysForPlans([plan], options);
|
|
278
|
+
if (shifts.some((shift) => {
|
|
279
|
+
const day = days.find((entry) => entry._id.equals(shift.day));
|
|
280
|
+
return !day || localDate(shift.start, timeZone) !== day.date;
|
|
281
|
+
}))
|
|
282
|
+
throw new ImmutableError('SHIFT_PLAN_REFERENCES_IN_USE');
|
|
283
|
+
}
|
|
284
|
+
async hasDayShifts(day, options) {
|
|
285
|
+
return await this.model.db.countDocuments({ company: options.company, project: options.project, day }) > 0;
|
|
286
|
+
}
|
|
287
|
+
/** A day cannot drop an area or change its date while shifts still reference it. */
|
|
288
|
+
async assertDayReferences(previous, configuration, options) {
|
|
289
|
+
const plan = await ShiftPlansService.planForScheduling(previous.plan, options);
|
|
290
|
+
const shifts = await this.model.db.find({ company: options.company, project: options.project, day: previous._id }).toArray();
|
|
291
|
+
if (shifts.some((shift) => localDate(shift.start, plan.timeZone) !== configuration.date
|
|
292
|
+
|| !(configuration.areas ?? []).some((link) => link.area.equals(shift.area)))) {
|
|
293
|
+
throw new ImmutableError('SHIFT_PLAN_REFERENCES_IN_USE');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async myShifts(customer, filters, options) {
|
|
297
|
+
if (!await CustomersService.memberForPlanning(customer, options))
|
|
298
|
+
throw new ImmutableError('MEMBER_NOT_ALLOWED');
|
|
299
|
+
const plans = await ShiftPlansService.plansForProject(options, filters.list);
|
|
300
|
+
const lists = await ListsService.listsForPlanning(plans.map((plan) => plan.list).filter((list) => list !== undefined), options);
|
|
301
|
+
const days = await ShiftPlanDaysService.daysForPlans(plans.map((plan) => plan._id), options);
|
|
302
|
+
const areas = await WorkAreasService.areasForPlanning(days.flatMap((day) => (day.areas ?? []).map((link) => link.area)), options);
|
|
303
|
+
const planLinks = (plan) => days.filter((day) => day.plan.equals(plan._id)).flatMap((day) => day.areas ?? []);
|
|
304
|
+
const visiblePlans = plans.filter((plan) => plan.list && lists.some((list) => list._id.equals(plan.list))
|
|
305
|
+
&& planLinks(plan).some((link) => areas.some((area) => area._id.equals(link.area))));
|
|
306
|
+
const { site, limit, skip } = planningPagination(filters);
|
|
307
|
+
if (!visiblePlans.length)
|
|
308
|
+
return planningPage([], 0, site, limit);
|
|
309
|
+
// Filtering usable references before pagination avoids disclosing totals for orphaned assignments.
|
|
310
|
+
const query = { company: options.company, project: options.project, 'assignments.customer': customer, ...planningIntervalFilter(filters),
|
|
311
|
+
$or: visiblePlans.map((plan) => ({ plan: plan._id, list: plan.list,
|
|
312
|
+
area: { $in: planLinks(plan).map((link) => link.area).filter((area) => areas.some((entry) => entry._id.equals(area))) },
|
|
313
|
+
assignments: { $elemMatch: { customer } },
|
|
314
|
+
})), };
|
|
315
|
+
const all = await this.model.db.countDocuments(query);
|
|
316
|
+
const shifts = await this.model.db.find(query).sort({ start: 1, _id: 1 }).skip(skip).limit(limit).toArray();
|
|
317
|
+
const data = [];
|
|
318
|
+
for (const shift of shifts) {
|
|
319
|
+
const plan = visiblePlans.find((entry) => entry._id.equals(shift.plan));
|
|
320
|
+
const list = lists.find((entry) => entry._id.equals(shift.list));
|
|
321
|
+
const assignment = shift.assignments.find((entry) => entry.customer.equals(customer));
|
|
322
|
+
const area = areas.find((entry) => entry._id.equals(shift.area));
|
|
323
|
+
const day = days.find((entry) => entry._id.equals(shift.day));
|
|
324
|
+
if (!plan || !list || !assignment || !area || !day)
|
|
325
|
+
continue;
|
|
326
|
+
// Explicit projection keeps future internal area/shift fields out of the member contract automatically.
|
|
327
|
+
const publicArea = { _id: area._id, name: area.name };
|
|
328
|
+
if (area.description !== undefined)
|
|
329
|
+
publicArea.description = area.description;
|
|
330
|
+
if (area.location !== undefined)
|
|
331
|
+
publicArea.location = area.location;
|
|
332
|
+
if (area.address !== undefined)
|
|
333
|
+
publicArea.address = area.address;
|
|
334
|
+
data.push({ _id: shift._id, day: day.date, title: shift.title, start: shift.start, end: shift.end,
|
|
335
|
+
plan: { _id: plan._id, name: plan.name, timeZone: plan.timeZone }, list: { _id: list._id, name: list.name }, area: publicArea, breaks: assignment.breaks,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return planningPage(data, all, site, limit);
|
|
339
|
+
}
|
|
340
|
+
async reportContext(documents, options, contextOptions = {}) {
|
|
341
|
+
const { replacement, plan, preparation } = contextOptions;
|
|
342
|
+
const customerIds = [...new Map(documents.flatMap((shift) => shift.assignments.map((entry) => [entry.customer.toHexString(), entry.customer]))).values()];
|
|
343
|
+
const earliest = Math.min(...documents.map((shift) => Date.parse(shift.start)));
|
|
344
|
+
const latest = Math.max(...documents.map((shift) => Date.parse(shift.end)));
|
|
345
|
+
// Plan reads stay inside their plan; scheduling writes omit the scope to keep their company-wide conflict checks.
|
|
346
|
+
const schedules = contextOptions.schedules ?? await this.model.db.find({ company: options.company, ...(plan ? { plan } : {}), 'assignments.customer': { $in: customerIds },
|
|
347
|
+
end: { $gt: boundedPlanningInstant(earliest - scheduleWindowMilliseconds) }, start: { $lt: boundedPlanningInstant(latest + scheduleWindowMilliseconds) },
|
|
348
|
+
}).toArray();
|
|
349
|
+
if (replacement) {
|
|
350
|
+
const index = schedules.findIndex((shift) => shift._id.equals(replacement._id));
|
|
351
|
+
if (index >= 0)
|
|
352
|
+
schedules[index] = replacement;
|
|
353
|
+
else
|
|
354
|
+
schedules.push(replacement);
|
|
355
|
+
}
|
|
356
|
+
const participantIds = documents.flatMap((shift) => shift.assignments.map((entry) => entry.participant));
|
|
357
|
+
const planIds = [...new Map([...schedules, ...documents].map((shift) => [shift.plan.toHexString(), shift.plan])).values()]
|
|
358
|
+
.filter((planId) => !preparation?.plan._id.equals(planId));
|
|
359
|
+
const [plans, settings, absences, people] = await Promise.all([
|
|
360
|
+
planIds.length ? ShiftPlansService.plansForCompany(planIds, options) : [],
|
|
361
|
+
preparation ? preparation.settings : SettingsService.singleSettings(options),
|
|
362
|
+
AbsencesService.absencesForPlanning(customerIds, boundedPlanningInstant(earliest), boundedPlanningInstant(latest), options),
|
|
363
|
+
ParticipantsService.planningParticipantContext(participantIds, customerIds, options.company, preparation),
|
|
364
|
+
]);
|
|
365
|
+
return { schedules, plans: preparation ? [preparation.plan, ...plans] : plans, settings: settings?.settings, absences, ...people };
|
|
366
|
+
}
|
|
367
|
+
async reports(documents, options, replacement, evidence, plan) {
|
|
368
|
+
if (!documents.length)
|
|
369
|
+
return [];
|
|
370
|
+
const context = await this.reportContext(documents, options, { replacement, plan });
|
|
371
|
+
return documents.map((shift) => reportShift(shift, context, evidence === undefined, evidence));
|
|
372
|
+
}
|
|
373
|
+
scheduleNeighbors(candidate, options, previous) {
|
|
374
|
+
const customers = [...new Map([...candidate.assignments, ...(previous?.assignments ?? [])].map((entry) => [entry.customer.toHexString(), entry.customer])).values()];
|
|
375
|
+
// Both intervals matter when moving a shift, since its old and new positions can affect different neighbors.
|
|
376
|
+
const earliest = Math.min(Date.parse(candidate.start), previous ? Date.parse(previous.start) : Infinity);
|
|
377
|
+
const latest = Math.max(Date.parse(candidate.end), previous ? Date.parse(previous.end) : -Infinity);
|
|
378
|
+
// Future shifts in other plans may impose stricter incoming-rest rules than the proposed shift's own plan.
|
|
379
|
+
return this.model.db.find({ company: options.company, 'assignments.customer': { $in: customers },
|
|
380
|
+
end: { $gte: new Date(earliest).toISOString() }, start: { $lte: boundedPlanningInstant(latest + scheduleWindowMilliseconds) },
|
|
381
|
+
}).toArray();
|
|
382
|
+
}
|
|
383
|
+
async validateSchedule(candidate, options, previous) {
|
|
384
|
+
if (!candidate.assignments.length && !previous?.assignments.length)
|
|
385
|
+
return;
|
|
386
|
+
const neighbors = await this.scheduleNeighbors(candidate, options, previous);
|
|
387
|
+
// Capture persisted evidence before evaluating the in-memory replacement, just as autofill does for its trials.
|
|
388
|
+
const baseline = new Map();
|
|
389
|
+
await this.reports(neighbors, options, undefined, baseline);
|
|
390
|
+
const updatedEvidence = new Map();
|
|
391
|
+
const updated = await this.reports([...neighbors.filter((shift) => !shift._id.equals(candidate._id)), candidate], options, candidate, updatedEvidence);
|
|
392
|
+
const violations = newScheduleViolations(candidate, previous, updated, baseline, updatedEvidence);
|
|
393
|
+
if (violations.some((violation) => violation.rule === 'settingsMissing' && violation.shift === candidate._id.toHexString())) {
|
|
394
|
+
throw new ImmutableError('SHIFT_PLANNING_SETTINGS_NOT_CONFIGURED');
|
|
395
|
+
}
|
|
396
|
+
if (violations.length) {
|
|
397
|
+
throw new ImmutableError('SHIFT_PLANNING_RULE_VIOLATION', [{
|
|
398
|
+
instancePath: '/assignments', schemaPath: '#/shiftPlanning', keyword: 'shiftPlanning', params: { violations },
|
|
399
|
+
message: 'assignments violate the effective working-time policy',
|
|
400
|
+
}]);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
export default new ShiftsService();
|
|
405
|
+
//# sourceMappingURL=shifts.service.js.map
|