@opencxh/domain 1.232.0 → 1.233.1
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/dist/entities/analytics/index.d.ts +1 -0
- package/dist/entities/analytics/picker.d.ts +49 -0
- package/dist/entities/analytics/picker.test.d.ts +1 -0
- package/dist/entities/planning/capacity.d.ts +69 -0
- package/dist/entities/planning/capacity.test.d.ts +1 -0
- package/dist/entities/planning/contracts.d.ts +118 -0
- package/dist/entities/planning/coverage.d.ts +73 -0
- package/dist/entities/planning/coverage.test.d.ts +1 -0
- package/dist/entities/planning/erlang.d.ts +37 -0
- package/dist/entities/planning/erlang.test.d.ts +1 -0
- package/dist/entities/planning/forecast.d.ts +34 -0
- package/dist/entities/planning/forecast.test.d.ts +1 -0
- package/dist/entities/planning/index.d.ts +11 -0
- package/dist/entities/planning/keys.d.ts +12 -0
- package/dist/entities/planning/kinds.d.ts +56 -0
- package/dist/entities/planning/pattern.d.ts +63 -0
- package/dist/entities/planning/spread.d.ts +61 -0
- package/dist/entities/planning/spread.test.d.ts +1 -0
- package/dist/entities/planning/terms.d.ts +42 -0
- package/dist/entities/planning/types.d.ts +188 -0
- package/dist/entities/strategy/levels.d.ts +12 -0
- package/dist/entities/strategy/progress.d.ts +12 -4
- package/dist/index.cjs +16 -16
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1979 -1495
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many hours somebody is available in a week.
|
|
3
|
+
*
|
|
4
|
+
* An employment fact, not a planning one — it changes because a contract changes, and it has
|
|
5
|
+
* readers outside planning waiting: overtime is booked time above these hours, utilisation is
|
|
6
|
+
* billable divided by them, and leave accrual is a function of them.
|
|
7
|
+
*
|
|
8
|
+
* **It lives in `apps/planning` on purpose, and temporarily.** Three things keep the eventual move
|
|
9
|
+
* to an HR app a hostname rather than a project: this type is in `domain` and not in the app; the
|
|
10
|
+
* app keeps it in one resource folder with no terms logic scattered elsewhere; and every reader
|
|
11
|
+
* reads it over HTTP, which is the only option anyway since apps share no store.
|
|
12
|
+
*/
|
|
13
|
+
export interface EmploymentTerms {
|
|
14
|
+
id: string;
|
|
15
|
+
organizationId: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
/**
|
|
18
|
+
* Contract hours, as minutes per week.
|
|
19
|
+
*
|
|
20
|
+
* **Known limit:** a Dutch contract is often "36 hours averaged over 13 weeks". This field
|
|
21
|
+
* cannot say that. An averaging period is one field more the day somebody needs it; it is
|
|
22
|
+
* written down here so it is not a surprise.
|
|
23
|
+
*/
|
|
24
|
+
weeklyMinutes: number;
|
|
25
|
+
/** From when this holds. A changed contract is a new row, never a patch on the old one. */
|
|
26
|
+
effectiveFrom?: number;
|
|
27
|
+
note?: string;
|
|
28
|
+
createdBy: string;
|
|
29
|
+
createdAt?: number;
|
|
30
|
+
updatedAt?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The terms that applied to somebody on a given day.
|
|
34
|
+
*
|
|
35
|
+
* The most recent `effectiveFrom` that had already started wins, so the row that governed last
|
|
36
|
+
* quarter stays readable next to the weeks it governed.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately no `fte`: that is `weeklyMinutes` divided by a full-time norm which differs per
|
|
39
|
+
* organisation — forty hours here, thirty-six elsewhere. Storing it would be a second truth;
|
|
40
|
+
* showing it can wait until there is a norm to divide by.
|
|
41
|
+
*/
|
|
42
|
+
export declare function termsFor(terms: readonly EmploymentTerms[], userId: string, day: number): EmploymentTerms | undefined;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { FilterClause } from '../analytics/report';
|
|
2
|
+
import { OwnerScope } from '../scope/types';
|
|
3
|
+
/**
|
|
4
|
+
* Where a planned row stands.
|
|
5
|
+
*
|
|
6
|
+
* `draft` is the whole reason this field exists: a planner builds next week over an afternoon,
|
|
7
|
+
* and half a roster must not read as coverage while they do. `requested`/`declined` only ever
|
|
8
|
+
* occur on an approvable kind (leave) — see {@link ScheduleKind.approvable}.
|
|
9
|
+
*/
|
|
10
|
+
export type ScheduleStatus = "draft" | "published" | "requested" | "approved" | "declined" | "cancelled";
|
|
11
|
+
/** Who asked for this row, and why when a system did. */
|
|
12
|
+
export interface ScheduleEntrySource {
|
|
13
|
+
initiator: "user" | "system";
|
|
14
|
+
/** Free reason on `system` ("copy_week", "playbook"). Same shape as WorkItemSource. */
|
|
15
|
+
reason?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One person, one time span, one kind of time.
|
|
19
|
+
*
|
|
20
|
+
* Shift, break, training, leave request, project allocation and customer appointment are all
|
|
21
|
+
* this row with a different `kindKey`. They share the span, the person, the overlap rules, the
|
|
22
|
+
* grid, publication and the capacity arithmetic; what differs is whether the kind *makes*
|
|
23
|
+
* capacity, *spends* it or *takes it away*, and that is two flags on the kind — not three tables.
|
|
24
|
+
*/
|
|
25
|
+
export interface ScheduleEntry {
|
|
26
|
+
id: string;
|
|
27
|
+
organizationId: string;
|
|
28
|
+
/** Who. **Absent = an open shift** — planned work with no name on it, visible as a gap. */
|
|
29
|
+
userId?: string;
|
|
30
|
+
/** Only on an open shift: the role being looked for. A free string until skills exist. */
|
|
31
|
+
placeholderRole?: string;
|
|
32
|
+
/** Which kind of time. See `FIXED_KINDS`; an unknown key is treated as unproductive. */
|
|
33
|
+
kindKey: string;
|
|
34
|
+
/** Epoch ms, half-open `[start, end)` — the same convention as analytics' DateRange. */
|
|
35
|
+
start: number;
|
|
36
|
+
end: number;
|
|
37
|
+
status: ScheduleStatus;
|
|
38
|
+
/** Where this time goes on the demand side. Absent on a shift = unallocated capacity. */
|
|
39
|
+
workstreamId?: string;
|
|
40
|
+
/** Where it goes on the work side: `work_project:7`, `work_item:42`, `company:9`. */
|
|
41
|
+
scopeKey?: string;
|
|
42
|
+
/** Which team this time is worked for. Feeds `WorkMode.source = "roster"` later. */
|
|
43
|
+
teamId?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Which `WorkType` the hours on this block will be booked under.
|
|
46
|
+
*
|
|
47
|
+
* The join with `apps/time`: planned and booked time land on the same key, so planned-versus-
|
|
48
|
+
* actual is one grouping instead of a mapping table. Absent = the workstream's default.
|
|
49
|
+
*/
|
|
50
|
+
workTypeKey?: string;
|
|
51
|
+
note?: string;
|
|
52
|
+
/** Set once a published row reached the person's calendar. Absent = never pushed. */
|
|
53
|
+
calendarEventId?: string;
|
|
54
|
+
externalIds?: string[];
|
|
55
|
+
source: ScheduleEntrySource;
|
|
56
|
+
createdBy: string;
|
|
57
|
+
createdAt?: number;
|
|
58
|
+
updatedAt?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* How a workstream's demand is derived.
|
|
62
|
+
*
|
|
63
|
+
* Three shapes, one output: required person-seconds per interval. `metric` is the support case
|
|
64
|
+
* (history → forecast), `source` the delivery case (an app that knows what work is coming),
|
|
65
|
+
* `fixed` the back-office case (always two people on it).
|
|
66
|
+
*
|
|
67
|
+
* `source` is a provider role and not a list of app names on purpose: planning must not grow a
|
|
68
|
+
* branch per app that can generate work. `metric` needs no provider because analytics already
|
|
69
|
+
* federates every app's volume — a source only declares itself when its demand is not a
|
|
70
|
+
* countable stream with a handle time.
|
|
71
|
+
*/
|
|
72
|
+
export type WorkstreamDemand = {
|
|
73
|
+
kind: "metric";
|
|
74
|
+
/** An analytics metric id, e.g. "comms.conversations.created". */
|
|
75
|
+
metricId: string;
|
|
76
|
+
/** The mapping rule. There is no second rule engine — this *is* it. */
|
|
77
|
+
filters?: FilterClause[];
|
|
78
|
+
/** Average handle time per unit, in seconds. */
|
|
79
|
+
handleSeconds: number;
|
|
80
|
+
/** A duration metric to read AHT from instead of the fixed number. Not in v1. */
|
|
81
|
+
handleSecondsMetricId?: string;
|
|
82
|
+
} | {
|
|
83
|
+
kind: "source";
|
|
84
|
+
/** The app answering `POST /provider/demand/query`. See {@link DemandSourceDescribe}. */
|
|
85
|
+
sourceApp: string;
|
|
86
|
+
/** Opaque to planning, validated by the source: `{ projectIds, cycleId }` for apps/work. */
|
|
87
|
+
params?: Record<string, unknown>;
|
|
88
|
+
} | {
|
|
89
|
+
kind: "fixed";
|
|
90
|
+
headcount: number;
|
|
91
|
+
/** Local hours the fixed staffing applies to, e.g. `[8, 18]`. Absent = around the clock. */
|
|
92
|
+
weekdayHours?: [number, number];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* How required work turns into required people.
|
|
96
|
+
*
|
|
97
|
+
* `queue` is for work that arrives and waits — you steer on how fast it gets picked up. `hours`
|
|
98
|
+
* is for work that is already known — you divide it by the clock. Named after the question a
|
|
99
|
+
* planner answers, not after the maths: `queue` runs Erlang C underneath (see `erlang.ts`), and
|
|
100
|
+
* a field called `erlang_c` asks every reader to know a 1917 formula before they can pick one.
|
|
101
|
+
*
|
|
102
|
+
* `unplannedLossPercent` — shrinkage, in the trade — defaults to 0 on purpose: most WFM tools
|
|
103
|
+
* assume 25-35% because breaks, training and meetings are not in their roster. Here they are, as
|
|
104
|
+
* rows that give no coverage. It is only for what you *cannot* roster (sick leave, ad hoc), and
|
|
105
|
+
* counting it twice is the classic error.
|
|
106
|
+
*/
|
|
107
|
+
export type StaffingPolicy = {
|
|
108
|
+
model: "hours";
|
|
109
|
+
unplannedLossPercent?: number;
|
|
110
|
+
} | {
|
|
111
|
+
model: "queue";
|
|
112
|
+
/** e.g. 80 for "80% of contacts answered within `targetSeconds`". */
|
|
113
|
+
serviceLevelPercent: number;
|
|
114
|
+
targetSeconds: number;
|
|
115
|
+
/** Cap on how busy an agent may be, e.g. 85. Absent = no cap. */
|
|
116
|
+
maxOccupancyPercent?: number;
|
|
117
|
+
unplannedLossPercent?: number;
|
|
118
|
+
};
|
|
119
|
+
/** A bucket demand comes from: an inbox, a project, a process, a pipeline. */
|
|
120
|
+
export interface Workstream {
|
|
121
|
+
id: string;
|
|
122
|
+
organizationId: string;
|
|
123
|
+
/** Org-unique slug — the same shape as `WorkType.key`. */
|
|
124
|
+
key: string;
|
|
125
|
+
name: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
color?: string;
|
|
128
|
+
icon?: string;
|
|
129
|
+
/** Team or org, never personal: a workstream nobody but you can see plans nothing. */
|
|
130
|
+
ownerScope: OwnerScope;
|
|
131
|
+
demand: WorkstreamDemand;
|
|
132
|
+
staffing: StaffingPolicy;
|
|
133
|
+
/**
|
|
134
|
+
* The `WorkType` blocks on this workstream get when nobody picks one.
|
|
135
|
+
*
|
|
136
|
+
* Deliberately not a `billable` flag of its own. Billability already has a home —
|
|
137
|
+
* `WorkType.defaultBillable`, which every booked hour resolves through — and a second flag
|
|
138
|
+
* here would drift from it the first time somebody changes one of the two.
|
|
139
|
+
*/
|
|
140
|
+
defaultWorkTypeKey?: string;
|
|
141
|
+
archived?: boolean;
|
|
142
|
+
order?: number;
|
|
143
|
+
createdBy: string;
|
|
144
|
+
createdAt?: number;
|
|
145
|
+
updatedAt?: number;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A human override on a forecast, and the only forecast row that is stored.
|
|
149
|
+
*
|
|
150
|
+
* The forecast itself is a pure function of history + these; recomputing it on read costs less
|
|
151
|
+
* than a job plus the "why is this number stale" question that a stored forecast creates.
|
|
152
|
+
*/
|
|
153
|
+
export interface ForecastAdjustment {
|
|
154
|
+
id: string;
|
|
155
|
+
organizationId: string;
|
|
156
|
+
workstreamId: string;
|
|
157
|
+
/** Epoch ms, half-open `[from, to)`. */
|
|
158
|
+
from: number;
|
|
159
|
+
to: number;
|
|
160
|
+
/** A multiplier (1.3 = campaign week). Ignored when `absoluteVolume` is set. */
|
|
161
|
+
factor?: number;
|
|
162
|
+
/** An absolute volume per interval. Wins over `factor`. */
|
|
163
|
+
absoluteVolume?: number;
|
|
164
|
+
note?: string;
|
|
165
|
+
createdBy: string;
|
|
166
|
+
createdAt?: number;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* One interval of the coverage answer. Derived on read, never stored.
|
|
170
|
+
*
|
|
171
|
+
* Headcounts are FTE-equivalents (`seconds / intervalSeconds`) and may be fractional: somebody
|
|
172
|
+
* who works half the hour covers half of it. Rounding to whole people here would make a
|
|
173
|
+
* half-staffed interval look either fully covered or empty.
|
|
174
|
+
*/
|
|
175
|
+
export interface CoverageInterval {
|
|
176
|
+
/** "2026-09-17T09" — the same period key `MetricFact` uses. */
|
|
177
|
+
period: string;
|
|
178
|
+
forecastVolume?: number;
|
|
179
|
+
requiredSeconds: number;
|
|
180
|
+
requiredHeadcount: number;
|
|
181
|
+
scheduledSeconds: number;
|
|
182
|
+
scheduledHeadcount: number;
|
|
183
|
+
/** `scheduled - required`. Negative = short. */
|
|
184
|
+
netHeadcount: number;
|
|
185
|
+
/** All shift time, whether or not it is allocated to this workstream. */
|
|
186
|
+
capacitySeconds: number;
|
|
187
|
+
absentSeconds: number;
|
|
188
|
+
}
|
|
@@ -60,6 +60,18 @@ export declare function levelOrder(levelKey: string, levels?: readonly StrategyL
|
|
|
60
60
|
* key this platform does not recognise.
|
|
61
61
|
*/
|
|
62
62
|
export declare function canParent(childLevelKey: string, parentLevelKey: string, levels?: readonly StrategyLevel[]): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The rung a child of this row gets by default.
|
|
65
|
+
*
|
|
66
|
+
* What the inline "+" on a row creates: one step down the ladder, so a focus area yields a goal
|
|
67
|
+
* and a goal yields a key result. **The deepest rung yields itself** rather than nothing — a key
|
|
68
|
+
* result under a key result is unusual but legal (`canParent` allows equal rungs), and returning
|
|
69
|
+
* `undefined` would mean the affordance disappears exactly where a team is working deepest.
|
|
70
|
+
*
|
|
71
|
+
* An unknown level ranks last, so it also yields itself. That is the safe direction: a level a
|
|
72
|
+
* source invented gets a sibling, not a guess at what sits under it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function nextLevelBelow(levelKey: string, levels?: readonly StrategyLevel[]): string;
|
|
63
75
|
/**
|
|
64
76
|
* Which of our own rungs a source's name means, or `null`.
|
|
65
77
|
*
|
|
@@ -4,11 +4,19 @@ import { StrategyHealth, StrategyItem, StrategyMeasure, StrategyProgress } from
|
|
|
4
4
|
*
|
|
5
5
|
* Measured from `startValue`, not from zero: "from 5h30m down to 2h" is 0% on day one, while a
|
|
6
6
|
* zero baseline would report that same goal as already two-thirds done before anyone did
|
|
7
|
-
* anything.
|
|
7
|
+
* anything.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* **`direction` and the bounds have to agree, and this is where they are checked.** The maths
|
|
10
|
+
* needs no `direction` at all — a falling goal is one whose target lies below its start, and the
|
|
11
|
+
* division handles that by itself. The field exists for the one case the bounds cannot express
|
|
12
|
+
* (holding a number steady, where start equals target) and for the label on the form. So when the
|
|
13
|
+
* two disagree — "lower is better" with a target *above* the start, which a form will happily
|
|
14
|
+
* accept — the bounds are not a scale at all, and this falls back to the only honest answer:
|
|
15
|
+
* "are we there, yes or no". Dividing anyway is what reported a goal as 100% done the moment its
|
|
16
|
+
* value went the wrong way.
|
|
17
|
+
*
|
|
18
|
+
* A value that cannot be a number is 0, never `NaN`: a `NaN` ratio renders as an empty bar with
|
|
19
|
+
* no width and reads as "not started".
|
|
12
20
|
*/
|
|
13
21
|
export declare function measureRatio(measure: Pick<StrategyMeasure, "direction" | "startValue" | "targetValue">, currentValue: number): number;
|
|
14
22
|
/**
|