dabke 0.78.2 → 0.80.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 +34 -0
- package/README.md +68 -31
- package/dist/client.types.d.ts +58 -0
- package/dist/client.types.d.ts.map +1 -1
- package/dist/client.types.js.map +1 -1
- package/dist/cpsat/model-builder.d.ts +13 -2
- package/dist/cpsat/model-builder.d.ts.map +1 -1
- package/dist/cpsat/model-builder.js.map +1 -1
- package/dist/cpsat/response.d.ts +12 -3
- package/dist/cpsat/response.d.ts.map +1 -1
- package/dist/cpsat/response.js.map +1 -1
- package/dist/cpsat/rules/assign-together.d.ts +7 -0
- package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
- package/dist/cpsat/rules/assign-together.js +1 -0
- package/dist/cpsat/rules/assign-together.js.map +1 -1
- package/dist/cpsat/rules/employee-assignment-priority.d.ts +11 -37
- package/dist/cpsat/rules/employee-assignment-priority.d.ts.map +1 -1
- package/dist/cpsat/rules/employee-assignment-priority.js +12 -104
- package/dist/cpsat/rules/employee-assignment-priority.js.map +1 -1
- package/dist/cpsat/rules/location-preference.d.ts +12 -10
- package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
- package/dist/cpsat/rules/location-preference.js +16 -14
- package/dist/cpsat/rules/location-preference.js.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.d.ts +12 -13
- package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.js +11 -12
- package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/max-hours-day.d.ts +12 -28
- package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-day.js +12 -95
- package/dist/cpsat/rules/max-hours-day.js.map +1 -1
- package/dist/cpsat/rules/max-hours-week.d.ts +14 -34
- package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-week.js +12 -103
- package/dist/cpsat/rules/max-hours-week.js.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.d.ts +14 -39
- package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.js +14 -106
- package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.d.ts +12 -13
- package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.js +11 -12
- package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/min-hours-day.d.ts +12 -13
- package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-day.js +11 -12
- package/dist/cpsat/rules/min-hours-day.js.map +1 -1
- package/dist/cpsat/rules/min-hours-week.d.ts +13 -13
- package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-week.js +10 -14
- package/dist/cpsat/rules/min-hours-week.js.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts +13 -14
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.js +11 -12
- package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
- package/dist/cpsat/rules/resolver.d.ts +2 -2
- package/dist/cpsat/rules/resolver.d.ts.map +1 -1
- package/dist/cpsat/rules/resolver.js +55 -30
- package/dist/cpsat/rules/resolver.js.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +267 -0
- package/dist/cpsat/rules/scope.types.d.ts.map +1 -0
- package/dist/cpsat/rules/scope.types.js +325 -0
- package/dist/cpsat/rules/scope.types.js.map +1 -0
- package/dist/cpsat/rules/time-off.d.ts +21 -25
- package/dist/cpsat/rules/time-off.d.ts.map +1 -1
- package/dist/cpsat/rules/time-off.js +20 -110
- package/dist/cpsat/rules/time-off.js.map +1 -1
- package/dist/cpsat/semantic-time.d.ts +2 -0
- package/dist/cpsat/semantic-time.d.ts.map +1 -1
- package/dist/cpsat/semantic-time.js +2 -4
- package/dist/cpsat/semantic-time.js.map +1 -1
- package/dist/cpsat/types.d.ts +22 -6
- package/dist/cpsat/types.d.ts.map +1 -1
- package/dist/cpsat/utils.d.ts +1 -1
- package/dist/cpsat/utils.js +1 -1
- package/dist/cpsat/validation-reporter.js +1 -1
- package/dist/cpsat/validation-reporter.js.map +1 -1
- package/dist/datetime.utils.d.ts +14 -14
- package/dist/datetime.utils.d.ts.map +1 -1
- package/dist/datetime.utils.js +26 -27
- package/dist/datetime.utils.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/llms.d.ts +1 -1
- package/dist/llms.d.ts.map +1 -1
- package/dist/llms.js +1 -1
- package/dist/llms.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/solver-container.js +3 -1
- package/dist/testing/solver-container.js.map +1 -1
- package/dist/types.d.ts +18 -20
- package/dist/types.d.ts.map +1 -1
- package/llms.txt +516 -263
- package/package.json +25 -25
- package/src/client.types.ts +58 -0
- package/src/cpsat/model-builder.ts +19 -7
- package/src/cpsat/response.ts +12 -3
- package/src/cpsat/rules/assign-together.ts +7 -0
- package/src/cpsat/rules/employee-assignment-priority.ts +28 -128
- package/src/cpsat/rules/location-preference.ts +24 -17
- package/src/cpsat/rules/max-consecutive-days.ts +19 -15
- package/src/cpsat/rules/max-hours-day.ts +29 -119
- package/src/cpsat/rules/max-hours-week.ts +42 -135
- package/src/cpsat/rules/max-shifts-day.ts +31 -130
- package/src/cpsat/rules/min-consecutive-days.ts +19 -15
- package/src/cpsat/rules/min-hours-day.ts +19 -15
- package/src/cpsat/rules/min-hours-week.ts +28 -26
- package/src/cpsat/rules/min-rest-between-shifts.ts +21 -17
- package/src/cpsat/rules/resolver.ts +66 -45
- package/src/cpsat/rules/scope.types.ts +534 -0
- package/src/cpsat/rules/time-off.ts +48 -145
- package/src/cpsat/semantic-time.ts +10 -8
- package/src/cpsat/types.ts +22 -6
- package/src/cpsat/utils.ts +1 -1
- package/src/cpsat/validation-reporter.ts +1 -1
- package/src/datetime.utils.ts +27 -29
- package/src/index.ts +11 -7
- package/src/llms.ts +1 -1
- package/src/testing/index.ts +1 -1
- package/src/testing/solver-container.ts +3 -3
- package/src/types.ts +27 -31
- package/dist/cpsat/rules/scoping.d.ts +0 -129
- package/dist/cpsat/rules/scoping.d.ts.map +0 -1
- package/dist/cpsat/rules/scoping.js +0 -190
- package/dist/cpsat/rules/scoping.js.map +0 -1
- package/src/cpsat/rules/scoping.ts +0 -340
package/package.json
CHANGED
|
@@ -1,41 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dabke",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.80.0",
|
|
5
4
|
"description": "Scheduling library powered by constraint programming (CP-SAT)",
|
|
6
|
-
"author": "Christian Klotz <hello@christianklotz.co.uk>",
|
|
7
|
-
"homepage": "https://github.com/christianklotz/dabke#readme",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/christianklotz/dabke.git"
|
|
11
|
-
},
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/christianklotz/dabke/issues"
|
|
14
|
-
},
|
|
15
5
|
"keywords": [
|
|
16
|
-
"scheduling",
|
|
17
|
-
"staff-scheduling",
|
|
18
6
|
"constraint-programming",
|
|
19
7
|
"cp-sat",
|
|
20
|
-
"workforce-management",
|
|
21
|
-
"optimization",
|
|
22
8
|
"operations-research",
|
|
23
|
-
"
|
|
9
|
+
"optimization",
|
|
10
|
+
"or-tools",
|
|
11
|
+
"scheduling",
|
|
12
|
+
"staff-scheduling",
|
|
13
|
+
"workforce-management"
|
|
24
14
|
],
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
"homepage": "https://github.com/christianklotz/dabke#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/christianklotz/dabke/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Christian Klotz <hello@christianklotz.co.uk>",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/christianklotz/dabke.git"
|
|
29
24
|
},
|
|
30
25
|
"files": [
|
|
26
|
+
"CHANGELOG.md",
|
|
31
27
|
"dist",
|
|
32
|
-
"src",
|
|
33
|
-
"solver",
|
|
34
|
-
"llms.txt",
|
|
35
28
|
"LICENSE",
|
|
29
|
+
"llms.txt",
|
|
36
30
|
"README.md",
|
|
37
|
-
"
|
|
31
|
+
"solver",
|
|
32
|
+
"src"
|
|
38
33
|
],
|
|
34
|
+
"type": "module",
|
|
35
|
+
"sideEffects": false,
|
|
39
36
|
"main": "dist/index.js",
|
|
40
37
|
"types": "dist/index.d.ts",
|
|
41
38
|
"exports": {
|
|
@@ -72,5 +69,8 @@
|
|
|
72
69
|
"commander": "^14.0.2",
|
|
73
70
|
"typescript": "^5.9.3",
|
|
74
71
|
"vitest": "^4.0.18"
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": ">=20"
|
|
75
75
|
}
|
|
76
|
-
}
|
|
76
|
+
}
|
package/src/client.types.ts
CHANGED
|
@@ -22,20 +22,78 @@ import type {
|
|
|
22
22
|
// Types derived from Zod schemas
|
|
23
23
|
// --------------------------------------------------------------------------
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* A single linear term in a constraint or objective.
|
|
27
|
+
*
|
|
28
|
+
* - `var` (required): variable name
|
|
29
|
+
* - `coeff` (required): integer coefficient
|
|
30
|
+
*/
|
|
25
31
|
export type SolverTerm = z.infer<typeof SolverTermSchema>;
|
|
26
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A decision variable in the CP-SAT model.
|
|
35
|
+
*
|
|
36
|
+
* - `name` (required): unique variable identifier
|
|
37
|
+
* - `lb` (required): lower bound
|
|
38
|
+
* - `ub` (required): upper bound
|
|
39
|
+
* - `isBoolean` (optional): whether this is a boolean variable
|
|
40
|
+
* - `isInterval` (optional): whether this is an interval variable
|
|
41
|
+
* - `start`, `end`, `size`, `presenceVar` (optional): interval variable fields
|
|
42
|
+
*/
|
|
27
43
|
export type SolverVariable = z.infer<typeof SolverVariableSchema>;
|
|
28
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A constraint in the CP-SAT model.
|
|
47
|
+
*
|
|
48
|
+
* - `name` (required): constraint identifier
|
|
49
|
+
* - `type` (required): constraint kind (e.g. "linear", "bool_and", "no_overlap")
|
|
50
|
+
* - Additional fields vary by constraint type
|
|
51
|
+
*/
|
|
29
52
|
export type SolverConstraint = z.infer<typeof SolverConstraintSchema>;
|
|
30
53
|
|
|
54
|
+
/**
|
|
55
|
+
* An optimization objective for the solver.
|
|
56
|
+
*
|
|
57
|
+
* - `terms` (required): linear terms to minimize/maximize
|
|
58
|
+
* - `minimize` (required): whether to minimize (true) or maximize (false)
|
|
59
|
+
*/
|
|
31
60
|
export type SolverObjective = z.infer<typeof SolverObjectiveSchema>;
|
|
32
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The full request payload sent to the CP-SAT solver service.
|
|
64
|
+
*
|
|
65
|
+
* - `variables` (required): all decision variables
|
|
66
|
+
* - `constraints` (required): all constraints
|
|
67
|
+
* - `objective` (optional): optimization objective
|
|
68
|
+
* - `timeoutSeconds` (optional): solver time limit
|
|
69
|
+
*/
|
|
33
70
|
export type SolverRequest = z.infer<typeof SolverRequestSchema>;
|
|
34
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The response payload returned by the CP-SAT solver service.
|
|
74
|
+
*
|
|
75
|
+
* - `status` (required): solve outcome (see {@link SolverStatus})
|
|
76
|
+
* - `values` (optional): variable assignments when a solution is found
|
|
77
|
+
* - `statistics` (optional): solve time, conflicts, branches
|
|
78
|
+
* - `softViolations` (optional): which soft constraints were violated
|
|
79
|
+
* - `error` (optional): error message on failure
|
|
80
|
+
* - `solutionInfo` (optional): solver diagnostic info
|
|
81
|
+
*/
|
|
35
82
|
export type SolverResponse = z.infer<typeof SolverResponseSchema>;
|
|
36
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Solver outcome status.
|
|
86
|
+
*
|
|
87
|
+
* One of `"OPTIMAL"`, `"FEASIBLE"`, `"INFEASIBLE"`, `"TIMEOUT"`, or `"ERROR"`.
|
|
88
|
+
*/
|
|
37
89
|
export type SolverStatus = z.infer<typeof SolverStatusSchema>;
|
|
38
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A soft constraint violation reported by the solver.
|
|
93
|
+
*
|
|
94
|
+
* - `constraintId` (required): which soft constraint was violated
|
|
95
|
+
* - `violationAmount` (required): magnitude of the violation
|
|
96
|
+
*/
|
|
39
97
|
export type SoftConstraintViolation = z.infer<typeof SoftConstraintViolationSchema>;
|
|
40
98
|
|
|
41
99
|
// --------------------------------------------------------------------------
|
|
@@ -37,8 +37,17 @@ export interface RuleValidationContext {
|
|
|
37
37
|
readonly shiftPatterns: ShiftPattern[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A rule that adds constraints or objectives to the solver model.
|
|
42
|
+
*
|
|
43
|
+
* Rules implement `compile` to emit solver constraints during model building,
|
|
44
|
+
* and optionally `validate` to check the solution after solving.
|
|
45
|
+
* Use the `create*Rule` functions to create built-in rules.
|
|
46
|
+
*/
|
|
40
47
|
export interface CompilationRule {
|
|
48
|
+
/** Emit constraints and objectives into the model builder. */
|
|
41
49
|
compile(builder: ModelBuilder): void;
|
|
50
|
+
/** Validate the solved schedule and report violations. */
|
|
42
51
|
validate?(
|
|
43
52
|
assignments: ResolvedShiftAssignment[],
|
|
44
53
|
reporter: ValidationReporter,
|
|
@@ -69,11 +78,13 @@ export interface CompilationResult {
|
|
|
69
78
|
* ```
|
|
70
79
|
*/
|
|
71
80
|
export interface ModelBuilderConfig extends ModelBuilderOptions {
|
|
81
|
+
/** Team members available for scheduling. */
|
|
72
82
|
employees: SchedulingEmployee[];
|
|
83
|
+
/** Available shift patterns (time slots) that employees can be assigned to. */
|
|
73
84
|
shiftPatterns: ShiftPattern[];
|
|
74
85
|
/**
|
|
75
|
-
* Defines when scheduling should occur
|
|
76
|
-
*
|
|
86
|
+
* Defines when scheduling should occur as a date range with optional
|
|
87
|
+
* `daysOfWeek` and `dates` filters that compose to narrow which days are included.
|
|
77
88
|
*/
|
|
78
89
|
schedulingPeriod: SchedulingPeriod;
|
|
79
90
|
coverage: CoverageRequirement[];
|
|
@@ -448,11 +459,12 @@ export class ModelBuilder {
|
|
|
448
459
|
const covStart = timeOfDayToMinutes(cov.startTime);
|
|
449
460
|
const covEnd = normalizeEndMinutes(covStart, timeOfDayToMinutes(cov.endTime));
|
|
450
461
|
const covKey = cov.roleIds?.join(",") ?? cov.skillIds?.join(",") ?? "unknown";
|
|
451
|
-
const coverageLabel =
|
|
452
|
-
|
|
453
|
-
?
|
|
454
|
-
|
|
455
|
-
|
|
462
|
+
const coverageLabel =
|
|
463
|
+
cov.roleIds && cov.roleIds.length > 0
|
|
464
|
+
? cov.roleIds.length === 1
|
|
465
|
+
? `role "${cov.roleIds[0]}"`
|
|
466
|
+
: `role "${cov.roleIds.join(" or ")}"`
|
|
467
|
+
: `skills [${cov.skillIds?.join(", ")}]`;
|
|
456
468
|
const coverageWindow = formatTimeRange(covStart, covEnd);
|
|
457
469
|
|
|
458
470
|
const eligibleEmployees = this.employeesForCoverage(cov);
|
package/src/cpsat/response.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { SolverResponse } from "../client.types.js";
|
|
|
2
2
|
import type { ShiftPattern } from "./types.js";
|
|
3
3
|
import type { TimeOfDay } from "../types.js";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* A shift assignment extracted from the solver response.
|
|
7
|
-
*/
|
|
5
|
+
/** A raw assignment from the solver: which employee works which shift on which day. */
|
|
8
6
|
export interface ShiftAssignment {
|
|
7
|
+
/** The assigned employee's ID. */
|
|
9
8
|
employeeId: string;
|
|
9
|
+
/** The shift pattern this employee is assigned to. */
|
|
10
10
|
shiftPatternId: string;
|
|
11
|
+
/** The date of the assignment (YYYY-MM-DD). */
|
|
11
12
|
day: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -15,9 +16,13 @@ export interface ShiftAssignment {
|
|
|
15
16
|
* A shift assignment with resolved times.
|
|
16
17
|
*/
|
|
17
18
|
export interface ResolvedShiftAssignment {
|
|
19
|
+
/** The assigned employee's ID. */
|
|
18
20
|
employeeId: string;
|
|
21
|
+
/** The date of the assignment (YYYY-MM-DD). */
|
|
19
22
|
day: string;
|
|
23
|
+
/** When the shift starts. */
|
|
20
24
|
startTime: TimeOfDay;
|
|
25
|
+
/** When the shift ends. */
|
|
21
26
|
endTime: TimeOfDay;
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -25,9 +30,13 @@ export interface ResolvedShiftAssignment {
|
|
|
25
30
|
* Parsed solver result with assignments and metadata.
|
|
26
31
|
*/
|
|
27
32
|
export interface SolverResult {
|
|
33
|
+
/** The solver outcome: OPTIMAL, FEASIBLE, INFEASIBLE, TIMEOUT, or ERROR. */
|
|
28
34
|
status: SolverResponse["status"];
|
|
35
|
+
/** The shift assignments extracted from the solution. */
|
|
29
36
|
assignments: ShiftAssignment[];
|
|
37
|
+
/** Solver performance statistics (branches, conflicts, solve time). */
|
|
30
38
|
statistics?: SolverResponse["statistics"];
|
|
39
|
+
/** Error message if the solver returned an error status. */
|
|
31
40
|
error?: string;
|
|
32
41
|
}
|
|
33
42
|
|
|
@@ -17,12 +17,19 @@ const AssignTogetherSchema = z.object({
|
|
|
17
17
|
]),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for {@link createAssignTogetherRule}.
|
|
22
|
+
*
|
|
23
|
+
* - `groupEmployeeIds` (required): employee IDs to assign together (at least two, must be unique)
|
|
24
|
+
* - `priority` (required): how strictly the solver enforces this rule
|
|
25
|
+
*/
|
|
20
26
|
export type AssignTogetherConfig = z.infer<typeof AssignTogetherSchema>;
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Encourages or enforces that team members in the group work the same shift patterns on a day.
|
|
24
30
|
* For each pair of team members in the group, ensures they are assigned to the same shifts.
|
|
25
31
|
*
|
|
32
|
+
* @param config - See {@link AssignTogetherConfig}
|
|
26
33
|
* @example
|
|
27
34
|
* ```ts
|
|
28
35
|
* const rule = createAssignTogetherRule({
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import type { CompilationRule } from "../model-builder.js";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { OBJECTIVE_WEIGHTS } from "../utils.js";
|
|
4
|
+
import {
|
|
5
|
+
entityScope,
|
|
6
|
+
timeScope,
|
|
7
|
+
parseEntityScope,
|
|
8
|
+
parseTimeScope,
|
|
9
|
+
resolveEmployeesFromScope,
|
|
10
|
+
resolveActiveDaysFromScope,
|
|
11
|
+
} from "./scope.types.js";
|
|
12
|
+
|
|
13
|
+
const EmployeeAssignmentPrioritySchema = z
|
|
14
|
+
.object({
|
|
9
15
|
preference: z.union([z.literal("high"), z.literal("low")]),
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
times: ["dateRange", "specificDates", "dayOfWeek", "recurring"],
|
|
14
|
-
},
|
|
15
|
-
);
|
|
16
|
+
})
|
|
17
|
+
.and(entityScope(["employees", "roles", "skills"]))
|
|
18
|
+
.and(timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]));
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for {@link createEmployeeAssignmentPriorityRule}.
|
|
22
|
+
*
|
|
23
|
+
* - `preference` (required): `"high"` to prefer assigning or `"low"` to avoid assigning
|
|
24
|
+
*
|
|
25
|
+
* Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
|
|
26
|
+
* Time scoping (at most one, optional): `dateRange`, `specificDates`, `dayOfWeek`, `recurringPeriods`
|
|
27
|
+
*/
|
|
17
28
|
export type EmployeeAssignmentPriorityConfig = z.infer<typeof EmployeeAssignmentPrioritySchema>;
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
31
|
* Adds objective weight to prefer or avoid assigning team members.
|
|
21
32
|
*
|
|
22
|
-
*
|
|
23
|
-
* (date ranges, specific dates, days of week, recurring periods).
|
|
24
|
-
*
|
|
33
|
+
* @param config - See {@link EmployeeAssignmentPriorityConfig}
|
|
25
34
|
* @example Prefer specific team members
|
|
26
35
|
* ```ts
|
|
27
36
|
* createEmployeeAssignmentPriorityRule({
|
|
@@ -38,27 +47,19 @@ export type EmployeeAssignmentPriorityConfig = z.infer<typeof EmployeeAssignment
|
|
|
38
47
|
* preference: "low",
|
|
39
48
|
* });
|
|
40
49
|
* ```
|
|
41
|
-
*
|
|
42
|
-
* @example Prefer experienced staff on weekends
|
|
43
|
-
* ```ts
|
|
44
|
-
* createEmployeeAssignmentPriorityRule({
|
|
45
|
-
* skillIds: ["senior"],
|
|
46
|
-
* dayOfWeek: ["saturday", "sunday"],
|
|
47
|
-
* preference: "high",
|
|
48
|
-
* });
|
|
49
|
-
* ```
|
|
50
50
|
*/
|
|
51
51
|
export function createEmployeeAssignmentPriorityRule(
|
|
52
52
|
config: EmployeeAssignmentPriorityConfig,
|
|
53
53
|
): CompilationRule {
|
|
54
54
|
const parsed = EmployeeAssignmentPrioritySchema.parse(config);
|
|
55
55
|
const { preference } = parsed;
|
|
56
|
+
const entityScopeValue = parseEntityScope(parsed);
|
|
57
|
+
const timeScopeValue = parseTimeScope(parsed);
|
|
56
58
|
|
|
57
59
|
return {
|
|
58
60
|
compile(b) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const activeDays = resolveActiveDays(scope, b.days);
|
|
61
|
+
const targetEmployees = resolveEmployeesFromScope(entityScopeValue, b.employees);
|
|
62
|
+
const activeDays = resolveActiveDaysFromScope(timeScopeValue, b.days);
|
|
62
63
|
|
|
63
64
|
if (targetEmployees.length === 0 || activeDays.length === 0) return;
|
|
64
65
|
|
|
@@ -79,104 +80,3 @@ export function createEmployeeAssignmentPriorityRule(
|
|
|
79
80
|
},
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
-
function resolveEmployees(scope: RuleScope, employees: SchedulingEmployee[]): SchedulingEmployee[] {
|
|
84
|
-
const entity = scope.entity;
|
|
85
|
-
switch (entity.type) {
|
|
86
|
-
case "employees":
|
|
87
|
-
return employees.filter((e) => entity.employeeIds.includes(e.id));
|
|
88
|
-
case "roles":
|
|
89
|
-
return employees.filter((e) => e.roleIds.some((r) => entity.roleIds.includes(r)));
|
|
90
|
-
case "skills":
|
|
91
|
-
return employees.filter((e) => e.skillIds?.some((s) => entity.skillIds.includes(s)));
|
|
92
|
-
case "global":
|
|
93
|
-
default:
|
|
94
|
-
return employees;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
type DayName = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
|
|
99
|
-
|
|
100
|
-
function getDayOfWeekName(dayIndex: number): DayName {
|
|
101
|
-
const names: Record<number, DayName> = {
|
|
102
|
-
0: "sunday",
|
|
103
|
-
1: "monday",
|
|
104
|
-
2: "tuesday",
|
|
105
|
-
3: "wednesday",
|
|
106
|
-
4: "thursday",
|
|
107
|
-
5: "friday",
|
|
108
|
-
6: "saturday",
|
|
109
|
-
};
|
|
110
|
-
return names[dayIndex % 7] ?? "sunday";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function resolveActiveDays(scope: RuleScope, allDays: string[]): string[] {
|
|
114
|
-
const timeScope = scope.time;
|
|
115
|
-
|
|
116
|
-
if (!timeScope) {
|
|
117
|
-
return allDays;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
switch (timeScope.type) {
|
|
121
|
-
case "always":
|
|
122
|
-
return allDays;
|
|
123
|
-
|
|
124
|
-
case "dateRange": {
|
|
125
|
-
const start = timeScope.start;
|
|
126
|
-
const end = timeScope.end;
|
|
127
|
-
return allDays.filter((day) => day >= start && day <= end);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
case "specificDates":
|
|
131
|
-
return allDays.filter((day) => timeScope.dates.includes(day));
|
|
132
|
-
|
|
133
|
-
case "dayOfWeek": {
|
|
134
|
-
const targetDays = new Set(timeScope.days);
|
|
135
|
-
return allDays.filter((day) => {
|
|
136
|
-
const date = parseDayString(day);
|
|
137
|
-
const dayName = getDayOfWeekName(date.getUTCDay());
|
|
138
|
-
return targetDays.has(dayName);
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
case "recurring": {
|
|
143
|
-
return allDays.filter((day) => {
|
|
144
|
-
const date = parseDayString(day);
|
|
145
|
-
const month = date.getUTCMonth() + 1;
|
|
146
|
-
const dayOfMonth = date.getUTCDate();
|
|
147
|
-
|
|
148
|
-
return timeScope.periods.some((period) =>
|
|
149
|
-
isDateInRecurringPeriod(month, dayOfMonth, period),
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
default:
|
|
155
|
-
return allDays;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function isDateInRecurringPeriod(
|
|
160
|
-
month: number,
|
|
161
|
-
dayOfMonth: number,
|
|
162
|
-
period: {
|
|
163
|
-
startMonth: number;
|
|
164
|
-
startDay: number;
|
|
165
|
-
endMonth: number;
|
|
166
|
-
endDay: number;
|
|
167
|
-
},
|
|
168
|
-
): boolean {
|
|
169
|
-
const { startMonth, startDay, endMonth, endDay } = period;
|
|
170
|
-
|
|
171
|
-
if (startMonth <= endMonth) {
|
|
172
|
-
if (month < startMonth || month > endMonth) return false;
|
|
173
|
-
if (month === startMonth && dayOfMonth < startDay) return false;
|
|
174
|
-
if (month === endMonth && dayOfMonth > endDay) return false;
|
|
175
|
-
return true;
|
|
176
|
-
} else {
|
|
177
|
-
if (month > endMonth && month < startMonth) return false;
|
|
178
|
-
if (month === startMonth && dayOfMonth < startDay) return false;
|
|
179
|
-
if (month === endMonth && dayOfMonth > endDay) return false;
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
@@ -2,10 +2,10 @@ import * as z from "zod";
|
|
|
2
2
|
import type { CompilationRule } from "../model-builder.js";
|
|
3
3
|
import type { Priority } from "../types.js";
|
|
4
4
|
import { OBJECTIVE_WEIGHTS } from "../utils.js";
|
|
5
|
-
import {
|
|
5
|
+
import { entityScope, parseEntityScope, resolveEmployeesFromScope } from "./scope.types.js";
|
|
6
6
|
|
|
7
|
-
const LocationPreferenceSchema =
|
|
8
|
-
|
|
7
|
+
const LocationPreferenceSchema = z
|
|
8
|
+
.object({
|
|
9
9
|
locationId: z.string(),
|
|
10
10
|
priority: z.union([
|
|
11
11
|
z.literal("LOW"),
|
|
@@ -13,43 +13,50 @@ const LocationPreferenceSchema = withScopes(
|
|
|
13
13
|
z.literal("HIGH"),
|
|
14
14
|
z.literal("MANDATORY"),
|
|
15
15
|
]),
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
);
|
|
16
|
+
})
|
|
17
|
+
.and(entityScope(["employees", "roles", "skills"]));
|
|
19
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for {@link createLocationPreferenceRule}.
|
|
21
|
+
*
|
|
22
|
+
* - `locationId` (required): the location ID to prefer for matching shift patterns
|
|
23
|
+
* - `priority` (required): how strongly to prefer this location
|
|
24
|
+
*
|
|
25
|
+
* Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
|
|
26
|
+
*/
|
|
20
27
|
export type LocationPreferenceConfig = z.infer<typeof LocationPreferenceSchema>;
|
|
21
28
|
|
|
22
29
|
const PRIORITY_WEIGHTS: Record<Priority, number> = {
|
|
23
|
-
LOW: OBJECTIVE_WEIGHTS.FAIRNESS,
|
|
24
|
-
MEDIUM: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE,
|
|
25
|
-
HIGH: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 2.5,
|
|
26
|
-
MANDATORY: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 5,
|
|
30
|
+
LOW: OBJECTIVE_WEIGHTS.FAIRNESS,
|
|
31
|
+
MEDIUM: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE,
|
|
32
|
+
HIGH: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 2.5,
|
|
33
|
+
MANDATORY: OBJECTIVE_WEIGHTS.ASSIGNMENT_PREFERENCE * 5,
|
|
27
34
|
};
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
37
|
* Prefers assigning a person to shift patterns matching a specific location.
|
|
31
38
|
*
|
|
39
|
+
* @param config - See {@link LocationPreferenceConfig}
|
|
32
40
|
* @example
|
|
33
41
|
* ```ts
|
|
34
|
-
*
|
|
42
|
+
* createLocationPreferenceRule({
|
|
35
43
|
* locationId: "terrace",
|
|
36
44
|
* priority: "HIGH",
|
|
37
45
|
* employeeIds: ["alice"],
|
|
38
46
|
* });
|
|
39
|
-
* builder = new ModelBuilder({ ...config, rules: [rule] });
|
|
40
47
|
* ```
|
|
41
48
|
*/
|
|
42
49
|
export function createLocationPreferenceRule(config: LocationPreferenceConfig): CompilationRule {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
50
|
+
const parsed = LocationPreferenceSchema.parse(config);
|
|
51
|
+
const scope = parseEntityScope(parsed);
|
|
52
|
+
const { locationId } = parsed;
|
|
53
|
+
const weight = PRIORITY_WEIGHTS[parsed.priority] ?? 0;
|
|
45
54
|
|
|
46
55
|
return {
|
|
47
56
|
compile(b) {
|
|
48
57
|
if (weight === 0) return;
|
|
49
58
|
|
|
50
|
-
const employees =
|
|
51
|
-
? b.employees.filter((e) => employeeIds.includes(e.id))
|
|
52
|
-
: b.employees;
|
|
59
|
+
const employees = resolveEmployeesFromScope(scope, b.employees);
|
|
53
60
|
|
|
54
61
|
for (const emp of employees) {
|
|
55
62
|
for (const pattern of b.shiftPatterns) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import type { CompilationRule } from "../model-builder.js";
|
|
3
3
|
import { priorityToPenalty } from "../utils.js";
|
|
4
|
-
import {
|
|
4
|
+
import { entityScope, parseEntityScope, resolveEmployeesFromScope } from "./scope.types.js";
|
|
5
5
|
|
|
6
|
-
const MaxConsecutiveDaysSchema =
|
|
7
|
-
|
|
6
|
+
const MaxConsecutiveDaysSchema = z
|
|
7
|
+
.object({
|
|
8
8
|
days: z.number().min(0),
|
|
9
9
|
priority: z.union([
|
|
10
10
|
z.literal("LOW"),
|
|
@@ -12,33 +12,37 @@ const MaxConsecutiveDaysSchema = withScopes(
|
|
|
12
12
|
z.literal("HIGH"),
|
|
13
13
|
z.literal("MANDATORY"),
|
|
14
14
|
]),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
);
|
|
15
|
+
})
|
|
16
|
+
.and(entityScope(["employees", "roles", "skills"]));
|
|
18
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for {@link createMaxConsecutiveDaysRule}.
|
|
20
|
+
*
|
|
21
|
+
* - `days` (required): maximum consecutive days allowed
|
|
22
|
+
* - `priority` (required): how strictly the solver enforces this rule
|
|
23
|
+
*
|
|
24
|
+
* Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
|
|
25
|
+
*/
|
|
19
26
|
export type MaxConsecutiveDaysConfig = z.infer<typeof MaxConsecutiveDaysSchema>;
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* Limits how many consecutive days a person can be assigned.
|
|
23
30
|
*
|
|
31
|
+
* @param config - See {@link MaxConsecutiveDaysConfig}
|
|
24
32
|
* @example
|
|
25
33
|
* ```ts
|
|
26
|
-
*
|
|
27
|
-
* days: 5,
|
|
28
|
-
* priority: "MANDATORY",
|
|
29
|
-
* });
|
|
30
|
-
* builder = new ModelBuilder({ ...config, rules: [rule] });
|
|
34
|
+
* createMaxConsecutiveDaysRule({ days: 5, priority: "MANDATORY" });
|
|
31
35
|
* ```
|
|
32
36
|
*/
|
|
33
37
|
export function createMaxConsecutiveDaysRule(config: MaxConsecutiveDaysConfig): CompilationRule {
|
|
34
|
-
const
|
|
38
|
+
const parsed = MaxConsecutiveDaysSchema.parse(config);
|
|
39
|
+
const scope = parseEntityScope(parsed);
|
|
40
|
+
const { days, priority } = parsed;
|
|
35
41
|
const windowSize = days + 1;
|
|
36
42
|
|
|
37
43
|
return {
|
|
38
44
|
compile(b) {
|
|
39
|
-
const employees =
|
|
40
|
-
? b.employees.filter((e) => employeeIds.includes(e.id))
|
|
41
|
-
: b.employees;
|
|
45
|
+
const employees = resolveEmployeesFromScope(scope, b.employees);
|
|
42
46
|
|
|
43
47
|
for (const emp of employees) {
|
|
44
48
|
for (let i = 0; i <= b.days.length - windowSize; i++) {
|