dabke 0.81.1 → 0.83.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/CHANGELOG.md +58 -0
- package/README.md +45 -27
- package/dist/client.d.ts +20 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -1
- package/dist/client.js.map +1 -1
- package/dist/client.types.d.ts +9 -0
- package/dist/client.types.d.ts.map +1 -1
- package/dist/client.types.js +1 -0
- package/dist/client.types.js.map +1 -1
- package/dist/cpsat/model-builder.d.ts +9 -0
- package/dist/cpsat/model-builder.d.ts.map +1 -1
- package/dist/cpsat/model-builder.js +36 -34
- package/dist/cpsat/model-builder.js.map +1 -1
- package/dist/cpsat/response.d.ts +13 -1
- package/dist/cpsat/response.d.ts.map +1 -1
- package/dist/cpsat/response.js +4 -0
- package/dist/cpsat/response.js.map +1 -1
- package/dist/cpsat/rules/cost-utils.d.ts +11 -0
- package/dist/cpsat/rules/cost-utils.d.ts.map +1 -0
- package/dist/cpsat/rules/cost-utils.js +24 -0
- package/dist/cpsat/rules/cost-utils.js.map +1 -0
- package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-multiplier.js +3 -14
- package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.js +3 -7
- package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/index.d.ts +3 -0
- package/dist/cpsat/rules/index.d.ts.map +1 -1
- package/dist/cpsat/rules/index.js +3 -0
- package/dist/cpsat/rules/index.js.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.js +16 -2
- package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/max-days-week.d.ts +44 -0
- package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/max-days-week.js +95 -0
- package/dist/cpsat/rules/max-days-week.js.map +1 -0
- package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-day.js +15 -2
- package/dist/cpsat/rules/max-hours-day.js.map +1 -1
- package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-week.js +16 -2
- package/dist/cpsat/rules/max-hours-week.js.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.js +15 -2
- package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.js +15 -2
- package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/min-days-week.d.ts +34 -0
- package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/min-days-week.js +84 -0
- package/dist/cpsat/rules/min-days-week.js.map +1 -0
- package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-day.js +15 -2
- package/dist/cpsat/rules/min-hours-day.js.map +1 -1
- package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-week.js +16 -2
- package/dist/cpsat/rules/min-hours-week.js.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.js +72 -2
- package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
- package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
- package/dist/cpsat/rules/minimize-cost.js +2 -23
- package/dist/cpsat/rules/minimize-cost.js.map +1 -1
- package/dist/cpsat/rules/must-assign.d.ts +49 -0
- package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
- package/dist/cpsat/rules/must-assign.js +86 -0
- package/dist/cpsat/rules/must-assign.js.map +1 -0
- package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.js +1 -5
- package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +5 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.js +1 -12
- package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.js +1 -5
- package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
- package/dist/cpsat/rules/registry.d.ts +28 -2
- package/dist/cpsat/rules/registry.d.ts.map +1 -1
- package/dist/cpsat/rules/registry.js +4 -1
- package/dist/cpsat/rules/registry.js.map +1 -1
- package/dist/cpsat/rules/resolver.js +2 -2
- package/dist/cpsat/rules/resolver.js.map +1 -1
- package/dist/cpsat/rules/rules.types.d.ts +3 -0
- package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +18 -1
- package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.js +59 -16
- package/dist/cpsat/rules/scope.types.js.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.js +2 -1
- package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/time-off.d.ts.map +1 -1
- package/dist/cpsat/rules/time-off.js +6 -3
- package/dist/cpsat/rules/time-off.js.map +1 -1
- package/dist/cpsat/semantic-time.d.ts +44 -42
- package/dist/cpsat/semantic-time.d.ts.map +1 -1
- package/dist/cpsat/semantic-time.js +64 -46
- package/dist/cpsat/semantic-time.js.map +1 -1
- package/dist/cpsat/types.d.ts +37 -27
- package/dist/cpsat/types.d.ts.map +1 -1
- package/dist/cpsat/utils.d.ts.map +1 -1
- package/dist/cpsat/utils.js +7 -12
- package/dist/cpsat/utils.js.map +1 -1
- package/dist/cpsat/validation-reporter.d.ts +10 -7
- package/dist/cpsat/validation-reporter.d.ts.map +1 -1
- package/dist/cpsat/validation-reporter.js +44 -72
- package/dist/cpsat/validation-reporter.js.map +1 -1
- package/dist/cpsat/validation.types.d.ts +54 -44
- package/dist/cpsat/validation.types.d.ts.map +1 -1
- package/dist/cpsat/validation.types.js +15 -10
- package/dist/cpsat/validation.types.js.map +1 -1
- package/dist/datetime.utils.d.ts +3 -203
- package/dist/datetime.utils.d.ts.map +1 -1
- package/dist/datetime.utils.js +1 -288
- package/dist/datetime.utils.js.map +1 -1
- package/dist/index.d.ts +14 -83
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -83
- package/dist/index.js.map +1 -1
- package/dist/schedule/cost.d.ts +204 -0
- package/dist/schedule/cost.d.ts.map +1 -0
- package/dist/schedule/cost.js +187 -0
- package/dist/schedule/cost.js.map +1 -0
- package/dist/schedule/coverage.d.ts +85 -0
- package/dist/schedule/coverage.d.ts.map +1 -0
- package/dist/schedule/coverage.js +33 -0
- package/dist/schedule/coverage.js.map +1 -0
- package/dist/schedule/definition.d.ts +227 -0
- package/dist/schedule/definition.d.ts.map +1 -0
- package/dist/schedule/definition.js +659 -0
- package/dist/schedule/definition.js.map +1 -0
- package/dist/schedule/index.d.ts +67 -0
- package/dist/schedule/index.d.ts.map +1 -0
- package/dist/schedule/index.js +69 -0
- package/dist/schedule/index.js.map +1 -0
- package/dist/schedule/rules.d.ts +353 -0
- package/dist/schedule/rules.d.ts.map +1 -0
- package/dist/schedule/rules.js +352 -0
- package/dist/schedule/rules.js.map +1 -0
- package/dist/schedule/shift-patterns.d.ts +34 -0
- package/dist/schedule/shift-patterns.d.ts.map +1 -0
- package/dist/schedule/shift-patterns.js +41 -0
- package/dist/schedule/shift-patterns.js.map +1 -0
- package/dist/schedule/time-periods.d.ts +69 -0
- package/dist/schedule/time-periods.d.ts.map +1 -0
- package/dist/schedule/time-periods.js +91 -0
- package/dist/schedule/time-periods.js.map +1 -0
- package/dist/types.d.ts +14 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -9
- package/solver/src/solver/app.py +1 -1
- package/solver/src/solver/solver.py +7 -4
- package/src/client.ts +6 -8
- package/src/client.types.ts +9 -0
- package/src/cpsat/model-builder.ts +44 -35
- package/src/cpsat/response.ts +13 -1
- package/src/cpsat/rules/cost-utils.ts +25 -0
- package/src/cpsat/rules/day-cost-multiplier.ts +3 -14
- package/src/cpsat/rules/day-cost-surcharge.ts +3 -8
- package/src/cpsat/rules/index.ts +3 -0
- package/src/cpsat/rules/max-consecutive-days.ts +17 -0
- package/src/cpsat/rules/max-days-week.ts +143 -0
- package/src/cpsat/rules/max-hours-day.ts +21 -1
- package/src/cpsat/rules/max-hours-week.ts +22 -1
- package/src/cpsat/rules/max-shifts-day.ts +21 -1
- package/src/cpsat/rules/min-consecutive-days.ts +16 -1
- package/src/cpsat/rules/min-days-week.ts +120 -0
- package/src/cpsat/rules/min-hours-day.ts +16 -1
- package/src/cpsat/rules/min-hours-week.ts +17 -1
- package/src/cpsat/rules/min-rest-between-shifts.ts +92 -2
- package/src/cpsat/rules/minimize-cost.ts +2 -29
- package/src/cpsat/rules/must-assign.ts +108 -0
- package/src/cpsat/rules/overtime-daily-multiplier.ts +1 -12
- package/src/cpsat/rules/overtime-daily-surcharge.ts +1 -6
- package/src/cpsat/rules/overtime-tiered-multiplier.ts +6 -13
- package/src/cpsat/rules/overtime-weekly-multiplier.ts +1 -12
- package/src/cpsat/rules/overtime-weekly-surcharge.ts +1 -6
- package/src/cpsat/rules/registry.ts +8 -2
- package/src/cpsat/rules/resolver.ts +2 -2
- package/src/cpsat/rules/rules.types.ts +3 -0
- package/src/cpsat/rules/scope.types.ts +73 -20
- package/src/cpsat/rules/time-cost-surcharge.ts +2 -1
- package/src/cpsat/rules/time-off.ts +6 -2
- package/src/cpsat/semantic-time.ts +115 -91
- package/src/cpsat/types.ts +37 -27
- package/src/cpsat/utils.ts +8 -12
- package/src/cpsat/validation-reporter.ts +51 -82
- package/src/cpsat/validation.types.ts +72 -47
- package/src/datetime.utils.ts +3 -334
- package/src/index.ts +35 -107
- package/src/schedule/cost.ts +242 -0
- package/src/schedule/coverage.ts +135 -0
- package/src/schedule/definition.ts +958 -0
- package/src/schedule/index.ts +112 -0
- package/src/schedule/rules.ts +529 -0
- package/src/schedule/shift-patterns.ts +46 -0
- package/src/schedule/time-periods.ts +110 -0
- package/src/types.ts +14 -88
- package/dist/errors.d.ts +0 -12
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -17
- package/dist/errors.js.map +0 -1
- package/dist/llms.d.ts +0 -2
- package/dist/llms.d.ts.map +0 -1
- package/dist/llms.js +0 -3
- package/dist/llms.js.map +0 -1
- package/dist/schedule.d.ts +0 -724
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js +0 -899
- package/dist/schedule.js.map +0 -1
- package/dist/validation.d.ts +0 -105
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -130
- package/dist/validation.js.map +0 -1
- package/llms.txt +0 -925
- package/src/errors.ts +0 -17
- package/src/llms.ts +0 -3
- package/src/schedule.ts +0 -1419
- package/src/validation.ts +0 -188
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import type { CompilationRule } from "../model-builder.js";
|
|
3
|
-
import {
|
|
2
|
+
import type { CompilationRule, RuleValidationContext } from "../model-builder.js";
|
|
3
|
+
import type { ResolvedShiftAssignment } from "../response.js";
|
|
4
|
+
import { normalizeEndMinutes, priorityToPenalty, timeOfDayToMinutes } from "../utils.js";
|
|
5
|
+
import type { ValidationReporter } from "../validation-reporter.js";
|
|
4
6
|
import {
|
|
5
7
|
PrioritySchema,
|
|
6
8
|
entityScope,
|
|
7
9
|
parseEntityScope,
|
|
8
10
|
resolveMembersFromScope,
|
|
11
|
+
ruleGroup,
|
|
9
12
|
} from "./scope.types.js";
|
|
10
13
|
|
|
11
14
|
const MinRestBetweenShiftsSchema = z
|
|
@@ -41,6 +44,11 @@ export function createMinRestBetweenShiftsRule(
|
|
|
41
44
|
const scope = parseEntityScope(parsed);
|
|
42
45
|
const { hours, priority } = parsed;
|
|
43
46
|
const minMinutes = hours * 60;
|
|
47
|
+
const group = ruleGroup(
|
|
48
|
+
`min-rest-between-shifts:${hours}`,
|
|
49
|
+
`Min ${hours}h rest between shifts`,
|
|
50
|
+
scope,
|
|
51
|
+
);
|
|
44
52
|
|
|
45
53
|
return {
|
|
46
54
|
compile(b) {
|
|
@@ -121,5 +129,87 @@ export function createMinRestBetweenShiftsRule(
|
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
131
|
},
|
|
132
|
+
|
|
133
|
+
validate(
|
|
134
|
+
assignments: ResolvedShiftAssignment[],
|
|
135
|
+
reporter: ValidationReporter,
|
|
136
|
+
context: RuleValidationContext,
|
|
137
|
+
) {
|
|
138
|
+
if (priority === "MANDATORY") return;
|
|
139
|
+
|
|
140
|
+
const members = resolveMembersFromScope(scope, context.members);
|
|
141
|
+
if (members.length === 0) return;
|
|
142
|
+
|
|
143
|
+
const memberIds = new Set(members.map((m) => m.id));
|
|
144
|
+
|
|
145
|
+
// Group assignments by member, sort by day then start time
|
|
146
|
+
const byMember = new Map<string, ResolvedShiftAssignment[]>();
|
|
147
|
+
for (const a of assignments) {
|
|
148
|
+
if (!memberIds.has(a.memberId)) continue;
|
|
149
|
+
const list = byMember.get(a.memberId) ?? [];
|
|
150
|
+
list.push(a);
|
|
151
|
+
byMember.set(a.memberId, list);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const [memberId, memberAssignments] of byMember) {
|
|
155
|
+
const sorted = memberAssignments.toSorted((a, b) => {
|
|
156
|
+
if (a.day !== b.day) return a.day < b.day ? -1 : 1;
|
|
157
|
+
return timeOfDayToMinutes(a.startTime) - timeOfDayToMinutes(b.startTime);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
let violated = false;
|
|
161
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
162
|
+
const current = sorted[i]!;
|
|
163
|
+
const next = sorted[i + 1]!;
|
|
164
|
+
|
|
165
|
+
const currentEndMin = timeOfDayToMinutes(current.endTime);
|
|
166
|
+
const normalizedEnd = normalizeEndMinutes(
|
|
167
|
+
timeOfDayToMinutes(current.startTime),
|
|
168
|
+
currentEndMin,
|
|
169
|
+
);
|
|
170
|
+
const nextStartMin = timeOfDayToMinutes(next.startTime);
|
|
171
|
+
|
|
172
|
+
// Calculate gap considering day boundaries
|
|
173
|
+
let gap: number;
|
|
174
|
+
if (current.day === next.day) {
|
|
175
|
+
gap = nextStartMin - normalizedEnd;
|
|
176
|
+
} else {
|
|
177
|
+
// Different days: account for the day boundary
|
|
178
|
+
const dayDiff = daysBetween(current.day, next.day);
|
|
179
|
+
gap = dayDiff * 24 * 60 - normalizedEnd + nextStartMin;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (gap >= 0 && gap < minMinutes) {
|
|
183
|
+
reporter.reportRuleViolation({
|
|
184
|
+
rule: "min-rest-between-shifts",
|
|
185
|
+
message: `${memberId} has ${Math.round((gap / 60) * 10) / 10}h rest between shifts on ${current.day} and ${next.day}, need ${hours}h`,
|
|
186
|
+
context: { memberIds: [memberId], days: [current.day, next.day] },
|
|
187
|
+
shortfall: minMinutes - gap,
|
|
188
|
+
group,
|
|
189
|
+
});
|
|
190
|
+
violated = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!violated && sorted.length > 1) {
|
|
195
|
+
reporter.reportRulePassed({
|
|
196
|
+
rule: "min-rest-between-shifts",
|
|
197
|
+
message: `${memberId} has ${hours}h+ rest between all shifts`,
|
|
198
|
+
context: {
|
|
199
|
+
memberIds: [memberId],
|
|
200
|
+
days: [...new Set(sorted.map((a) => a.day))],
|
|
201
|
+
},
|
|
202
|
+
group,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
},
|
|
124
207
|
};
|
|
125
208
|
}
|
|
209
|
+
|
|
210
|
+
/** Compute day difference from YYYY-MM-DD strings. */
|
|
211
|
+
function daysBetween(day1: string, day2: string): number {
|
|
212
|
+
const d1 = new Date(`${day1}T00:00:00Z`);
|
|
213
|
+
const d2 = new Date(`${day2}T00:00:00Z`);
|
|
214
|
+
return Math.round((d2.getTime() - d1.getTime()) / (24 * 60 * 60 * 1000));
|
|
215
|
+
}
|
|
@@ -4,12 +4,7 @@ import type { CompilationRule, CostContribution } from "../model-builder.js";
|
|
|
4
4
|
import type { ShiftPattern, SchedulingMember } from "../types.js";
|
|
5
5
|
import type { ShiftAssignment } from "../response.js";
|
|
6
6
|
import { COST_CATEGORY } from "../cost.js";
|
|
7
|
-
import {
|
|
8
|
-
OBJECTIVE_WEIGHTS,
|
|
9
|
-
timeOfDayToMinutes,
|
|
10
|
-
normalizeEndMinutes,
|
|
11
|
-
splitIntoWeeks,
|
|
12
|
-
} from "../utils.js";
|
|
7
|
+
import { OBJECTIVE_WEIGHTS, splitIntoWeeks } from "../utils.js";
|
|
13
8
|
import {
|
|
14
9
|
entityScope,
|
|
15
10
|
timeScope,
|
|
@@ -18,6 +13,7 @@ import {
|
|
|
18
13
|
resolveMembersFromScope,
|
|
19
14
|
resolveActiveDaysFromScope,
|
|
20
15
|
} from "./scope.types.js";
|
|
16
|
+
import { getHourlyRate, getSalariedPay, patternDurationMinutes } from "./cost-utils.js";
|
|
21
17
|
|
|
22
18
|
const MinimizeCostSchema = entityScope(["members", "roles", "skills"]).and(
|
|
23
19
|
timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]),
|
|
@@ -26,29 +22,6 @@ const MinimizeCostSchema = entityScope(["members", "roles", "skills"]).and(
|
|
|
26
22
|
/** Configuration for {@link createMinimizeCostRule}. */
|
|
27
23
|
export type MinimizeCostConfig = z.infer<typeof MinimizeCostSchema>;
|
|
28
24
|
|
|
29
|
-
/** Returns the hourly rate for a member, or undefined if not hourly. */
|
|
30
|
-
function getHourlyRate(member: SchedulingMember): number | undefined {
|
|
31
|
-
if (!member.pay) return undefined;
|
|
32
|
-
if ("hourlyRate" in member.pay) return member.pay.hourlyRate;
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Returns salaried pay info, or undefined if not salaried. */
|
|
37
|
-
function getSalariedPay(
|
|
38
|
-
member: SchedulingMember,
|
|
39
|
-
): { annual: number; hoursPerWeek: number } | undefined {
|
|
40
|
-
if (!member.pay) return undefined;
|
|
41
|
-
if ("annual" in member.pay) return member.pay;
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Computes shift duration in minutes from a pattern. */
|
|
46
|
-
function patternDurationMinutes(pattern: ShiftPattern): number {
|
|
47
|
-
const start = timeOfDayToMinutes(pattern.startTime);
|
|
48
|
-
const end = normalizeEndMinutes(start, timeOfDayToMinutes(pattern.endTime));
|
|
49
|
-
return end - start;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
25
|
/**
|
|
53
26
|
* Creates the minimize-cost rule.
|
|
54
27
|
*
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { DayOfWeekSchema } from "../../types.js";
|
|
3
|
+
import type { CompilationRule } from "../model-builder.js";
|
|
4
|
+
import type { Term } from "../types.js";
|
|
5
|
+
import { priorityToPenalty, splitIntoWeeks } from "../utils.js";
|
|
6
|
+
import {
|
|
7
|
+
entityScope,
|
|
8
|
+
parseEntityScope,
|
|
9
|
+
resolveMembersFromScope,
|
|
10
|
+
ruleGroup,
|
|
11
|
+
} from "./scope.types.js";
|
|
12
|
+
|
|
13
|
+
/** Internally always HIGH. Not user-configurable. */
|
|
14
|
+
const MUST_ASSIGN_PENALTY = priorityToPenalty("HIGH");
|
|
15
|
+
|
|
16
|
+
const MustAssignSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
weekStartsOn: DayOfWeekSchema.optional(),
|
|
19
|
+
})
|
|
20
|
+
.and(entityScope(["members", "roles", "skills"]));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for {@link createMustAssignRule}.
|
|
24
|
+
*
|
|
25
|
+
* - `weekStartsOn` (optional): which day starts the week; defaults to
|
|
26
|
+
* {@link ModelBuilder.weekStartsOn}
|
|
27
|
+
*
|
|
28
|
+
* Entity scoping (at most one): `memberIds`, `roleIds`, `skillIds`
|
|
29
|
+
*/
|
|
30
|
+
export type MustAssignConfig = z.infer<typeof MustAssignSchema>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Guarantees that targeted members appear on the schedule.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Use this for staffing obligations: salaried employees who are paid
|
|
37
|
+
* regardless of whether they work, or contracted staff who must be
|
|
38
|
+
* rostered. The solver ensures each targeted member has at least one
|
|
39
|
+
* assignment per scheduling week.
|
|
40
|
+
*
|
|
41
|
+
* Always compiles as a soft constraint (HIGH priority internally) so the
|
|
42
|
+
* schedule still generates when a member genuinely cannot be placed
|
|
43
|
+
* (e.g., full week of absences, no compatible shift patterns). Violations
|
|
44
|
+
* surface as validation warnings with distinct messaging from
|
|
45
|
+
* `min-days-week`.
|
|
46
|
+
*
|
|
47
|
+
* Unlike {@link createMinDaysWeekRule}, this rule communicates a staffing
|
|
48
|
+
* obligation rather than a scheduling preference. Validation messages
|
|
49
|
+
* reflect this: "Alice was not assigned (staffing obligation)" rather
|
|
50
|
+
* than "Alice worked 0 days, minimum was 1."
|
|
51
|
+
*
|
|
52
|
+
* @param config - See {@link MustAssignConfig}
|
|
53
|
+
*
|
|
54
|
+
* @example Ensure a salaried manager is always rostered
|
|
55
|
+
* ```ts
|
|
56
|
+
* createMustAssignRule({ memberIds: ["diana"] });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example Ensure all supervisors appear on the rota
|
|
60
|
+
* ```ts
|
|
61
|
+
* createMustAssignRule({ roleIds: ["supervisor"] });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function createMustAssignRule(config: MustAssignConfig): CompilationRule {
|
|
65
|
+
const parsed = MustAssignSchema.parse(config);
|
|
66
|
+
const scope = parseEntityScope(parsed);
|
|
67
|
+
const { weekStartsOn } = parsed;
|
|
68
|
+
const group = ruleGroup("must-assign", "Must assign", scope);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
compile(b) {
|
|
72
|
+
const members = resolveMembersFromScope(scope, b.members);
|
|
73
|
+
const weeks = splitIntoWeeks(b.days, weekStartsOn ?? b.weekStartsOn);
|
|
74
|
+
|
|
75
|
+
for (const emp of members) {
|
|
76
|
+
for (const weekDays of weeks) {
|
|
77
|
+
const terms: Term[] = [];
|
|
78
|
+
|
|
79
|
+
for (const day of weekDays) {
|
|
80
|
+
for (const pattern of b.shiftPatterns) {
|
|
81
|
+
if (!b.canAssign(emp, pattern)) continue;
|
|
82
|
+
if (!b.patternAvailableOnDay(pattern, day)) continue;
|
|
83
|
+
terms.push({ var: b.assignment(emp.id, pattern.id, day), coeff: 1 });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (terms.length === 0) continue;
|
|
88
|
+
|
|
89
|
+
const weekLabel = weekDays[0]!;
|
|
90
|
+
const constraintId = `must-assign:${emp.id}:${weekLabel}`;
|
|
91
|
+
|
|
92
|
+
b.addSoftLinear(terms, ">=", 1, MUST_ASSIGN_PENALTY, constraintId);
|
|
93
|
+
b.reporter.trackConstraint({
|
|
94
|
+
id: constraintId,
|
|
95
|
+
type: "rule",
|
|
96
|
+
rule: "must-assign",
|
|
97
|
+
description: `${emp.id} not assigned in week starting ${weekLabel} (staffing obligation)`,
|
|
98
|
+
targetValue: 1,
|
|
99
|
+
comparator: ">=",
|
|
100
|
+
day: weekLabel,
|
|
101
|
+
context: { memberIds: [emp.id], days: weekDays },
|
|
102
|
+
group,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolveMembersFromScope,
|
|
18
18
|
resolveActiveDaysFromScope,
|
|
19
19
|
} from "./scope.types.js";
|
|
20
|
+
import { getHourlyRate, patternDurationMinutes } from "./cost-utils.js";
|
|
20
21
|
|
|
21
22
|
const OvertimeDailyMultiplierSchema = z
|
|
22
23
|
.object({
|
|
@@ -29,18 +30,6 @@ const OvertimeDailyMultiplierSchema = z
|
|
|
29
30
|
/** Configuration for {@link createOvertimeDailyMultiplierRule}. */
|
|
30
31
|
export type OvertimeDailyMultiplierConfig = z.infer<typeof OvertimeDailyMultiplierSchema>;
|
|
31
32
|
|
|
32
|
-
function getHourlyRate(emp: SchedulingMember): number | undefined {
|
|
33
|
-
if (!emp.pay) return undefined;
|
|
34
|
-
if ("hourlyRate" in emp.pay) return emp.pay.hourlyRate;
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function patternDurationMinutes(pattern: ShiftPattern): number {
|
|
39
|
-
const start = timeOfDayToMinutes(pattern.startTime);
|
|
40
|
-
const end = normalizeEndMinutes(start, timeOfDayToMinutes(pattern.endTime));
|
|
41
|
-
return end - start;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
33
|
/**
|
|
45
34
|
* Creates a daily overtime rate multiplier rule.
|
|
46
35
|
*
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolveMembersFromScope,
|
|
18
18
|
resolveActiveDaysFromScope,
|
|
19
19
|
} from "./scope.types.js";
|
|
20
|
+
import { patternDurationMinutes } from "./cost-utils.js";
|
|
20
21
|
|
|
21
22
|
const OvertimeDailySurchargeSchema = z
|
|
22
23
|
.object({
|
|
@@ -29,12 +30,6 @@ const OvertimeDailySurchargeSchema = z
|
|
|
29
30
|
/** Configuration for {@link createOvertimeDailySurchargeRule}. */
|
|
30
31
|
export type OvertimeDailySurchargeConfig = z.infer<typeof OvertimeDailySurchargeSchema>;
|
|
31
32
|
|
|
32
|
-
function patternDurationMinutes(pattern: ShiftPattern): number {
|
|
33
|
-
const start = timeOfDayToMinutes(pattern.startTime);
|
|
34
|
-
const end = normalizeEndMinutes(start, timeOfDayToMinutes(pattern.endTime));
|
|
35
|
-
return end - start;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
/**
|
|
39
34
|
* Creates a daily overtime flat surcharge rule.
|
|
40
35
|
*
|
|
@@ -19,13 +19,18 @@ import {
|
|
|
19
19
|
resolveMembersFromScope,
|
|
20
20
|
resolveActiveDaysFromScope,
|
|
21
21
|
} from "./scope.types.js";
|
|
22
|
+
import { getHourlyRate, patternDurationMinutes } from "./cost-utils.js";
|
|
22
23
|
|
|
23
24
|
const OvertimeTierSchema = z.object({
|
|
24
25
|
after: z.number().min(0),
|
|
25
26
|
factor: z.number().min(1),
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* A single tier in a tiered overtime configuration.
|
|
31
|
+
*
|
|
32
|
+
* @category Cost Optimization
|
|
33
|
+
*/
|
|
29
34
|
export type OvertimeTier = z.infer<typeof OvertimeTierSchema>;
|
|
30
35
|
|
|
31
36
|
const OvertimeTieredMultiplierSchema = z
|
|
@@ -39,18 +44,6 @@ const OvertimeTieredMultiplierSchema = z
|
|
|
39
44
|
/** Configuration for {@link createOvertimeTieredMultiplierRule}. */
|
|
40
45
|
export type OvertimeTieredMultiplierConfig = z.infer<typeof OvertimeTieredMultiplierSchema>;
|
|
41
46
|
|
|
42
|
-
function getHourlyRate(emp: SchedulingMember): number | undefined {
|
|
43
|
-
if (!emp.pay) return undefined;
|
|
44
|
-
if ("hourlyRate" in emp.pay) return emp.pay.hourlyRate;
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function patternDurationMinutes(pattern: ShiftPattern): number {
|
|
49
|
-
const start = timeOfDayToMinutes(pattern.startTime);
|
|
50
|
-
const end = normalizeEndMinutes(start, timeOfDayToMinutes(pattern.endTime));
|
|
51
|
-
return end - start;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
47
|
/**
|
|
55
48
|
* Creates a multi-threshold weekly overtime multiplier rule.
|
|
56
49
|
*
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveMembersFromScope,
|
|
20
20
|
resolveActiveDaysFromScope,
|
|
21
21
|
} from "./scope.types.js";
|
|
22
|
+
import { getHourlyRate, patternDurationMinutes } from "./cost-utils.js";
|
|
22
23
|
|
|
23
24
|
const OvertimeWeeklyMultiplierSchema = z
|
|
24
25
|
.object({
|
|
@@ -32,18 +33,6 @@ const OvertimeWeeklyMultiplierSchema = z
|
|
|
32
33
|
/** Configuration for {@link createOvertimeWeeklyMultiplierRule}. */
|
|
33
34
|
export type OvertimeWeeklyMultiplierConfig = z.infer<typeof OvertimeWeeklyMultiplierSchema>;
|
|
34
35
|
|
|
35
|
-
function getHourlyRate(emp: SchedulingMember): number | undefined {
|
|
36
|
-
if (!emp.pay) return undefined;
|
|
37
|
-
if ("hourlyRate" in emp.pay) return emp.pay.hourlyRate;
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function patternDurationMinutes(pattern: ShiftPattern): number {
|
|
42
|
-
const start = timeOfDayToMinutes(pattern.startTime);
|
|
43
|
-
const end = normalizeEndMinutes(start, timeOfDayToMinutes(pattern.endTime));
|
|
44
|
-
return end - start;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
36
|
/**
|
|
48
37
|
* Creates a weekly overtime rate multiplier rule.
|
|
49
38
|
*
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveMembersFromScope,
|
|
20
20
|
resolveActiveDaysFromScope,
|
|
21
21
|
} from "./scope.types.js";
|
|
22
|
+
import { patternDurationMinutes } from "./cost-utils.js";
|
|
22
23
|
|
|
23
24
|
const OvertimeWeeklySurchargeSchema = z
|
|
24
25
|
.object({
|
|
@@ -32,12 +33,6 @@ const OvertimeWeeklySurchargeSchema = z
|
|
|
32
33
|
/** Configuration for {@link createOvertimeWeeklySurchargeRule}. */
|
|
33
34
|
export type OvertimeWeeklySurchargeConfig = z.infer<typeof OvertimeWeeklySurchargeSchema>;
|
|
34
35
|
|
|
35
|
-
function patternDurationMinutes(pattern: ShiftPattern): number {
|
|
36
|
-
const start = timeOfDayToMinutes(pattern.startTime);
|
|
37
|
-
const end = normalizeEndMinutes(start, timeOfDayToMinutes(pattern.endTime));
|
|
38
|
-
return end - start;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
36
|
/**
|
|
42
37
|
* Creates a weekly overtime flat surcharge rule.
|
|
43
38
|
*
|
|
@@ -3,13 +3,16 @@ import {
|
|
|
3
3
|
createAssignmentPriorityRule,
|
|
4
4
|
createLocationPreferenceRule,
|
|
5
5
|
createMaxConsecutiveDaysRule,
|
|
6
|
+
createMaxDaysWeekRule,
|
|
6
7
|
createMaxHoursDayRule,
|
|
7
8
|
createMaxHoursWeekRule,
|
|
8
9
|
createMaxShiftsDayRule,
|
|
9
10
|
createMinConsecutiveDaysRule,
|
|
11
|
+
createMinDaysWeekRule,
|
|
10
12
|
createMinHoursDayRule,
|
|
11
13
|
createMinHoursWeekRule,
|
|
12
14
|
createMinRestBetweenShiftsRule,
|
|
15
|
+
createMustAssignRule,
|
|
13
16
|
createTimeOffRule,
|
|
14
17
|
createMinimizeCostRule,
|
|
15
18
|
createDayCostMultiplierRule,
|
|
@@ -27,18 +30,21 @@ import type {
|
|
|
27
30
|
CpsatRuleName,
|
|
28
31
|
} from "./rules.types.js";
|
|
29
32
|
|
|
30
|
-
export const builtInCpsatRuleFactories
|
|
33
|
+
export const builtInCpsatRuleFactories = {
|
|
31
34
|
"assign-together": createAssignTogetherRule,
|
|
32
35
|
"assignment-priority": createAssignmentPriorityRule,
|
|
33
36
|
"location-preference": createLocationPreferenceRule,
|
|
34
37
|
"max-consecutive-days": createMaxConsecutiveDaysRule,
|
|
38
|
+
"max-days-week": createMaxDaysWeekRule,
|
|
35
39
|
"max-hours-day": createMaxHoursDayRule,
|
|
36
40
|
"max-hours-week": createMaxHoursWeekRule,
|
|
37
41
|
"max-shifts-day": createMaxShiftsDayRule,
|
|
38
42
|
"min-consecutive-days": createMinConsecutiveDaysRule,
|
|
43
|
+
"min-days-week": createMinDaysWeekRule,
|
|
39
44
|
"min-hours-day": createMinHoursDayRule,
|
|
40
45
|
"min-hours-week": createMinHoursWeekRule,
|
|
41
46
|
"min-rest-between-shifts": createMinRestBetweenShiftsRule,
|
|
47
|
+
"must-assign": createMustAssignRule,
|
|
42
48
|
"time-off": createTimeOffRule,
|
|
43
49
|
"minimize-cost": createMinimizeCostRule,
|
|
44
50
|
"day-cost-multiplier": createDayCostMultiplierRule,
|
|
@@ -49,7 +55,7 @@ export const builtInCpsatRuleFactories: BuiltInCpsatRuleFactories = {
|
|
|
49
55
|
"overtime-daily-multiplier": createOvertimeDailyMultiplierRule,
|
|
50
56
|
"overtime-daily-surcharge": createOvertimeDailySurchargeRule,
|
|
51
57
|
"overtime-tiered-multiplier": createOvertimeTieredMultiplierRule,
|
|
52
|
-
};
|
|
58
|
+
} satisfies BuiltInCpsatRuleFactories;
|
|
53
59
|
|
|
54
60
|
/**
|
|
55
61
|
* Creates a rule factory map, preventing overriding built-in rules.
|
|
@@ -45,11 +45,11 @@ export function getMemberIdsForScope(
|
|
|
45
45
|
}
|
|
46
46
|
case "roles":
|
|
47
47
|
return members
|
|
48
|
-
.filter((e) => e.
|
|
48
|
+
.filter((e) => e.roleIds.some((r) => entity.roleIds.includes(r)))
|
|
49
49
|
.map((e) => e.id);
|
|
50
50
|
case "skills":
|
|
51
51
|
return members
|
|
52
|
-
.filter((e) => e.
|
|
52
|
+
.filter((e) => e.skillIds?.some((s) => entity.skillIds.includes(s)))
|
|
53
53
|
.map((e) => e.id);
|
|
54
54
|
case "global":
|
|
55
55
|
default:
|
|
@@ -8,13 +8,16 @@ export interface CpsatRuleRegistry {
|
|
|
8
8
|
"assignment-priority": import("./assignment-priority.js").AssignmentPriorityConfig;
|
|
9
9
|
"location-preference": import("./location-preference.js").LocationPreferenceConfig;
|
|
10
10
|
"max-consecutive-days": import("./max-consecutive-days.js").MaxConsecutiveDaysConfig;
|
|
11
|
+
"max-days-week": import("./max-days-week.js").MaxDaysWeekConfig;
|
|
11
12
|
"max-hours-day": import("./max-hours-day.js").MaxHoursDayConfig;
|
|
12
13
|
"max-hours-week": import("./max-hours-week.js").MaxHoursWeekConfig;
|
|
13
14
|
"max-shifts-day": import("./max-shifts-day.js").MaxShiftsDayConfig;
|
|
14
15
|
"min-consecutive-days": import("./min-consecutive-days.js").MinConsecutiveDaysConfig;
|
|
16
|
+
"min-days-week": import("./min-days-week.js").MinDaysWeekConfig;
|
|
15
17
|
"min-hours-day": import("./min-hours-day.js").MinHoursDayConfig;
|
|
16
18
|
"min-hours-week": import("./min-hours-week.js").MinHoursWeekConfig;
|
|
17
19
|
"min-rest-between-shifts": import("./min-rest-between-shifts.js").MinRestBetweenShiftsConfig;
|
|
20
|
+
"must-assign": import("./must-assign.js").MustAssignConfig;
|
|
18
21
|
"time-off": import("./time-off.js").TimeOffConfig;
|
|
19
22
|
"minimize-cost": import("./minimize-cost.js").MinimizeCostConfig;
|
|
20
23
|
"day-cost-multiplier": import("./day-cost-multiplier.js").DayCostMultiplierConfig;
|
|
@@ -35,8 +35,10 @@
|
|
|
35
35
|
|
|
36
36
|
import * as z from "zod";
|
|
37
37
|
import { DayOfWeekSchema, type DayOfWeek } from "../../types.js";
|
|
38
|
+
import { toDayOfWeekUTC } from "../../datetime.utils.js";
|
|
38
39
|
import type { SchedulingMember } from "../types.js";
|
|
39
40
|
import { parseDayString } from "../utils.js";
|
|
41
|
+
import { assertSafeKeySegments, type ValidationGroup } from "../validation.types.js";
|
|
40
42
|
|
|
41
43
|
// ============================================================================
|
|
42
44
|
// Priority
|
|
@@ -85,7 +87,11 @@ type InactiveEntityFields = {
|
|
|
85
87
|
|
|
86
88
|
type NonEmptyArray<T> = [T, ...T[]];
|
|
87
89
|
|
|
88
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Recurring calendar period for time scoping.
|
|
92
|
+
*
|
|
93
|
+
* @category Supporting Types
|
|
94
|
+
*/
|
|
89
95
|
export interface RecurringPeriod {
|
|
90
96
|
name: string;
|
|
91
97
|
startMonth: number;
|
|
@@ -470,9 +476,9 @@ export function resolveMembersFromScope(
|
|
|
470
476
|
return members.filter((e) => idSet.has(e.id));
|
|
471
477
|
}
|
|
472
478
|
case "roles":
|
|
473
|
-
return members.filter((e) => e.
|
|
479
|
+
return members.filter((e) => e.roleIds.some((r) => scope.roleIds.includes(r)));
|
|
474
480
|
case "skills":
|
|
475
|
-
return members.filter((e) => e.
|
|
481
|
+
return members.filter((e) => e.skillIds?.some((s) => scope.skillIds.includes(s)));
|
|
476
482
|
case "global":
|
|
477
483
|
default:
|
|
478
484
|
return members;
|
|
@@ -494,8 +500,7 @@ export function resolveActiveDaysFromScope(scope: ParsedTimeScope, allDays: stri
|
|
|
494
500
|
const targetDays = new Set(scope.days);
|
|
495
501
|
return allDays.filter((day) => {
|
|
496
502
|
const date = parseDayString(day);
|
|
497
|
-
|
|
498
|
-
return targetDays.has(dayName);
|
|
503
|
+
return targetDays.has(toDayOfWeekUTC(date));
|
|
499
504
|
});
|
|
500
505
|
}
|
|
501
506
|
case "recurring":
|
|
@@ -512,21 +517,6 @@ export function resolveActiveDaysFromScope(scope: ParsedTimeScope, allDays: stri
|
|
|
512
517
|
// Internal Helpers
|
|
513
518
|
// ============================================================================
|
|
514
519
|
|
|
515
|
-
type DayName = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
|
|
516
|
-
|
|
517
|
-
function getDayOfWeekName(dayIndex: number): DayName {
|
|
518
|
-
const names: Record<number, DayName> = {
|
|
519
|
-
0: "sunday",
|
|
520
|
-
1: "monday",
|
|
521
|
-
2: "tuesday",
|
|
522
|
-
3: "wednesday",
|
|
523
|
-
4: "thursday",
|
|
524
|
-
5: "friday",
|
|
525
|
-
6: "saturday",
|
|
526
|
-
};
|
|
527
|
-
return names[dayIndex % 7] ?? "sunday";
|
|
528
|
-
}
|
|
529
|
-
|
|
530
520
|
function isDateInRecurringPeriod(
|
|
531
521
|
month: number,
|
|
532
522
|
dayOfMonth: number,
|
|
@@ -546,3 +536,66 @@ function isDateInRecurringPeriod(
|
|
|
546
536
|
return true;
|
|
547
537
|
}
|
|
548
538
|
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Creates a validation group for a rule with a deterministic structural key.
|
|
542
|
+
*
|
|
543
|
+
* The key encodes the rule name (including parameters), entity scope, and
|
|
544
|
+
* time scope so that identical configurations always produce the same group.
|
|
545
|
+
*
|
|
546
|
+
* @param rule - Rule name with parameters, e.g. `"max-hours-week:40"`
|
|
547
|
+
* @param title - Human-readable label, e.g. `"Max 40h per week"`
|
|
548
|
+
* @param entity - Parsed entity scope (members, roles, skills, or global)
|
|
549
|
+
* @param time - Parsed time scope (optional)
|
|
550
|
+
*/
|
|
551
|
+
export function ruleGroup(
|
|
552
|
+
rule: string,
|
|
553
|
+
title: string,
|
|
554
|
+
entity: ParsedEntityScope,
|
|
555
|
+
time?: ParsedTimeScope,
|
|
556
|
+
): ValidationGroup {
|
|
557
|
+
let key = `rule:${rule}`;
|
|
558
|
+
|
|
559
|
+
switch (entity.type) {
|
|
560
|
+
case "global":
|
|
561
|
+
break;
|
|
562
|
+
case "members":
|
|
563
|
+
assertSafeKeySegments(entity.memberIds, "member ID");
|
|
564
|
+
key += `:members:${entity.memberIds.toSorted().join(",")}`;
|
|
565
|
+
break;
|
|
566
|
+
case "roles":
|
|
567
|
+
assertSafeKeySegments(entity.roleIds, "role ID");
|
|
568
|
+
key += `:roles:${entity.roleIds.toSorted().join(",")}`;
|
|
569
|
+
break;
|
|
570
|
+
case "skills":
|
|
571
|
+
assertSafeKeySegments(entity.skillIds, "skill ID");
|
|
572
|
+
key += `:skills:${entity.skillIds.toSorted().join(",")}`;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (time && time.type !== "none") {
|
|
577
|
+
switch (time.type) {
|
|
578
|
+
case "dateRange":
|
|
579
|
+
key += `:range:${time.start}..${time.end}`;
|
|
580
|
+
break;
|
|
581
|
+
case "specificDates":
|
|
582
|
+
key += `:dates:${time.dates.toSorted().join(",")}`;
|
|
583
|
+
break;
|
|
584
|
+
case "dayOfWeek":
|
|
585
|
+
key += `:dow:${time.days.toSorted().join(",")}`;
|
|
586
|
+
break;
|
|
587
|
+
case "recurring":
|
|
588
|
+
assertSafeKeySegments(
|
|
589
|
+
time.periods.map((p) => p.name),
|
|
590
|
+
"recurring period name",
|
|
591
|
+
);
|
|
592
|
+
key += `:recurring:${time.periods
|
|
593
|
+
.map((p) => p.name)
|
|
594
|
+
.toSorted()
|
|
595
|
+
.join(",")}`;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return { key, title };
|
|
601
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { CompilationRule, CostContribution } from "../model-builder.js";
|
|
|
3
3
|
import type { ShiftPattern, SchedulingMember } from "../types.js";
|
|
4
4
|
import type { TimeOfDay } from "../../types.js";
|
|
5
5
|
import type { ShiftAssignment } from "../response.js";
|
|
6
|
+
import { COST_CATEGORY } from "../cost.js";
|
|
6
7
|
import { timeOfDayToMinutes, normalizeEndMinutes, MINUTES_PER_DAY } from "../utils.js";
|
|
7
8
|
import {
|
|
8
9
|
entityScope,
|
|
@@ -158,7 +159,7 @@ export function createTimeCostSurchargeRule(config: TimeCostSurchargeConfig): Co
|
|
|
158
159
|
entries.push({
|
|
159
160
|
memberId: a.memberId,
|
|
160
161
|
day: a.day,
|
|
161
|
-
category:
|
|
162
|
+
category: COST_CATEGORY.PREMIUM,
|
|
162
163
|
amount,
|
|
163
164
|
});
|
|
164
165
|
}
|