@critical-path/core 0.16.0 → 0.18.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +50 -1
  3. package/dist/domain/entities.d.ts.map +1 -1
  4. package/dist/domain/entities.js +5 -0
  5. package/dist/domain/entities.js.map +1 -1
  6. package/dist/domain/index.d.ts +2 -0
  7. package/dist/domain/index.d.ts.map +1 -1
  8. package/dist/domain/index.js +2 -0
  9. package/dist/domain/index.js.map +1 -1
  10. package/dist/domain/ladder.d.ts.map +1 -1
  11. package/dist/domain/ladder.js +14 -0
  12. package/dist/domain/ladder.js.map +1 -1
  13. package/dist/domain/ladder.test.js +3 -3
  14. package/dist/domain/ladder.test.js.map +1 -1
  15. package/dist/domain/metrics.d.ts +29 -0
  16. package/dist/domain/metrics.d.ts.map +1 -0
  17. package/dist/domain/metrics.js +403 -0
  18. package/dist/domain/metrics.js.map +1 -0
  19. package/dist/domain/metrics.test.d.ts +2 -0
  20. package/dist/domain/metrics.test.d.ts.map +1 -0
  21. package/dist/domain/metrics.test.js +251 -0
  22. package/dist/domain/metrics.test.js.map +1 -0
  23. package/dist/domain/workload.d.ts +10 -0
  24. package/dist/domain/workload.d.ts.map +1 -0
  25. package/dist/domain/workload.js +349 -0
  26. package/dist/domain/workload.js.map +1 -0
  27. package/dist/domain/workload.test.d.ts +2 -0
  28. package/dist/domain/workload.test.d.ts.map +1 -0
  29. package/dist/domain/workload.test.js +201 -0
  30. package/dist/domain/workload.test.js.map +1 -0
  31. package/dist/engine/index.d.ts +5 -1
  32. package/dist/engine/index.d.ts.map +1 -1
  33. package/dist/engine/index.js +55 -2
  34. package/dist/engine/index.js.map +1 -1
  35. package/dist/types/index.d.ts +91 -0
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/domain/entities.ts +6 -0
  39. package/src/domain/index.ts +2 -0
  40. package/src/domain/ladder.test.ts +3 -3
  41. package/src/domain/ladder.ts +16 -0
  42. package/src/domain/metrics.test.ts +286 -0
  43. package/src/domain/metrics.ts +484 -0
  44. package/src/domain/workload.test.ts +238 -0
  45. package/src/domain/workload.ts +403 -0
  46. package/src/engine/index.ts +86 -3
  47. package/src/types/index.ts +110 -1
  48. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { inferTaskProgress, calculateTaskEVM, reconstructTaskProgressHistory, calculateTaskMetrics } from './metrics.js';
3
+ import { TaskEntity } from './entities.js';
4
+ import { CriticalPathEngine } from '../engine/index.js';
5
+ import { InMemoryStore } from '../store/index.js';
6
+ describe('Task Metrics Domain', () => {
7
+ const baseTask = {
8
+ id: 'task-1',
9
+ projectId: 'proj-1',
10
+ title: 'Design API Architecture',
11
+ status: 'in_progress',
12
+ semanticStatus: 'in_progress',
13
+ priority: 'high',
14
+ estimatedHours: 20,
15
+ loggedHours: 10,
16
+ actualStartDate: '2026-09-01T09:00:00Z',
17
+ dueDate: '2026-09-10T17:00:00Z',
18
+ createdAt: '2026-09-01T08:00:00Z',
19
+ updatedAt: '2026-09-05T12:00:00Z'
20
+ };
21
+ describe('inferTaskProgress', () => {
22
+ it('returns 100% progress for completed tasks', () => {
23
+ const completedTask = { ...baseTask, status: 'done', semanticStatus: 'completed' };
24
+ const res = inferTaskProgress(completedTask);
25
+ expect(res.progressPercentage).toBe(100);
26
+ expect(res.breakdown.effortProgress).toBe(100);
27
+ expect(res.breakdown.scheduleProgress).toBe(100);
28
+ });
29
+ it('uses explicit progress when provided', () => {
30
+ const taskWithExplicit = {
31
+ ...baseTask,
32
+ progress: 45,
33
+ todos: [
34
+ { id: 't1', title: 'Step 1', completed: true },
35
+ { id: 't2', title: 'Step 2', completed: false }
36
+ ]
37
+ };
38
+ const res = inferTaskProgress(taskWithExplicit);
39
+ expect(res.isExplicit).toBe(true);
40
+ expect(res.source).toBe('explicit');
41
+ expect(res.progressPercentage).toBe(45);
42
+ expect(res.breakdown.explicitProgress).toBe(45);
43
+ expect(res.breakdown.todoProgress).toBe(50);
44
+ });
45
+ it('infers progress from todos when explicit progress is absent', () => {
46
+ const taskWithTodos = {
47
+ ...baseTask,
48
+ progress: undefined,
49
+ todos: [
50
+ { id: 't1', title: 'Step 1', completed: true },
51
+ { id: 't2', title: 'Step 2', completed: true },
52
+ { id: 't3', title: 'Step 3', completed: false },
53
+ { id: 't4', title: 'Step 4', completed: false }
54
+ ]
55
+ };
56
+ const res = inferTaskProgress(taskWithTodos);
57
+ expect(res.isExplicit).toBe(false);
58
+ expect(res.source).toBe('todos');
59
+ expect(res.progressPercentage).toBe(50);
60
+ expect(res.breakdown.todoProgress).toBe(50);
61
+ });
62
+ it('infers progress from effort (logged vs estimated) when todos and explicit are absent', () => {
63
+ const taskWithEffort = {
64
+ ...baseTask,
65
+ progress: undefined,
66
+ todos: undefined,
67
+ estimatedHours: 40,
68
+ loggedHours: 10
69
+ };
70
+ const res = inferTaskProgress(taskWithEffort);
71
+ expect(res.isExplicit).toBe(false);
72
+ expect(res.source).toBe('time_effort');
73
+ expect(res.progressPercentage).toBe(25);
74
+ expect(res.breakdown.effortProgress).toBe(25);
75
+ });
76
+ it('infers progress from schedule elapsed time when no other metrics are present', () => {
77
+ const taskWithSchedule = {
78
+ ...baseTask,
79
+ progress: undefined,
80
+ todos: undefined,
81
+ estimatedHours: 0,
82
+ loggedHours: 0,
83
+ actualStartDate: '2026-09-01T00:00:00Z',
84
+ dueDate: '2026-09-11T00:00:00Z'
85
+ };
86
+ const midDate = new Date('2026-09-06T00:00:00Z'); // 50% elapsed
87
+ const res = inferTaskProgress(taskWithSchedule, { referenceDate: midDate });
88
+ expect(res.source).toBe('schedule_elapsed');
89
+ expect(res.progressPercentage).toBe(50);
90
+ expect(res.breakdown.scheduleProgress).toBe(50);
91
+ });
92
+ });
93
+ describe('calculateTaskEVM', () => {
94
+ it('computes standard EVM values for on-track tasks', () => {
95
+ const task = {
96
+ ...baseTask,
97
+ estimatedHours: 20,
98
+ loggedHours: 10,
99
+ actualStartDate: '2026-09-01T00:00:00Z',
100
+ dueDate: '2026-09-11T00:00:00Z',
101
+ progress: 50
102
+ };
103
+ const midDate = new Date('2026-09-06T00:00:00Z'); // 50% expected
104
+ const evm = calculateTaskEVM(task, { referenceDate: midDate });
105
+ expect(evm.plannedValue).toBe(10); // 50% of 20h
106
+ expect(evm.earnedValue).toBe(10); // 50% of 20h
107
+ expect(evm.actualCost).toBe(10);
108
+ expect(evm.costVariance).toBe(0);
109
+ expect(evm.scheduleVariance).toBe(0);
110
+ expect(evm.costPerformanceIndex).toBe(1.0);
111
+ expect(evm.schedulePerformanceIndex).toBe(1.0);
112
+ });
113
+ it('detects cost overrun and schedule lag (CPI < 1, SPI < 1)', () => {
114
+ const task = {
115
+ ...baseTask,
116
+ estimatedHours: 20,
117
+ loggedHours: 15, // Spent 15h
118
+ progress: 25, // Only 25% complete (5h EV)
119
+ actualStartDate: '2026-09-01T00:00:00Z',
120
+ dueDate: '2026-09-11T00:00:00Z'
121
+ };
122
+ const midDate = new Date('2026-09-06T00:00:00Z'); // 50% expected (10h PV)
123
+ const evm = calculateTaskEVM(task, { referenceDate: midDate });
124
+ expect(evm.plannedValue).toBe(10);
125
+ expect(evm.earnedValue).toBe(5);
126
+ expect(evm.actualCost).toBe(15);
127
+ expect(evm.costVariance).toBe(-10); // 5 - 15 = -10
128
+ expect(evm.scheduleVariance).toBe(-5); // 5 - 10 = -5
129
+ expect(evm.costPerformanceIndex).toBe(0.33); // 5 / 15
130
+ expect(evm.schedulePerformanceIndex).toBe(0.5); // 5 / 10
131
+ });
132
+ });
133
+ describe('reconstructTaskProgressHistory & Curve Profile', () => {
134
+ it('detects linear progress', () => {
135
+ const activities = [
136
+ { id: 'a1', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 25 }, createdAt: '2026-09-02T10:00:00Z' },
137
+ { id: 'a2', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 50 }, createdAt: '2026-09-04T10:00:00Z' },
138
+ { id: 'a3', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 75 }, createdAt: '2026-09-06T10:00:00Z' },
139
+ { id: 'a4', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 100 }, createdAt: '2026-09-08T10:00:00Z' }
140
+ ];
141
+ const task = { ...baseTask, progress: 100, updatedAt: '2026-09-08T10:00:00Z' };
142
+ const history = reconstructTaskProgressHistory(task, activities);
143
+ expect(history.points.length).toBeGreaterThanOrEqual(4);
144
+ expect(history.curveProfile).toBe('linear');
145
+ });
146
+ it('detects S-Curve progress', () => {
147
+ const activities = [
148
+ { id: 'a1', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 15 }, createdAt: '2026-09-02T10:00:00Z' },
149
+ { id: 'a2', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 45 }, createdAt: '2026-09-05T10:00:00Z' },
150
+ { id: 'a3', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 85 }, createdAt: '2026-09-07T10:00:00Z' },
151
+ { id: 'a4', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 95 }, createdAt: '2026-09-09T10:00:00Z' }
152
+ ];
153
+ const task = { ...baseTask, progress: 95, updatedAt: '2026-09-09T10:00:00Z' };
154
+ const history = reconstructTaskProgressHistory(task, activities);
155
+ expect(history.curveProfile).toBe('s_curve');
156
+ });
157
+ it('detects early surge progress', () => {
158
+ const activities = [
159
+ { id: 'a1', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 75 }, createdAt: '2026-09-02T10:00:00Z' },
160
+ { id: 'a2', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 75 }, createdAt: '2026-09-05T10:00:00Z' },
161
+ { id: 'a3', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 78 }, createdAt: '2026-09-08T10:00:00Z' }
162
+ ];
163
+ const task = { ...baseTask, progress: 78, updatedAt: '2026-09-08T10:00:00Z' };
164
+ const history = reconstructTaskProgressHistory(task, activities);
165
+ expect(history.curveProfile).toBe('early_surge');
166
+ });
167
+ it('detects late rush progress', () => {
168
+ const activities = [
169
+ { id: 'a1', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 10 }, createdAt: '2026-09-03T10:00:00Z' },
170
+ { id: 'a2', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 15 }, createdAt: '2026-09-06T10:00:00Z' },
171
+ { id: 'a3', taskId: 'task-1', actorId: 'u1', action: 'task.updated', details: { progress: 90 }, createdAt: '2026-09-08T10:00:00Z' }
172
+ ];
173
+ const task = { ...baseTask, progress: 90, updatedAt: '2026-09-08T10:00:00Z' };
174
+ const history = reconstructTaskProgressHistory(task, activities);
175
+ expect(history.curveProfile).toBe('late_rush');
176
+ });
177
+ });
178
+ describe('calculateTaskMetrics & Multi-dimensional Variance', () => {
179
+ it('computes complete metrics with effort and schedule variances', () => {
180
+ const task = {
181
+ ...baseTask,
182
+ plannedStartDate: '2026-09-01T09:00:00Z',
183
+ actualStartDate: '2026-09-01T09:00:00Z',
184
+ dueDate: '2026-09-05T17:00:00Z',
185
+ actualEndDate: '2026-09-07T17:00:00Z',
186
+ status: 'done',
187
+ semanticStatus: 'completed',
188
+ estimatedHours: 20,
189
+ loggedHours: 25
190
+ };
191
+ const metrics = calculateTaskMetrics(task);
192
+ expect(metrics.taskId).toBe('task-1');
193
+ expect(metrics.realityDelta.varianceHours).toBe(5);
194
+ expect(metrics.realityDelta.effortVarianceHours).toBe(5);
195
+ expect(metrics.realityDelta.accuracyRatio).toBe(1.25); // 25 / 20
196
+ expect(metrics.realityDelta.scheduleVarianceDays).toBe(2); // Completed 2 days late
197
+ expect(metrics.realityDelta.durationVarianceHours).toBe(48); // 2 days extra duration
198
+ expect(metrics.inferredActuals.activeWorkingHours).toBeGreaterThan(0);
199
+ expect(metrics.inferredActuals.isEndDateInferred).toBe(true);
200
+ });
201
+ });
202
+ describe('Task Re-opening Timestamp Behavior', () => {
203
+ it('resets actualEndDate when TaskEntity transitions from completed back to in_progress', () => {
204
+ const entity = TaskEntity.create({
205
+ projectId: 'proj-1',
206
+ title: 'Bugfix',
207
+ status: 'todo'
208
+ });
209
+ // Move to in_progress -> stamps actualStartDate
210
+ entity.transitionTo('in_progress');
211
+ expect(entity.actualStartDate).toBeDefined();
212
+ expect(entity.actualEndDate).toBeUndefined();
213
+ // Move to done -> stamps actualEndDate
214
+ entity.transitionTo('done');
215
+ expect(entity.actualEndDate).toBeDefined();
216
+ // Re-open back to in_progress -> actualEndDate must be cleared
217
+ entity.transitionTo('in_progress');
218
+ expect(entity.actualEndDate).toBeUndefined();
219
+ expect(entity.actualStartDate).toBeDefined();
220
+ });
221
+ it('integrates with CriticalPathEngine getTaskMetrics and getTaskProgressHistory', async () => {
222
+ const store = new InMemoryStore();
223
+ const engine = new CriticalPathEngine({ store });
224
+ const project = await engine.createProject({ name: 'Engine Test' });
225
+ const task = await engine.createTask({
226
+ projectId: project.id,
227
+ title: 'Implement Auth',
228
+ estimatedHours: 16
229
+ });
230
+ // Move to in_progress
231
+ await engine.updateTask(task.id, { status: 'in_progress' });
232
+ await engine.logTime({ taskId: task.id, hours: 8, isBillable: true });
233
+ const metrics = await engine.getTaskMetrics(task.id);
234
+ expect(metrics).not.toBeNull();
235
+ expect(metrics?.taskId).toBe(task.id);
236
+ expect(metrics?.realityDelta.loggedHours).toBe(8);
237
+ expect(metrics?.realityDelta.estimatedHours).toBe(16);
238
+ expect(metrics?.progress.source).toBe('time_effort');
239
+ expect(metrics?.progress.progressPercentage).toBe(50);
240
+ expect(metrics?.evm.earnedValue).toBe(8);
241
+ const history = await engine.getTaskProgressHistory(task.id);
242
+ expect(history).not.toBeNull();
243
+ expect(history?.points.length).toBeGreaterThanOrEqual(1);
244
+ // Verify ladder view includes metrics
245
+ const ladder = await engine.getTaskLadder(task.id);
246
+ expect(ladder?.metrics).toBeDefined();
247
+ expect(ladder?.metrics?.taskId).toBe(task.id);
248
+ });
249
+ });
250
+ });
251
+ //# sourceMappingURL=metrics.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.test.js","sourceRoot":"","sources":["../../src/domain/metrics.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,8BAA8B,EAC9B,oBAAoB,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,MAAM,QAAQ,GAAS;QACrB,EAAE,EAAE,QAAQ;QACZ,SAAS,EAAE,QAAQ;QACnB,KAAK,EAAE,yBAAyB;QAChC,MAAM,EAAE,aAAa;QACrB,cAAc,EAAE,aAAa;QAC7B,QAAQ,EAAE,MAAM;QAChB,cAAc,EAAE,EAAE;QAClB,WAAW,EAAE,EAAE;QACf,eAAe,EAAE,sBAAsB;QACvC,OAAO,EAAE,sBAAsB;QAC/B,SAAS,EAAE,sBAAsB;QACjC,SAAS,EAAE,sBAAsB;KAClC,CAAC;IAEF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;YACnD,MAAM,aAAa,GAAS,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC;YACzF,MAAM,GAAG,GAAG,iBAAiB,CAAC,aAAa,CAAC,CAAC;YAC7C,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACzC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC/C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;YAC9C,MAAM,gBAAgB,GAAS;gBAC7B,GAAG,QAAQ;gBACX,QAAQ,EAAE,EAAE;gBACZ,KAAK,EAAE;oBACL,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE;oBAC9C,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE;iBAChD;aACF,CAAC;YACF,MAAM,GAAG,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;YAChD,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACpC,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACxC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChD,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;YACrE,MAAM,aAAa,GAAS;gBAC1B,GAAG,QAAQ;gBACX,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE;oBACL,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE;oBAC9C,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE;oBAC9C,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE;oBAC/C,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE;iBAChD;aACF,CAAC;YACF,MAAM,GAAG,GAAG,iBAAiB,CAAC,aAAa,CAAC,CAAC;YAC7C,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACjC,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACxC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sFAAsF,EAAE,GAAG,EAAE;YAC9F,MAAM,cAAc,GAAS;gBAC3B,GAAG,QAAQ;gBACX,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE,SAAS;gBAChB,cAAc,EAAE,EAAE;gBAClB,WAAW,EAAE,EAAE;aAChB,CAAC;YACF,MAAM,GAAG,GAAG,iBAAiB,CAAC,cAAc,CAAC,CAAC;YAC9C,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACvC,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACxC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8EAA8E,EAAE,GAAG,EAAE;YACtF,MAAM,gBAAgB,GAAS;gBAC7B,GAAG,QAAQ;gBACX,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE,SAAS;gBAChB,cAAc,EAAE,CAAC;gBACjB,WAAW,EAAE,CAAC;gBACd,eAAe,EAAE,sBAAsB;gBACvC,OAAO,EAAE,sBAAsB;aAChC,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,cAAc;YAChE,MAAM,GAAG,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;YAC5E,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;YAC5C,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACxC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;YACzD,MAAM,IAAI,GAAS;gBACjB,GAAG,QAAQ;gBACX,cAAc,EAAE,EAAE;gBAClB,WAAW,EAAE,EAAE;gBACf,eAAe,EAAE,sBAAsB;gBACvC,OAAO,EAAE,sBAAsB;gBAC/B,QAAQ,EAAE,EAAE;aACb,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,eAAe;YACjE,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;YAE/D,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa;YAChD,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa;YAC/C,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC3C,MAAM,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;YAClE,MAAM,IAAI,GAAS;gBACjB,GAAG,QAAQ;gBACX,cAAc,EAAE,EAAE;gBAClB,WAAW,EAAE,EAAE,EAAE,YAAY;gBAC7B,QAAQ,EAAE,EAAE,EAAE,4BAA4B;gBAC1C,eAAe,EAAE,sBAAsB;gBACvC,OAAO,EAAE,sBAAsB;aAChC,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,wBAAwB;YAC1E,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;YAE/D,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,eAAe;YACnD,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc;YACrD,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YACtD,MAAM,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;QAC3D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gDAAgD,EAAE,GAAG,EAAE;QAC9D,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;YACjC,MAAM,UAAU,GAAe;gBAC7B,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;aACrI,CAAC;YACF,MAAM,IAAI,GAAS,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;YACrF,MAAM,OAAO,GAAG,8BAA8B,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YAEjE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;YACxD,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;YAClC,MAAM,UAAU,GAAe;gBAC7B,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;aACpI,CAAC;YACF,MAAM,IAAI,GAAS,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;YACpF,MAAM,OAAO,GAAG,8BAA8B,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YAEjE,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;YACtC,MAAM,UAAU,GAAe;gBAC7B,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;aACpI,CAAC;YACF,MAAM,IAAI,GAAS,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;YACpF,MAAM,OAAO,GAAG,8BAA8B,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YAEjE,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YACpC,MAAM,UAAU,GAAe;gBAC7B,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;gBACnI,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;aACpI,CAAC;YACF,MAAM,IAAI,GAAS,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC;YACpF,MAAM,OAAO,GAAG,8BAA8B,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YAEjE,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mDAAmD,EAAE,GAAG,EAAE;QACjE,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;YACtE,MAAM,IAAI,GAAS;gBACjB,GAAG,QAAQ;gBACX,gBAAgB,EAAE,sBAAsB;gBACxC,eAAe,EAAE,sBAAsB;gBACvC,OAAO,EAAE,sBAAsB;gBAC/B,aAAa,EAAE,sBAAsB;gBACrC,MAAM,EAAE,MAAM;gBACd,cAAc,EAAE,WAAW;gBAC3B,cAAc,EAAE,EAAE;gBAClB,WAAW,EAAE,EAAE;aAChB,CAAC;YAEF,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;YAC3C,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACnD,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACzD,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU;YACjE,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,wBAAwB;YACnF,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,wBAAwB;YACrF,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YACtE,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAClD,EAAE,CAAC,qFAAqF,EAAE,GAAG,EAAE;YAC7F,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;gBAC/B,SAAS,EAAE,QAAQ;gBACnB,KAAK,EAAE,QAAQ;gBACf,MAAM,EAAE,MAAM;aACf,CAAC,CAAC;YAEH,gDAAgD;YAChD,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;YACnC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,WAAW,EAAE,CAAC;YAC7C,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,aAAa,EAAE,CAAC;YAE7C,uCAAuC;YACvC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC5B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,WAAW,EAAE,CAAC;YAE3C,+DAA+D;YAC/D,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;YACnC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,aAAa,EAAE,CAAC;YAC7C,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;YAC5F,MAAM,KAAK,GAAG,IAAI,aAAa,EAAE,CAAC;YAClC,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YAEjD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;YACpE,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC;gBACnC,SAAS,EAAE,OAAO,CAAC,EAAE;gBACrB,KAAK,EAAE,gBAAgB;gBACvB,cAAc,EAAE,EAAE;aACnB,CAAC,CAAC;YAEH,sBAAsB;YACtB,MAAM,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;YAC5D,MAAM,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;YAEtE,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAC/B,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtC,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClD,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtD,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACrD,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtD,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAEzC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7D,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAC/B,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;YAEzD,sCAAsC;YACtC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;YACtC,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Task, TimeEntry, User, Team, WorkloadDistribution, WorkloadDistributionOptions } from '../types/index.js';
2
+ export interface WorkloadContext {
3
+ tasks: Task[];
4
+ timeEntries?: TimeEntry[];
5
+ users?: User[];
6
+ teams?: Team[];
7
+ projectId?: string;
8
+ }
9
+ export declare function calculateWorkloadDistribution(context: WorkloadContext, options?: WorkloadDistributionOptions): WorkloadDistribution;
10
+ //# sourceMappingURL=workload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload.d.ts","sourceRoot":"","sources":["../../src/domain/workload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,SAAS,EACT,IAAI,EACJ,IAAI,EAKJ,oBAAoB,EACpB,2BAA2B,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,WAAW,CAAC,EAAE,SAAS,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+DD,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CA6TtB"}
@@ -0,0 +1,349 @@
1
+ function toDateString(d) {
2
+ return d.toISOString().split('T')[0];
3
+ }
4
+ function parseDateOnly(isoString) {
5
+ const parts = isoString.split('T')[0].split('-');
6
+ return new Date(Date.UTC(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)));
7
+ }
8
+ function addDays(d, days) {
9
+ const res = new Date(d.getTime());
10
+ res.setUTCDate(res.getUTCDate() + days);
11
+ return res;
12
+ }
13
+ function startOfWeek(d) {
14
+ const date = new Date(d.getTime());
15
+ const day = date.getUTCDay();
16
+ // Monday is day 1, Sunday is day 0 -> shift Sunday to 7
17
+ const diff = date.getUTCDate() - day + (day === 0 ? -6 : 1);
18
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), diff));
19
+ }
20
+ function startOfMonth(d) {
21
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
22
+ }
23
+ function nextBucketDate(d, interval) {
24
+ if (interval === 'day') {
25
+ return addDays(d, 1);
26
+ }
27
+ if (interval === 'week') {
28
+ return addDays(d, 7);
29
+ }
30
+ // month: next month 1st
31
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1));
32
+ }
33
+ function getBucketKey(d, interval) {
34
+ if (interval === 'day') {
35
+ return toDateString(d);
36
+ }
37
+ if (interval === 'week') {
38
+ return toDateString(startOfWeek(d));
39
+ }
40
+ return toDateString(startOfMonth(d));
41
+ }
42
+ function getTaskDurationHours(task) {
43
+ if (typeof task.estimatedHours === 'number' && task.estimatedHours >= 0) {
44
+ return task.estimatedHours;
45
+ }
46
+ if (typeof task.estimatedDurationMinutes === 'number' && task.estimatedDurationMinutes >= 0) {
47
+ return task.estimatedDurationMinutes / 60;
48
+ }
49
+ if (typeof task.actualHours === 'number' && task.actualHours > 0) {
50
+ return task.actualHours;
51
+ }
52
+ return 1;
53
+ }
54
+ export function calculateWorkloadDistribution(context, options = {}) {
55
+ const { tasks, timeEntries = [], users = [], teams = [], projectId } = context;
56
+ const interval = options.interval || 'week';
57
+ const groupBy = options.groupBy || 'assignee';
58
+ const metric = options.metric || 'blended';
59
+ const defaultWeeklyCapacity = options.defaultWeeklyCapacityHours ?? 40;
60
+ // Build lookups for human-readable labels
61
+ const userMap = new Map(users.map((u) => [u.id, u]));
62
+ const teamMap = new Map(teams.map((t) => [t.id, t]));
63
+ // 1. Determine date range
64
+ let minDate = options.startDate ? parseDateOnly(options.startDate) : null;
65
+ let maxDate = options.endDate ? parseDateOnly(options.endDate) : null;
66
+ if (!minDate || !maxDate) {
67
+ const dates = [];
68
+ for (const t of tasks) {
69
+ if (t.plannedStartDate)
70
+ dates.push(parseDateOnly(t.plannedStartDate).getTime());
71
+ if (t.actualStartDate)
72
+ dates.push(parseDateOnly(t.actualStartDate).getTime());
73
+ if (t.dueDate)
74
+ dates.push(parseDateOnly(t.dueDate).getTime());
75
+ if (t.actualEndDate)
76
+ dates.push(parseDateOnly(t.actualEndDate).getTime());
77
+ if (t.createdAt)
78
+ dates.push(parseDateOnly(t.createdAt).getTime());
79
+ }
80
+ for (const te of timeEntries) {
81
+ if (te.loggedAt)
82
+ dates.push(parseDateOnly(te.loggedAt).getTime());
83
+ }
84
+ if (dates.length > 0) {
85
+ if (!minDate)
86
+ minDate = new Date(Math.min(...dates));
87
+ if (!maxDate)
88
+ maxDate = new Date(Math.max(...dates));
89
+ }
90
+ else {
91
+ const now = new Date();
92
+ if (!minDate)
93
+ minDate = addDays(now, -7);
94
+ if (!maxDate)
95
+ maxDate = addDays(now, 28);
96
+ }
97
+ }
98
+ // Ensure minDate <= maxDate
99
+ if (minDate > maxDate) {
100
+ maxDate = new Date(minDate.getTime());
101
+ }
102
+ // Align start to bucket boundary
103
+ let cursor = interval === 'week' ? startOfWeek(minDate) : interval === 'month' ? startOfMonth(minDate) : minDate;
104
+ const endLimit = interval === 'week' ? startOfWeek(maxDate) : interval === 'month' ? startOfMonth(maxDate) : maxDate;
105
+ const bucketList = [];
106
+ const bucketMap = new Map();
107
+ while (cursor <= endLimit || bucketList.length === 0) {
108
+ const dateStr = toDateString(cursor);
109
+ const nextDate = nextBucketDate(cursor, interval);
110
+ const b = {
111
+ date: dateStr,
112
+ startDate: new Date(cursor.getTime()),
113
+ endDate: nextDate,
114
+ timestamp: cursor.getTime(),
115
+ values: {}
116
+ };
117
+ bucketList.push(b);
118
+ bucketMap.set(dateStr, b);
119
+ cursor = nextDate;
120
+ }
121
+ const seriesKeySet = new Set();
122
+ const getDimensionKey = (task, timeEntry) => {
123
+ switch (groupBy) {
124
+ case 'assignee':
125
+ if (timeEntry?.userId)
126
+ return timeEntry.userId;
127
+ return task.assigneeId || 'unassigned';
128
+ case 'team':
129
+ return task.teamId || task.customFields?.teamId || 'unassigned';
130
+ case 'taskType':
131
+ return task.taskType || task.customFields?.type || 'standard';
132
+ case 'priority':
133
+ return task.priority || 'none';
134
+ case 'status':
135
+ return task.status || 'todo';
136
+ default:
137
+ return 'unassigned';
138
+ }
139
+ };
140
+ const addValue = (bucket, key, hours) => {
141
+ if (hours <= 0)
142
+ return;
143
+ seriesKeySet.add(key);
144
+ bucket.values[key] = (bucket.values[key] || 0) + hours;
145
+ };
146
+ const todayStr = toDateString(new Date());
147
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
148
+ // 2. Process Logged Hours from TimeEntries
149
+ if (metric === 'logged' || metric === 'blended') {
150
+ for (const entry of timeEntries) {
151
+ if (!entry.hours || entry.hours <= 0)
152
+ continue;
153
+ const entryDate = parseDateOnly(entry.loggedAt);
154
+ const bKey = getBucketKey(entryDate, interval);
155
+ const bucket = bucketMap.get(bKey);
156
+ if (bucket) {
157
+ const associatedTask = taskMap.get(entry.taskId) || {
158
+ id: entry.taskId,
159
+ projectId: projectId || '',
160
+ title: '',
161
+ status: 'in_progress',
162
+ priority: 'medium',
163
+ createdAt: entry.loggedAt,
164
+ updatedAt: entry.loggedAt
165
+ };
166
+ const dimKey = getDimensionKey(associatedTask, entry);
167
+ addValue(bucket, dimKey, entry.hours);
168
+ }
169
+ }
170
+ }
171
+ // 3. Process Scheduled / Remaining / Blended Hours from Tasks
172
+ if (metric === 'scheduled' || metric === 'remaining' || metric === 'blended') {
173
+ for (const task of tasks) {
174
+ const estimated = getTaskDurationHours(task);
175
+ const logged = task.loggedHours || 0;
176
+ let hoursToDistribute = 0;
177
+ if (metric === 'scheduled') {
178
+ hoursToDistribute = estimated;
179
+ }
180
+ else if (metric === 'remaining' || metric === 'blended') {
181
+ if (task.semanticStatus === 'completed' || task.semanticStatus === 'canceled') {
182
+ hoursToDistribute = 0;
183
+ }
184
+ else {
185
+ hoursToDistribute = Math.max(0, estimated - logged);
186
+ }
187
+ }
188
+ if (hoursToDistribute <= 0)
189
+ continue;
190
+ const dimKey = getDimensionKey(task);
191
+ // Determine task date range
192
+ let taskStart = task.plannedStartDate
193
+ ? parseDateOnly(task.plannedStartDate)
194
+ : task.actualStartDate
195
+ ? parseDateOnly(task.actualStartDate)
196
+ : task.createdAt
197
+ ? parseDateOnly(task.createdAt)
198
+ : minDate;
199
+ let taskEnd = task.dueDate
200
+ ? parseDateOnly(task.dueDate)
201
+ : taskStart
202
+ ? addDays(taskStart, Math.max(1, Math.ceil(hoursToDistribute / 8)))
203
+ : maxDate;
204
+ // In blended mode, uncompleted tasks' remaining hours start from today or planned start (whichever is later)
205
+ if (metric === 'blended') {
206
+ const today = parseDateOnly(todayStr);
207
+ if (taskStart < today) {
208
+ taskStart = today;
209
+ }
210
+ if (taskEnd < taskStart) {
211
+ taskEnd = addDays(taskStart, 1);
212
+ }
213
+ }
214
+ if (taskEnd < taskStart) {
215
+ taskEnd = taskStart;
216
+ }
217
+ // Calculate days in task window
218
+ const msPerDay = 1000 * 60 * 60 * 24;
219
+ const totalDays = Math.max(1, Math.round((taskEnd.getTime() - taskStart.getTime()) / msPerDay) + 1);
220
+ const hoursPerDay = hoursToDistribute / totalDays;
221
+ // Distribute across overlapping buckets
222
+ for (const bucket of bucketList) {
223
+ const bucketLastDay = addDays(bucket.endDate, -1);
224
+ const overlapStart = new Date(Math.max(taskStart.getTime(), bucket.startDate.getTime()));
225
+ const overlapEnd = new Date(Math.min(taskEnd.getTime(), bucketLastDay.getTime()));
226
+ if (overlapStart <= overlapEnd) {
227
+ const overlapDays = Math.round((overlapEnd.getTime() - overlapStart.getTime()) / msPerDay) + 1;
228
+ const bucketEffort = overlapDays * hoursPerDay;
229
+ addValue(bucket, dimKey, bucketEffort);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ // Ensure all registered users are in series keys if grouping by assignee
235
+ if (groupBy === 'assignee' && users.length > 0) {
236
+ for (const u of users) {
237
+ seriesKeySet.add(u.id);
238
+ }
239
+ }
240
+ if (groupBy === 'team' && teams.length > 0) {
241
+ for (const t of teams) {
242
+ seriesKeySet.add(t.id);
243
+ }
244
+ }
245
+ const seriesKeys = Array.from(seriesKeySet).sort();
246
+ // Create human-readable series labels
247
+ const seriesLabels = {};
248
+ for (const key of seriesKeys) {
249
+ if (groupBy === 'assignee') {
250
+ const u = userMap.get(key);
251
+ seriesLabels[key] = u ? u.name : key === 'unassigned' ? 'Unassigned' : key;
252
+ }
253
+ else if (groupBy === 'team') {
254
+ const t = teamMap.get(key);
255
+ seriesLabels[key] = t ? t.name : key === 'unassigned' ? 'Unassigned' : key;
256
+ }
257
+ else {
258
+ seriesLabels[key] = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' ');
259
+ }
260
+ }
261
+ // 4. Calculate Capacity & Normalize Buckets
262
+ let totalDistributedHours = 0;
263
+ let sumCapacityAcrossBuckets = 0;
264
+ const buckets = bucketList.map((ib) => {
265
+ const values = {};
266
+ const capacity = {};
267
+ let bucketTotalHours = 0;
268
+ let bucketTotalCapacity = 0;
269
+ for (const key of seriesKeys) {
270
+ const rawHours = ib.values[key] || 0;
271
+ const roundedHours = Math.round(rawHours * 100) / 100;
272
+ values[key] = roundedHours;
273
+ bucketTotalHours += roundedHours;
274
+ // Capacity calculation
275
+ let weeklyCap = defaultWeeklyCapacity;
276
+ if (options.capacityOverrides && typeof options.capacityOverrides[key] === 'number') {
277
+ weeklyCap = options.capacityOverrides[key];
278
+ }
279
+ else if (groupBy === 'assignee') {
280
+ const u = userMap.get(key);
281
+ if (u?.weeklyCapacityHours)
282
+ weeklyCap = u.weeklyCapacityHours;
283
+ else if (key === 'unassigned')
284
+ weeklyCap = 0;
285
+ }
286
+ else if (groupBy === 'team') {
287
+ const t = teamMap.get(key);
288
+ if (t?.weeklyCapacityHours) {
289
+ weeklyCap = t.weeklyCapacityHours;
290
+ }
291
+ else if (t?.memberIds?.length) {
292
+ weeklyCap = t.memberIds.length * defaultWeeklyCapacity;
293
+ }
294
+ else if (key === 'unassigned') {
295
+ weeklyCap = 0;
296
+ }
297
+ }
298
+ let bucketCap = 0;
299
+ if (interval === 'week') {
300
+ bucketCap = weeklyCap;
301
+ }
302
+ else if (interval === 'day') {
303
+ bucketCap = Math.round((weeklyCap / 5) * 100) / 100;
304
+ }
305
+ else if (interval === 'month') {
306
+ const daysInMonth = new Date(Date.UTC(ib.startDate.getUTCFullYear(), ib.startDate.getUTCMonth() + 1, 0)).getUTCDate();
307
+ bucketCap = Math.round((weeklyCap * (daysInMonth / 7)) * 100) / 100;
308
+ }
309
+ capacity[key] = bucketCap;
310
+ bucketTotalCapacity += bucketCap;
311
+ }
312
+ bucketTotalHours = Math.round(bucketTotalHours * 100) / 100;
313
+ bucketTotalCapacity = Math.round(bucketTotalCapacity * 100) / 100;
314
+ totalDistributedHours += bucketTotalHours;
315
+ sumCapacityAcrossBuckets += bucketTotalCapacity;
316
+ const utilizationRatio = bucketTotalCapacity > 0
317
+ ? Math.round((bucketTotalHours / bucketTotalCapacity) * 1000) / 1000
318
+ : undefined;
319
+ return {
320
+ date: ib.date,
321
+ timestamp: ib.timestamp,
322
+ totalHours: bucketTotalHours,
323
+ values,
324
+ capacity,
325
+ totalCapacity: bucketTotalCapacity,
326
+ utilizationRatio
327
+ };
328
+ });
329
+ totalDistributedHours = Math.round(totalDistributedHours * 100) / 100;
330
+ sumCapacityAcrossBuckets = Math.round(sumCapacityAcrossBuckets * 100) / 100;
331
+ const averageUtilization = sumCapacityAcrossBuckets > 0
332
+ ? Math.round((totalDistributedHours / sumCapacityAcrossBuckets) * 1000) / 1000
333
+ : undefined;
334
+ return {
335
+ projectId,
336
+ startDate: toDateString(minDate),
337
+ endDate: toDateString(maxDate),
338
+ interval,
339
+ groupBy,
340
+ metric,
341
+ seriesKeys,
342
+ seriesLabels,
343
+ buckets,
344
+ totalHours: totalDistributedHours,
345
+ totalCapacity: sumCapacityAcrossBuckets,
346
+ averageUtilization
347
+ };
348
+ }
349
+ //# sourceMappingURL=workload.js.map