placementt-core 1.400.1055 → 1.400.1056
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/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/typeDefinitions.d.ts +5 -0
- package/lib/workflowBands.d.ts +33 -0
- package/lib/workflowBands.js +78 -0
- package/lib/workflowBands.js.map +1 -0
- package/lib/workflowBands.test.d.ts +1 -0
- package/lib/workflowBands.test.js +97 -0
- package/lib/workflowBands.test.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/typeDefinitions.ts +5 -2
- package/src/workflowBands.test.ts +111 -0
- package/src/workflowBands.ts +82 -0
package/lib/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export * from "./tutorialTips";
|
|
|
29
29
|
export * from "./constants";
|
|
30
30
|
export * from "./dataViewerReload";
|
|
31
31
|
export * from "./hooks";
|
|
32
|
+
export * from "./workflowBands";
|
|
32
33
|
export * from "./util";
|
|
33
34
|
export * from "./images/GatsbyBenchmarks";
|
|
34
35
|
export * from "./databaseTypes";
|
package/lib/index.js
CHANGED
|
@@ -49,6 +49,7 @@ __exportStar(require("./tutorialTips"), exports);
|
|
|
49
49
|
__exportStar(require("./constants"), exports);
|
|
50
50
|
__exportStar(require("./dataViewerReload"), exports);
|
|
51
51
|
__exportStar(require("./hooks"), exports);
|
|
52
|
+
__exportStar(require("./workflowBands"), exports);
|
|
52
53
|
__exportStar(require("./util"), exports);
|
|
53
54
|
__exportStar(require("./images/GatsbyBenchmarks"), exports);
|
|
54
55
|
__exportStar(require("./databaseTypes"), exports);
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AACA,sDAAoC;AACpC,gEAA8C;AAC9C,0DAAwC;AACxC,2DAAyC;AACzC,kDAAgC;AAChC,uDAAqC;AACrC,4DAA0C;AAC1C,2DAAyC;AAEzC,oDAAkC;AAClC,8CAA4B;AAC5B,kDAAgC;AAChC,kDAAgC;AAChC,qDAAmC;AACnC,mDAAiC;AACjC,qDAAmC;AACnC,mDAAiC;AACjC,+CAA6B;AAC7B,wCAAsB;AACtB,oDAAkC;AAClC,4CAA0B;AAC1B,2CAAyB;AACzB,kDAAgC;AAChC,uDAAqC;AACrC,kDAAgC;AAChC,iDAA+B;AAC/B,2DAAyC;AACzC,uDAAqC;AACrC,iDAA+B;AAC/B,8CAA4B;AAC5B,qDAAmC;AACnC,0CAAwB;AACxB,yCAAuB;AACvB,4DAA0C;AAC1C,kDAAgC;AAChC,iDAA+B;AAC/B,2CAAyB;AACzB,4DAA0C;AAC1C,oEAAiD;AACjD,yEAAuD;AACvD,kEAAgD;AAChD,kEAAgD;AAChD,oEAAkD;AAClD,yGAAuF;AACvF,0GAAwF;AACxF,+FAA6E;AAC7E,4DAA0C;AAC1C,oEAAkD;AAClD,iEAA+C;AAC/C,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,8CAA4B;AAC5B,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AACA,sDAAoC;AACpC,gEAA8C;AAC9C,0DAAwC;AACxC,2DAAyC;AACzC,kDAAgC;AAChC,uDAAqC;AACrC,4DAA0C;AAC1C,2DAAyC;AAEzC,oDAAkC;AAClC,8CAA4B;AAC5B,kDAAgC;AAChC,kDAAgC;AAChC,qDAAmC;AACnC,mDAAiC;AACjC,qDAAmC;AACnC,mDAAiC;AACjC,+CAA6B;AAC7B,wCAAsB;AACtB,oDAAkC;AAClC,4CAA0B;AAC1B,2CAAyB;AACzB,kDAAgC;AAChC,uDAAqC;AACrC,kDAAgC;AAChC,iDAA+B;AAC/B,2DAAyC;AACzC,uDAAqC;AACrC,iDAA+B;AAC/B,8CAA4B;AAC5B,qDAAmC;AACnC,0CAAwB;AACxB,kDAAgC;AAChC,yCAAuB;AACvB,4DAA0C;AAC1C,kDAAgC;AAChC,iDAA+B;AAC/B,2CAAyB;AACzB,4DAA0C;AAC1C,oEAAiD;AACjD,yEAAuD;AACvD,kEAAgD;AAChD,kEAAgD;AAChD,oEAAkD;AAClD,yGAAuF;AACvF,0GAAwF;AACxF,+FAA6E;AAC7E,4DAA0C;AAC1C,oEAAkD;AAClD,iEAA+C;AAC/C,gEAA8C;AAC9C,2EAAyD;AACzD,6CAA2B;AAC3B,+CAA6B;AAC7B,8CAA4B;AAC5B,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB"}
|
package/lib/typeDefinitions.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export type Products = "institutes" | "providers" | "students" | "admin";
|
|
|
48
48
|
export type StudentPlacementData = {
|
|
49
49
|
title: string;
|
|
50
50
|
reqUserType: "Students" | "Staff" | "Provider" | "Parent";
|
|
51
|
+
reqUserTypes?: ("Students" | "Staff" | "Provider" | "Parent")[];
|
|
52
|
+
/** Set while a completion is claiming the band's single advance, so two
|
|
53
|
+
* parties finishing at once cannot both move the placement on. */
|
|
54
|
+
advancingFrom?: number;
|
|
51
55
|
lastStageChange?: string;
|
|
52
56
|
uid: string;
|
|
53
57
|
startDate: string;
|
|
@@ -751,6 +755,7 @@ export type WorkflowStage = {
|
|
|
751
755
|
y: number;
|
|
752
756
|
};
|
|
753
757
|
userType?: "Students" | "Staff" | "Provider" | "Parent";
|
|
758
|
+
concurrentWithNext?: boolean;
|
|
754
759
|
forms?: string[];
|
|
755
760
|
files?: string[];
|
|
756
761
|
deadline?: string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WorkflowStage } from "./typeDefinitions";
|
|
2
|
+
/**
|
|
3
|
+
* Concurrent sign-off bands.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately free of any firebase import so it can be unit tested and pulled
|
|
6
|
+
* into the backend, the external review payload and the staff UI alike.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The stage that starts the concurrent band `stageId` belongs to.
|
|
10
|
+
*
|
|
11
|
+
* A band is a run of stages chained by their first button where every stage but
|
|
12
|
+
* the last carries `concurrentWithNext`. The head is the stage a placement's
|
|
13
|
+
* `status` points at while the band is open, which is what lets `status` stay a
|
|
14
|
+
* single number: the band only changes who we ask, not where the placement is.
|
|
15
|
+
*/
|
|
16
|
+
export declare const getBandHead: (workflow: WorkflowStage[], stageId: number) => WorkflowStage | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Every stage that runs at the same time as `stageId`, in workflow order.
|
|
19
|
+
*
|
|
20
|
+
* Returns just the stage itself when nothing is marked concurrent, so callers can
|
|
21
|
+
* use this unconditionally and existing workflows behave exactly as before.
|
|
22
|
+
*/
|
|
23
|
+
export declare const getConcurrentBand: (workflow: WorkflowStage[], stageId: number) => WorkflowStage[];
|
|
24
|
+
/** True when the workflow asks more than one party at this point. */
|
|
25
|
+
export declare const isConcurrentBand: (workflow: WorkflowStage[], stageId: number) => boolean;
|
|
26
|
+
/** The band's stages that nobody has signed off yet. */
|
|
27
|
+
export declare const getBandOutstanding: (band: WorkflowStage[], completedStages?: number[]) => WorkflowStage[];
|
|
28
|
+
/** Distinct user types the band is still waiting on, in workflow order. */
|
|
29
|
+
export declare const getBandOutstandingUserTypes: (band: WorkflowStage[], completedStages?: number[]) => NonNullable<"Students" | "Staff" | "Provider" | "Parent" | undefined>[];
|
|
30
|
+
/** Where the placement goes once every stage in the band is signed off. */
|
|
31
|
+
export declare const getBandExitStageId: (band: WorkflowStage[]) => number | undefined;
|
|
32
|
+
/** The label to show staff for a band, e.g. "Employer review & parental consent". */
|
|
33
|
+
export declare const getBandName: (band: WorkflowStage[]) => string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getBandName = exports.getBandExitStageId = exports.getBandOutstandingUserTypes = exports.getBandOutstanding = exports.isConcurrentBand = exports.getConcurrentBand = exports.getBandHead = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Concurrent sign-off bands.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately free of any firebase import so it can be unit tested and pulled
|
|
8
|
+
* into the backend, the external review payload and the staff UI alike.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The stage that starts the concurrent band `stageId` belongs to.
|
|
12
|
+
*
|
|
13
|
+
* A band is a run of stages chained by their first button where every stage but
|
|
14
|
+
* the last carries `concurrentWithNext`. The head is the stage a placement's
|
|
15
|
+
* `status` points at while the band is open, which is what lets `status` stay a
|
|
16
|
+
* single number: the band only changes who we ask, not where the placement is.
|
|
17
|
+
*/
|
|
18
|
+
const getBandHead = (workflow, stageId) => {
|
|
19
|
+
let stage = workflow.find((s) => s.id === stageId);
|
|
20
|
+
if (!stage)
|
|
21
|
+
return undefined;
|
|
22
|
+
const visited = new Set([stage.id]);
|
|
23
|
+
for (;;) {
|
|
24
|
+
const previous = workflow.find((s) => s.concurrentWithNext && s.buttons?.[0]?.id === stage?.id);
|
|
25
|
+
// No flagged predecessor means we are already at the head.
|
|
26
|
+
if (!previous || visited.has(previous.id))
|
|
27
|
+
return stage;
|
|
28
|
+
visited.add(previous.id);
|
|
29
|
+
stage = previous;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
exports.getBandHead = getBandHead;
|
|
33
|
+
/**
|
|
34
|
+
* Every stage that runs at the same time as `stageId`, in workflow order.
|
|
35
|
+
*
|
|
36
|
+
* Returns just the stage itself when nothing is marked concurrent, so callers can
|
|
37
|
+
* use this unconditionally and existing workflows behave exactly as before.
|
|
38
|
+
*/
|
|
39
|
+
const getConcurrentBand = (workflow, stageId) => {
|
|
40
|
+
const head = (0, exports.getBandHead)(workflow, stageId);
|
|
41
|
+
if (!head)
|
|
42
|
+
return [];
|
|
43
|
+
const band = [head];
|
|
44
|
+
const visited = new Set([head.id]);
|
|
45
|
+
let stage = head;
|
|
46
|
+
while (stage?.concurrentWithNext) {
|
|
47
|
+
const nextId = stage.buttons?.[0]?.id;
|
|
48
|
+
// A flagged stage with nowhere to go, or a loop, ends the band rather
|
|
49
|
+
// than stranding the caller.
|
|
50
|
+
if (nextId === undefined || visited.has(nextId))
|
|
51
|
+
break;
|
|
52
|
+
stage = workflow.find((s) => s.id === nextId);
|
|
53
|
+
if (!stage)
|
|
54
|
+
break;
|
|
55
|
+
visited.add(stage.id);
|
|
56
|
+
band.push(stage);
|
|
57
|
+
}
|
|
58
|
+
return band;
|
|
59
|
+
};
|
|
60
|
+
exports.getConcurrentBand = getConcurrentBand;
|
|
61
|
+
/** True when the workflow asks more than one party at this point. */
|
|
62
|
+
const isConcurrentBand = (workflow, stageId) => (0, exports.getConcurrentBand)(workflow, stageId).length > 1;
|
|
63
|
+
exports.isConcurrentBand = isConcurrentBand;
|
|
64
|
+
/** The band's stages that nobody has signed off yet. */
|
|
65
|
+
const getBandOutstanding = (band, completedStages = []) => band.filter((stage) => !completedStages.includes(stage.id));
|
|
66
|
+
exports.getBandOutstanding = getBandOutstanding;
|
|
67
|
+
/** Distinct user types the band is still waiting on, in workflow order. */
|
|
68
|
+
const getBandOutstandingUserTypes = (band, completedStages = []) => (0, exports.getBandOutstanding)(band, completedStages)
|
|
69
|
+
.map((stage) => stage.userType)
|
|
70
|
+
.filter((userType, i, all) => Boolean(userType) && all.indexOf(userType) === i);
|
|
71
|
+
exports.getBandOutstandingUserTypes = getBandOutstandingUserTypes;
|
|
72
|
+
/** Where the placement goes once every stage in the band is signed off. */
|
|
73
|
+
const getBandExitStageId = (band) => band[band.length - 1]?.buttons?.[0]?.id;
|
|
74
|
+
exports.getBandExitStageId = getBandExitStageId;
|
|
75
|
+
/** The label to show staff for a band, e.g. "Employer review & parental consent". */
|
|
76
|
+
const getBandName = (band) => band.map((stage) => stage.name).join(" & ");
|
|
77
|
+
exports.getBandName = getBandName;
|
|
78
|
+
//# sourceMappingURL=workflowBands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflowBands.js","sourceRoot":"","sources":["../src/workflowBands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const workflowBands_1 = require("./workflowBands");
|
|
4
|
+
describe("Concurrent bands", () => {
|
|
5
|
+
// A typical work experience chain: staff check, then employer review, then
|
|
6
|
+
// parental consent, then the placement start stage.
|
|
7
|
+
const stage = (id, userType, next, concurrentWithNext) => ({
|
|
8
|
+
id,
|
|
9
|
+
name: `Stage ${id}`,
|
|
10
|
+
userType,
|
|
11
|
+
concurrentWithNext,
|
|
12
|
+
buttons: next === undefined ? [] : [{ id: next, name: "Continue", required: true }],
|
|
13
|
+
});
|
|
14
|
+
const sequential = [
|
|
15
|
+
stage(1, "Students", 2),
|
|
16
|
+
stage(2, "Staff", 3),
|
|
17
|
+
stage(3, "Provider", 4),
|
|
18
|
+
stage(4, "Parent", 6),
|
|
19
|
+
stage(6, undefined),
|
|
20
|
+
];
|
|
21
|
+
// Same workflow, but employer review and parental consent run together.
|
|
22
|
+
const banded = [
|
|
23
|
+
stage(1, "Students", 2),
|
|
24
|
+
stage(2, "Staff", 3),
|
|
25
|
+
stage(3, "Provider", 4, true),
|
|
26
|
+
stage(4, "Parent", 6),
|
|
27
|
+
stage(6, undefined),
|
|
28
|
+
];
|
|
29
|
+
it("returns just the stage when nothing is marked concurrent", () => {
|
|
30
|
+
expect((0, workflowBands_1.getConcurrentBand)(sequential, 3).map((s) => s.id)).toEqual([3]);
|
|
31
|
+
expect((0, workflowBands_1.getConcurrentBand)(sequential, 4).map((s) => s.id)).toEqual([4]);
|
|
32
|
+
expect((0, workflowBands_1.isConcurrentBand)(sequential, 3)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it("resolves the same band from any member", () => {
|
|
35
|
+
expect((0, workflowBands_1.getConcurrentBand)(banded, 3).map((s) => s.id)).toEqual([3, 4]);
|
|
36
|
+
expect((0, workflowBands_1.getConcurrentBand)(banded, 4).map((s) => s.id)).toEqual([3, 4]);
|
|
37
|
+
expect((0, workflowBands_1.isConcurrentBand)(banded, 3)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it("resolves a band of three from the middle", () => {
|
|
40
|
+
const three = [
|
|
41
|
+
stage(2, "Staff", 3, true),
|
|
42
|
+
stage(3, "Provider", 4, true),
|
|
43
|
+
stage(4, "Parent", 6),
|
|
44
|
+
stage(6, undefined),
|
|
45
|
+
];
|
|
46
|
+
expect((0, workflowBands_1.getConcurrentBand)(three, 3).map((s) => s.id)).toEqual([2, 3, 4]);
|
|
47
|
+
expect((0, workflowBands_1.getBandHead)(three, 4)?.id).toEqual(2);
|
|
48
|
+
});
|
|
49
|
+
it("does not pull in the stage after the band", () => {
|
|
50
|
+
expect((0, workflowBands_1.getConcurrentBand)(banded, 3).some((s) => s.id === 6)).toBe(false);
|
|
51
|
+
expect((0, workflowBands_1.getBandExitStageId)((0, workflowBands_1.getConcurrentBand)(banded, 3))).toEqual(6);
|
|
52
|
+
});
|
|
53
|
+
it("terminates on a cycle", () => {
|
|
54
|
+
// The builder stops a school authoring this, but a hand-edited workflow
|
|
55
|
+
// must not hang or repeat a stage. A cycle has no real head, so only the
|
|
56
|
+
// membership is meaningful here, not the order.
|
|
57
|
+
const looped = [
|
|
58
|
+
stage(3, "Provider", 4, true),
|
|
59
|
+
stage(4, "Parent", 3, true),
|
|
60
|
+
];
|
|
61
|
+
const ids = (0, workflowBands_1.getConcurrentBand)(looped, 3).map((s) => s.id);
|
|
62
|
+
expect(ids.slice().sort()).toEqual([3, 4]);
|
|
63
|
+
expect(new Set(ids).size).toEqual(ids.length);
|
|
64
|
+
});
|
|
65
|
+
it("terminates when a flagged stage points nowhere", () => {
|
|
66
|
+
const dangling = [stage(3, "Provider", undefined, true)];
|
|
67
|
+
expect((0, workflowBands_1.getConcurrentBand)(dangling, 3).map((s) => s.id)).toEqual([3]);
|
|
68
|
+
});
|
|
69
|
+
it("returns nothing for a stage that is not in the workflow", () => {
|
|
70
|
+
expect((0, workflowBands_1.getConcurrentBand)(banded, 99)).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
it("reports what the band is still waiting on", () => {
|
|
73
|
+
const band = (0, workflowBands_1.getConcurrentBand)(banded, 3);
|
|
74
|
+
expect((0, workflowBands_1.getBandOutstanding)(band, []).map((s) => s.id)).toEqual([3, 4]);
|
|
75
|
+
expect((0, workflowBands_1.getBandOutstandingUserTypes)(band, [])).toEqual(["Provider", "Parent"]);
|
|
76
|
+
// The employer has signed off, so only the parent is still owed.
|
|
77
|
+
expect((0, workflowBands_1.getBandOutstanding)(band, [3]).map((s) => s.id)).toEqual([4]);
|
|
78
|
+
expect((0, workflowBands_1.getBandOutstandingUserTypes)(band, [3])).toEqual(["Parent"]);
|
|
79
|
+
expect((0, workflowBands_1.getBandOutstanding)(band, [3, 4])).toEqual([]);
|
|
80
|
+
expect((0, workflowBands_1.getBandOutstandingUserTypes)(band, [3, 4])).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
it("does not repeat a user type that owns two stages in the band", () => {
|
|
83
|
+
const twoProviderStages = [
|
|
84
|
+
stage(3, "Provider", 4, true),
|
|
85
|
+
stage(4, "Provider", 5, true),
|
|
86
|
+
stage(5, "Parent", 6),
|
|
87
|
+
stage(6, undefined),
|
|
88
|
+
];
|
|
89
|
+
const band = (0, workflowBands_1.getConcurrentBand)(twoProviderStages, 3);
|
|
90
|
+
expect((0, workflowBands_1.getBandOutstandingUserTypes)(band, [])).toEqual(["Provider", "Parent"]);
|
|
91
|
+
});
|
|
92
|
+
it("names the band for staff", () => {
|
|
93
|
+
expect((0, workflowBands_1.getBandName)((0, workflowBands_1.getConcurrentBand)(banded, 3))).toEqual("Stage 3 & Stage 4");
|
|
94
|
+
expect((0, workflowBands_1.getBandName)((0, workflowBands_1.getConcurrentBand)(sequential, 3))).toEqual("Stage 3");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=workflowBands.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflowBands.test.js","sourceRoot":"","sources":["../src/workflowBands.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from "./tutorialTips";
|
|
|
31
31
|
export * from "./constants";
|
|
32
32
|
export * from "./dataViewerReload";
|
|
33
33
|
export * from "./hooks";
|
|
34
|
+
export * from "./workflowBands";
|
|
34
35
|
export * from "./util";
|
|
35
36
|
export * from "./images/GatsbyBenchmarks";
|
|
36
37
|
export * from "./databaseTypes";
|
package/src/typeDefinitions.ts
CHANGED
|
@@ -36,6 +36,10 @@ export type Products = "institutes"|"providers"|"students"|"admin";
|
|
|
36
36
|
export type StudentPlacementData = {
|
|
37
37
|
title: string,
|
|
38
38
|
reqUserType: "Students" | "Staff" | "Provider" | "Parent",
|
|
39
|
+
reqUserTypes?: ("Students" | "Staff" | "Provider" | "Parent")[],
|
|
40
|
+
/** Set while a completion is claiming the band's single advance, so two
|
|
41
|
+
* parties finishing at once cannot both move the placement on. */
|
|
42
|
+
advancingFrom?: number,
|
|
39
43
|
lastStageChange?: string // yyyy-mm-dd Set every time a stage changes!
|
|
40
44
|
uid: string,
|
|
41
45
|
startDate: string,
|
|
@@ -65,8 +69,6 @@ export type StudentPlacementData = {
|
|
|
65
69
|
locality: string,
|
|
66
70
|
postal_code: string,
|
|
67
71
|
remote?: boolean;
|
|
68
|
-
// The employer contact for this placement is the student's parent or
|
|
69
|
-
// guardian, so parent and employer sign-off are the same person.
|
|
70
72
|
parentIsEmployer?: boolean;
|
|
71
73
|
what3Words?: string,
|
|
72
74
|
country: string,
|
|
@@ -756,6 +758,7 @@ export type WorkflowStage = {
|
|
|
756
758
|
buttons?: {id: number, name: string|false, description?: string, required: boolean}[],
|
|
757
759
|
pos?: { x: number, y: number },
|
|
758
760
|
userType?: "Students" | "Staff" | "Provider" | "Parent",
|
|
761
|
+
concurrentWithNext?: boolean,
|
|
759
762
|
forms?: string[],
|
|
760
763
|
files?: string[],
|
|
761
764
|
deadline?: string,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {getBandExitStageId, getBandHead, getBandName, getBandOutstanding,
|
|
2
|
+
getBandOutstandingUserTypes, getConcurrentBand, isConcurrentBand} from "./workflowBands";
|
|
3
|
+
import {WorkflowStage} from "./typeDefinitions";
|
|
4
|
+
|
|
5
|
+
describe("Concurrent bands", () => {
|
|
6
|
+
// A typical work experience chain: staff check, then employer review, then
|
|
7
|
+
// parental consent, then the placement start stage.
|
|
8
|
+
const stage = (id: number, userType: WorkflowStage["userType"], next?: number, concurrentWithNext?: boolean): WorkflowStage => ({
|
|
9
|
+
id,
|
|
10
|
+
name: `Stage ${id}`,
|
|
11
|
+
userType,
|
|
12
|
+
concurrentWithNext,
|
|
13
|
+
buttons: next === undefined ? [] : [{id: next, name: "Continue", required: true}],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const sequential: WorkflowStage[] = [
|
|
17
|
+
stage(1, "Students", 2),
|
|
18
|
+
stage(2, "Staff", 3),
|
|
19
|
+
stage(3, "Provider", 4),
|
|
20
|
+
stage(4, "Parent", 6),
|
|
21
|
+
stage(6, undefined),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Same workflow, but employer review and parental consent run together.
|
|
25
|
+
const banded: WorkflowStage[] = [
|
|
26
|
+
stage(1, "Students", 2),
|
|
27
|
+
stage(2, "Staff", 3),
|
|
28
|
+
stage(3, "Provider", 4, true),
|
|
29
|
+
stage(4, "Parent", 6),
|
|
30
|
+
stage(6, undefined),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
it("returns just the stage when nothing is marked concurrent", () => {
|
|
34
|
+
expect(getConcurrentBand(sequential, 3).map((s) => s.id)).toEqual([3]);
|
|
35
|
+
expect(getConcurrentBand(sequential, 4).map((s) => s.id)).toEqual([4]);
|
|
36
|
+
expect(isConcurrentBand(sequential, 3)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("resolves the same band from any member", () => {
|
|
40
|
+
expect(getConcurrentBand(banded, 3).map((s) => s.id)).toEqual([3, 4]);
|
|
41
|
+
expect(getConcurrentBand(banded, 4).map((s) => s.id)).toEqual([3, 4]);
|
|
42
|
+
expect(isConcurrentBand(banded, 3)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("resolves a band of three from the middle", () => {
|
|
46
|
+
const three: WorkflowStage[] = [
|
|
47
|
+
stage(2, "Staff", 3, true),
|
|
48
|
+
stage(3, "Provider", 4, true),
|
|
49
|
+
stage(4, "Parent", 6),
|
|
50
|
+
stage(6, undefined),
|
|
51
|
+
];
|
|
52
|
+
expect(getConcurrentBand(three, 3).map((s) => s.id)).toEqual([2, 3, 4]);
|
|
53
|
+
expect(getBandHead(three, 4)?.id).toEqual(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("does not pull in the stage after the band", () => {
|
|
57
|
+
expect(getConcurrentBand(banded, 3).some((s) => s.id === 6)).toBe(false);
|
|
58
|
+
expect(getBandExitStageId(getConcurrentBand(banded, 3))).toEqual(6);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("terminates on a cycle", () => {
|
|
62
|
+
// The builder stops a school authoring this, but a hand-edited workflow
|
|
63
|
+
// must not hang or repeat a stage. A cycle has no real head, so only the
|
|
64
|
+
// membership is meaningful here, not the order.
|
|
65
|
+
const looped: WorkflowStage[] = [
|
|
66
|
+
stage(3, "Provider", 4, true),
|
|
67
|
+
stage(4, "Parent", 3, true),
|
|
68
|
+
];
|
|
69
|
+
const ids = getConcurrentBand(looped, 3).map((s) => s.id);
|
|
70
|
+
expect(ids.slice().sort()).toEqual([3, 4]);
|
|
71
|
+
expect(new Set(ids).size).toEqual(ids.length);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("terminates when a flagged stage points nowhere", () => {
|
|
75
|
+
const dangling: WorkflowStage[] = [stage(3, "Provider", undefined, true)];
|
|
76
|
+
expect(getConcurrentBand(dangling, 3).map((s) => s.id)).toEqual([3]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns nothing for a stage that is not in the workflow", () => {
|
|
80
|
+
expect(getConcurrentBand(banded, 99)).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("reports what the band is still waiting on", () => {
|
|
84
|
+
const band = getConcurrentBand(banded, 3);
|
|
85
|
+
expect(getBandOutstanding(band, []).map((s) => s.id)).toEqual([3, 4]);
|
|
86
|
+
expect(getBandOutstandingUserTypes(band, [])).toEqual(["Provider", "Parent"]);
|
|
87
|
+
|
|
88
|
+
// The employer has signed off, so only the parent is still owed.
|
|
89
|
+
expect(getBandOutstanding(band, [3]).map((s) => s.id)).toEqual([4]);
|
|
90
|
+
expect(getBandOutstandingUserTypes(band, [3])).toEqual(["Parent"]);
|
|
91
|
+
|
|
92
|
+
expect(getBandOutstanding(band, [3, 4])).toEqual([]);
|
|
93
|
+
expect(getBandOutstandingUserTypes(band, [3, 4])).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not repeat a user type that owns two stages in the band", () => {
|
|
97
|
+
const twoProviderStages: WorkflowStage[] = [
|
|
98
|
+
stage(3, "Provider", 4, true),
|
|
99
|
+
stage(4, "Provider", 5, true),
|
|
100
|
+
stage(5, "Parent", 6),
|
|
101
|
+
stage(6, undefined),
|
|
102
|
+
];
|
|
103
|
+
const band = getConcurrentBand(twoProviderStages, 3);
|
|
104
|
+
expect(getBandOutstandingUserTypes(band, [])).toEqual(["Provider", "Parent"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("names the band for staff", () => {
|
|
108
|
+
expect(getBandName(getConcurrentBand(banded, 3))).toEqual("Stage 3 & Stage 4");
|
|
109
|
+
expect(getBandName(getConcurrentBand(sequential, 3))).toEqual("Stage 3");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {WorkflowStage} from "./typeDefinitions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Concurrent sign-off bands.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately free of any firebase import so it can be unit tested and pulled
|
|
7
|
+
* into the backend, the external review payload and the staff UI alike.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The stage that starts the concurrent band `stageId` belongs to.
|
|
12
|
+
*
|
|
13
|
+
* A band is a run of stages chained by their first button where every stage but
|
|
14
|
+
* the last carries `concurrentWithNext`. The head is the stage a placement's
|
|
15
|
+
* `status` points at while the band is open, which is what lets `status` stay a
|
|
16
|
+
* single number: the band only changes who we ask, not where the placement is.
|
|
17
|
+
*/
|
|
18
|
+
export const getBandHead = (workflow: WorkflowStage[], stageId: number): WorkflowStage|undefined => {
|
|
19
|
+
let stage = workflow.find((s) => s.id === stageId);
|
|
20
|
+
if (!stage) return undefined;
|
|
21
|
+
|
|
22
|
+
const visited = new Set<number>([stage.id]);
|
|
23
|
+
|
|
24
|
+
for (;;) {
|
|
25
|
+
const previous = workflow.find((s) => s.concurrentWithNext && s.buttons?.[0]?.id === stage?.id);
|
|
26
|
+
// No flagged predecessor means we are already at the head.
|
|
27
|
+
if (!previous || visited.has(previous.id)) return stage;
|
|
28
|
+
visited.add(previous.id);
|
|
29
|
+
stage = previous;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every stage that runs at the same time as `stageId`, in workflow order.
|
|
35
|
+
*
|
|
36
|
+
* Returns just the stage itself when nothing is marked concurrent, so callers can
|
|
37
|
+
* use this unconditionally and existing workflows behave exactly as before.
|
|
38
|
+
*/
|
|
39
|
+
export const getConcurrentBand = (workflow: WorkflowStage[], stageId: number): WorkflowStage[] => {
|
|
40
|
+
const head = getBandHead(workflow, stageId);
|
|
41
|
+
if (!head) return [];
|
|
42
|
+
|
|
43
|
+
const band = [head];
|
|
44
|
+
const visited = new Set<number>([head.id]);
|
|
45
|
+
|
|
46
|
+
let stage: WorkflowStage|undefined = head;
|
|
47
|
+
while (stage?.concurrentWithNext) {
|
|
48
|
+
const nextId: number|undefined = stage.buttons?.[0]?.id;
|
|
49
|
+
// A flagged stage with nowhere to go, or a loop, ends the band rather
|
|
50
|
+
// than stranding the caller.
|
|
51
|
+
if (nextId === undefined || visited.has(nextId)) break;
|
|
52
|
+
stage = workflow.find((s) => s.id === nextId);
|
|
53
|
+
if (!stage) break;
|
|
54
|
+
visited.add(stage.id);
|
|
55
|
+
band.push(stage);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return band;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** True when the workflow asks more than one party at this point. */
|
|
62
|
+
export const isConcurrentBand = (workflow: WorkflowStage[], stageId: number) =>
|
|
63
|
+
getConcurrentBand(workflow, stageId).length > 1;
|
|
64
|
+
|
|
65
|
+
/** The band's stages that nobody has signed off yet. */
|
|
66
|
+
export const getBandOutstanding = (band: WorkflowStage[], completedStages: number[] = []) =>
|
|
67
|
+
band.filter((stage) => !completedStages.includes(stage.id));
|
|
68
|
+
|
|
69
|
+
/** Distinct user types the band is still waiting on, in workflow order. */
|
|
70
|
+
export const getBandOutstandingUserTypes = (band: WorkflowStage[], completedStages: number[] = []) =>
|
|
71
|
+
getBandOutstanding(band, completedStages)
|
|
72
|
+
.map((stage) => stage.userType)
|
|
73
|
+
.filter((userType, i, all): userType is NonNullable<WorkflowStage["userType"]> =>
|
|
74
|
+
Boolean(userType) && all.indexOf(userType) === i);
|
|
75
|
+
|
|
76
|
+
/** Where the placement goes once every stage in the band is signed off. */
|
|
77
|
+
export const getBandExitStageId = (band: WorkflowStage[]) =>
|
|
78
|
+
band[band.length - 1]?.buttons?.[0]?.id;
|
|
79
|
+
|
|
80
|
+
/** The label to show staff for a band, e.g. "Employer review & parental consent". */
|
|
81
|
+
export const getBandName = (band: WorkflowStage[]) =>
|
|
82
|
+
band.map((stage) => stage.name).join(" & ");
|