@critical-path/core 0.14.0 → 0.15.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/CHANGELOG.md +24 -0
- package/README.md +22 -0
- package/dist/deliverables.test.js +2 -2
- package/dist/deliverables.test.js.map +1 -1
- package/dist/domain/entities.d.ts +2 -1
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +11 -4
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/events.d.ts +8 -1
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/engine/index.d.ts +7 -3
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +102 -13
- package/dist/engine/index.js.map +1 -1
- package/dist/index.test.js +64 -9
- package/dist/index.test.js.map +1 -1
- package/dist/status.test.d.ts +2 -0
- package/dist/status.test.d.ts.map +1 -0
- package/dist/status.test.js +199 -0
- package/dist/status.test.js.map +1 -0
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +12 -2
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/firebase.test.js +14 -1
- package/dist/store/firebase.test.js.map +1 -1
- package/dist/store/sqlite.test.js +1 -1
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +21 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/use-case-validation.test.js +1 -1
- package/dist/use-case-validation.test.js.map +1 -1
- package/dist/utils/status.d.ts +7 -10
- package/dist/utils/status.d.ts.map +1 -1
- package/dist/utils/status.js +104 -21
- package/dist/utils/status.js.map +1 -1
- package/dist/utils/workflow.d.ts +1 -0
- package/dist/utils/workflow.d.ts.map +1 -1
- package/dist/utils/workflow.js +65 -18
- package/dist/utils/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/deliverables.test.ts +2 -2
- package/src/domain/entities.ts +15 -5
- package/src/domain/events.ts +9 -0
- package/src/engine/index.ts +123 -14
- package/src/index.test.ts +77 -9
- package/src/status.test.ts +226 -0
- package/src/store/firebase.test.ts +17 -1
- package/src/store/firebase.ts +12 -2
- package/src/store/sqlite.test.ts +1 -1
- package/src/types/index.ts +23 -5
- package/src/use-case-validation.test.ts +1 -1
- package/src/utils/status.ts +119 -33
- package/src/utils/workflow.ts +75 -18
- package/tsconfig.tsbuildinfo +1 -1
package/src/types/index.ts
CHANGED
|
@@ -4,17 +4,33 @@ export type Priority = DefaultPriority | (string & {});
|
|
|
4
4
|
export type DefaultTaskStatus = 'backlog' | 'todo' | 'in_progress' | 'in_review' | 'done' | 'canceled';
|
|
5
5
|
export type TaskStatus = DefaultTaskStatus | (string & {});
|
|
6
6
|
|
|
7
|
-
export type
|
|
8
|
-
export type ExecutionState = 'active' | 'inactive';
|
|
7
|
+
export type SemanticStatus = 'not_started' | 'in_progress' | 'completed' | 'canceled';
|
|
9
8
|
|
|
10
9
|
export interface StatusDefinition {
|
|
11
10
|
key: string;
|
|
12
11
|
label: string;
|
|
13
|
-
|
|
14
|
-
executionState: ExecutionState;
|
|
15
|
-
isCancelled?: boolean;
|
|
12
|
+
category: SemanticStatus;
|
|
16
13
|
}
|
|
17
14
|
|
|
15
|
+
export interface TaskDerivedStatus {
|
|
16
|
+
semanticStatus: SemanticStatus;
|
|
17
|
+
isReady: boolean;
|
|
18
|
+
isBlocked: boolean;
|
|
19
|
+
blockingTaskIds: string[];
|
|
20
|
+
isOverdue: boolean;
|
|
21
|
+
isUpcoming: boolean;
|
|
22
|
+
isUnplanned: boolean;
|
|
23
|
+
isUnassigned: boolean;
|
|
24
|
+
isStalled: boolean;
|
|
25
|
+
isOverEstimate: boolean;
|
|
26
|
+
isPaceWarning: boolean;
|
|
27
|
+
isDone: boolean;
|
|
28
|
+
isActive: boolean;
|
|
29
|
+
isCancelled: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type TaskLifecycleState = TaskDerivedStatus;
|
|
33
|
+
|
|
18
34
|
export type Role = 'admin' | 'project_manager' | 'contributor' | 'viewer';
|
|
19
35
|
|
|
20
36
|
export interface User {
|
|
@@ -142,6 +158,7 @@ export interface Task {
|
|
|
142
158
|
title: string;
|
|
143
159
|
description?: string;
|
|
144
160
|
status: TaskStatus;
|
|
161
|
+
semanticStatus?: SemanticStatus;
|
|
145
162
|
priority: Priority;
|
|
146
163
|
taskType?: string; // e.g. "bug", "feature", "task", "epic"
|
|
147
164
|
assigneeId?: string;
|
|
@@ -352,6 +369,7 @@ export type WebhookEvent =
|
|
|
352
369
|
| 'task.updated'
|
|
353
370
|
| 'task.deleted'
|
|
354
371
|
| 'task.status_changed'
|
|
372
|
+
| 'task.unblocked'
|
|
355
373
|
| 'comment.created'
|
|
356
374
|
| 'comment.updated'
|
|
357
375
|
| 'comment.deleted'
|
|
@@ -138,7 +138,7 @@ describe('Critical Path Use Case Validation Suite', () => {
|
|
|
138
138
|
// Check Lifecycle State
|
|
139
139
|
const lifecycle = await engine.getTaskLifecycleState(featureTask.id);
|
|
140
140
|
expect(lifecycle?.isDone).toBe(true);
|
|
141
|
-
expect(lifecycle?.
|
|
141
|
+
expect(lifecycle?.semanticStatus).toBe('completed');
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
144
|
|
package/src/utils/status.ts
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Task,
|
|
3
3
|
StatusDefinition,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
SemanticStatus,
|
|
5
|
+
TaskDerivedStatus,
|
|
6
6
|
} from '../types/index.js';
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_STATUS_DEFINITIONS: Record<string, StatusDefinition> = {
|
|
9
|
-
backlog: { key: 'backlog', label: 'Backlog',
|
|
10
|
-
todo: { key: 'todo', label: 'To Do',
|
|
11
|
-
in_progress: { key: 'in_progress', label: 'In Progress',
|
|
12
|
-
in_review: { key: 'in_review', label: 'In Review',
|
|
13
|
-
done: { key: 'done', label: 'Done',
|
|
14
|
-
canceled: { key: 'canceled', label: 'Canceled',
|
|
9
|
+
backlog: { key: 'backlog', label: 'Backlog', category: 'not_started' },
|
|
10
|
+
todo: { key: 'todo', label: 'To Do', category: 'not_started' },
|
|
11
|
+
in_progress: { key: 'in_progress', label: 'In Progress', category: 'in_progress' },
|
|
12
|
+
in_review: { key: 'in_review', label: 'In Review', category: 'in_progress' },
|
|
13
|
+
done: { key: 'done', label: 'Done', category: 'completed' },
|
|
14
|
+
canceled: { key: 'canceled', label: 'Canceled', category: 'canceled' }
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
isCancelled: boolean;
|
|
23
|
-
isOverdue: boolean;
|
|
24
|
-
isUpcoming: boolean;
|
|
17
|
+
export interface DeriveTaskLifecycleOptions {
|
|
18
|
+
customDefinitions?: StatusDefinition[];
|
|
19
|
+
referenceDate?: Date;
|
|
20
|
+
upstreamTasks?: Task[];
|
|
21
|
+
stalledThresholdDays?: number;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
export function resolveStatusDefinition(
|
|
@@ -35,28 +32,66 @@ export function resolveStatusDefinition(
|
|
|
35
32
|
if (DEFAULT_STATUS_DEFINITIONS[statusKey]) {
|
|
36
33
|
return DEFAULT_STATUS_DEFINITIONS[statusKey];
|
|
37
34
|
}
|
|
35
|
+
|
|
38
36
|
// Fallback heuristic for unknown custom statuses:
|
|
39
|
-
const
|
|
40
|
-
|
|
37
|
+
const lower = statusKey.toLowerCase();
|
|
38
|
+
let category: SemanticStatus = 'not_started';
|
|
39
|
+
if (['canceled', 'cancelled', 'rejected', 'abandoned', 'void', 'dropped'].some((k) => lower.includes(k))) {
|
|
40
|
+
category = 'canceled';
|
|
41
|
+
} else if (['done', 'completed', 'finished', 'closed', 'approved', 'final', 'delivered', 'shipped'].some((k) => lower.includes(k))) {
|
|
42
|
+
category = 'completed';
|
|
43
|
+
} else if (['in_progress', 'active', 'working', 'in_review', 'review', 'testing', 'qa', 'building', 'editing'].some((k) => lower.includes(k))) {
|
|
44
|
+
category = 'in_progress';
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
return {
|
|
42
48
|
key: statusKey,
|
|
43
49
|
label: statusKey,
|
|
44
|
-
|
|
45
|
-
executionState: isActiveKey ? 'active' : 'inactive',
|
|
46
|
-
isCancelled: statusKey.toLowerCase().includes('cancel')
|
|
50
|
+
category
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export function deriveTaskLifecycleState(
|
|
51
55
|
task: Task,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
):
|
|
56
|
+
customDefinitionsOrOptions?: StatusDefinition[] | DeriveTaskLifecycleOptions,
|
|
57
|
+
legacyReferenceDate?: Date
|
|
58
|
+
): TaskDerivedStatus {
|
|
59
|
+
let customDefinitions: StatusDefinition[] | undefined;
|
|
60
|
+
let referenceDate = legacyReferenceDate ?? new Date();
|
|
61
|
+
let upstreamTasks: Task[] = [];
|
|
62
|
+
let stalledThresholdDays = 5;
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(customDefinitionsOrOptions)) {
|
|
65
|
+
customDefinitions = customDefinitionsOrOptions;
|
|
66
|
+
} else if (customDefinitionsOrOptions) {
|
|
67
|
+
customDefinitions = customDefinitionsOrOptions.customDefinitions;
|
|
68
|
+
if (customDefinitionsOrOptions.referenceDate) {
|
|
69
|
+
referenceDate = customDefinitionsOrOptions.referenceDate;
|
|
70
|
+
}
|
|
71
|
+
if (customDefinitionsOrOptions.upstreamTasks) {
|
|
72
|
+
upstreamTasks = customDefinitionsOrOptions.upstreamTasks;
|
|
73
|
+
}
|
|
74
|
+
if (customDefinitionsOrOptions.stalledThresholdDays !== undefined) {
|
|
75
|
+
stalledThresholdDays = customDefinitionsOrOptions.stalledThresholdDays;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
const def = resolveStatusDefinition(task.status, customDefinitions);
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
80
|
+
const semanticStatus: SemanticStatus = def.category;
|
|
81
|
+
const isDone = semanticStatus === 'completed' || semanticStatus === 'canceled';
|
|
82
|
+
const isActive = semanticStatus === 'in_progress';
|
|
83
|
+
const isCancelled = semanticStatus === 'canceled';
|
|
59
84
|
|
|
85
|
+
// Dependency & Flow Indicators
|
|
86
|
+
const blockingTasks = upstreamTasks.filter((t) => {
|
|
87
|
+
const upstreamDef = resolveStatusDefinition(t.status, customDefinitions);
|
|
88
|
+
return upstreamDef.category !== 'completed';
|
|
89
|
+
});
|
|
90
|
+
const blockingTaskIds = blockingTasks.map((t) => t.id);
|
|
91
|
+
const isBlocked = !isDone && blockingTaskIds.length > 0;
|
|
92
|
+
const isReady = semanticStatus === 'not_started' && blockingTaskIds.length === 0;
|
|
93
|
+
|
|
94
|
+
// Time & Schedule Indicators
|
|
60
95
|
let isOverdue = false;
|
|
61
96
|
if (!isDone && task.dueDate) {
|
|
62
97
|
const due = new Date(task.dueDate);
|
|
@@ -66,20 +101,71 @@ export function deriveTaskLifecycleState(
|
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
let isUpcoming = false;
|
|
69
|
-
if (
|
|
104
|
+
if (semanticStatus === 'not_started' && task.plannedStartDate) {
|
|
70
105
|
const plannedStart = new Date(task.plannedStartDate);
|
|
71
106
|
if (!isNaN(plannedStart.getTime()) && plannedStart > referenceDate) {
|
|
72
107
|
isUpcoming = true;
|
|
73
108
|
}
|
|
74
109
|
}
|
|
75
110
|
|
|
111
|
+
const isUnplanned = semanticStatus === 'not_started' && !task.iterationId && !task.plannedStartDate && !task.dueDate;
|
|
112
|
+
|
|
113
|
+
// Resource & Activity Indicators
|
|
114
|
+
const hasAssignee = Boolean(task.assigneeId || (task.assignees && task.assignees.length > 0));
|
|
115
|
+
const isUnassigned = !isDone && !hasAssignee;
|
|
116
|
+
|
|
117
|
+
let isStalled = false;
|
|
118
|
+
if (semanticStatus === 'in_progress') {
|
|
119
|
+
const lastActivity = task.updatedAt ? new Date(task.updatedAt) : (task.createdAt ? new Date(task.createdAt) : null);
|
|
120
|
+
if (lastActivity && !isNaN(lastActivity.getTime())) {
|
|
121
|
+
const thresholdMs = stalledThresholdDays * 24 * 60 * 60 * 1000;
|
|
122
|
+
if (referenceDate.getTime() - lastActivity.getTime() > thresholdMs) {
|
|
123
|
+
isStalled = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Effort & Estimate Indicators
|
|
129
|
+
let isOverEstimate = false;
|
|
130
|
+
const loggedH = task.loggedHours ?? task.actualHours;
|
|
131
|
+
const estimatedH = task.estimatedHours;
|
|
132
|
+
if (estimatedH !== undefined && estimatedH > 0 && loggedH !== undefined && loggedH > estimatedH) {
|
|
133
|
+
isOverEstimate = true;
|
|
134
|
+
}
|
|
135
|
+
const actualM = task.actualDurationMinutes;
|
|
136
|
+
const estimatedM = task.estimatedDurationMinutes;
|
|
137
|
+
if (estimatedM !== undefined && estimatedM > 0 && actualM !== undefined && actualM > estimatedM) {
|
|
138
|
+
isOverEstimate = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let isPaceWarning = false;
|
|
142
|
+
if (semanticStatus === 'in_progress' && task.actualStartDate) {
|
|
143
|
+
const start = new Date(task.actualStartDate);
|
|
144
|
+
if (!isNaN(start.getTime())) {
|
|
145
|
+
const elapsedMs = referenceDate.getTime() - start.getTime();
|
|
146
|
+
const elapsedHours = elapsedMs / (1000 * 60 * 60);
|
|
147
|
+
if (estimatedH !== undefined && estimatedH > 0 && elapsedHours > estimatedH) {
|
|
148
|
+
isPaceWarning = true;
|
|
149
|
+
} else if (estimatedM !== undefined && estimatedM > 0 && (elapsedMs / (1000 * 60)) > estimatedM) {
|
|
150
|
+
isPaceWarning = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
76
155
|
return {
|
|
77
|
-
|
|
78
|
-
|
|
156
|
+
semanticStatus,
|
|
157
|
+
isReady,
|
|
158
|
+
isBlocked,
|
|
159
|
+
blockingTaskIds,
|
|
160
|
+
isOverdue,
|
|
161
|
+
isUpcoming,
|
|
162
|
+
isUnplanned,
|
|
163
|
+
isUnassigned,
|
|
164
|
+
isStalled,
|
|
165
|
+
isOverEstimate,
|
|
166
|
+
isPaceWarning,
|
|
79
167
|
isDone,
|
|
80
168
|
isActive,
|
|
81
|
-
isCancelled
|
|
82
|
-
isOverdue,
|
|
83
|
-
isUpcoming
|
|
169
|
+
isCancelled
|
|
84
170
|
};
|
|
85
171
|
}
|
package/src/utils/workflow.ts
CHANGED
|
@@ -90,25 +90,38 @@ export const DEFAULT_VFX_WORKFLOW: Workflow = {
|
|
|
90
90
|
name: 'VFX Production Workflow',
|
|
91
91
|
description: 'Visual effects production pipeline from bidding and drafting to internal lead review and client supervisor approval.',
|
|
92
92
|
statuses: [
|
|
93
|
-
{ key: 'bidding', label: 'Bidding & Draft',
|
|
94
|
-
{ key: 'awarded', label: 'Awarded / Ready',
|
|
95
|
-
{ key: 'in_production', label: 'In Production',
|
|
96
|
-
{ key: 'internal_review', label: 'Internal Review',
|
|
97
|
-
{ key: 'client_review', label: 'Client Review',
|
|
98
|
-
{ key: 'revision_requested', label: 'Revision Requested',
|
|
99
|
-
{ key: 'approved', label: 'Approved (Final)',
|
|
93
|
+
{ key: 'bidding', label: 'Bidding & Draft', category: 'not_started' },
|
|
94
|
+
{ key: 'awarded', label: 'Awarded / Ready', category: 'not_started' },
|
|
95
|
+
{ key: 'in_production', label: 'In Production', category: 'in_progress' },
|
|
96
|
+
{ key: 'internal_review', label: 'Internal Review', category: 'in_progress' },
|
|
97
|
+
{ key: 'client_review', label: 'Client Review', category: 'in_progress' },
|
|
98
|
+
{ key: 'revision_requested', label: 'Revision Requested', category: 'in_progress' },
|
|
99
|
+
{ key: 'approved', label: 'Approved (Final)', category: 'completed' },
|
|
100
100
|
DEFAULT_STATUS_DEFINITIONS.canceled
|
|
101
101
|
],
|
|
102
102
|
transitions: [
|
|
103
103
|
{ name: 'Award Bid', fromStatusKey: 'bidding', toStatusKey: 'awarded' },
|
|
104
|
+
{ name: 'Start Directly from Bid', fromStatusKey: 'bidding', toStatusKey: 'in_production' },
|
|
104
105
|
{ name: 'Start Work', fromStatusKey: 'awarded', toStatusKey: 'in_production' },
|
|
106
|
+
{ name: 'Reopen Bid', fromStatusKey: 'awarded', toStatusKey: 'bidding' },
|
|
105
107
|
{ name: 'Submit to Lead Review', fromStatusKey: 'in_production', toStatusKey: 'internal_review' },
|
|
108
|
+
{ name: 'Fast-Track Client Review', fromStatusKey: 'in_production', toStatusKey: 'client_review' },
|
|
109
|
+
{ name: 'Direct Approval', fromStatusKey: 'in_production', toStatusKey: 'approved' },
|
|
106
110
|
{ name: 'Submit to Client Review', fromStatusKey: 'internal_review', toStatusKey: 'client_review' },
|
|
107
111
|
{ name: 'Request Internal Revisions', fromStatusKey: 'internal_review', toStatusKey: 'revision_requested' },
|
|
112
|
+
{ name: 'Return to Production', fromStatusKey: 'internal_review', toStatusKey: 'in_production' },
|
|
113
|
+
{ name: 'Approve from Lead Review', fromStatusKey: 'internal_review', toStatusKey: 'approved' },
|
|
108
114
|
{ name: 'Request Client Revisions', fromStatusKey: 'client_review', toStatusKey: 'revision_requested' },
|
|
109
|
-
{ name: '
|
|
115
|
+
{ name: 'Return to Lead Review', fromStatusKey: 'client_review', toStatusKey: 'internal_review' },
|
|
110
116
|
{ name: 'Approve Final Shot', fromStatusKey: 'client_review', toStatusKey: 'approved' },
|
|
111
|
-
{ name: '
|
|
117
|
+
{ name: 'Address Revisions in Production', fromStatusKey: 'revision_requested', toStatusKey: 'in_production' },
|
|
118
|
+
{ name: 'Resubmit to Lead Review', fromStatusKey: 'revision_requested', toStatusKey: 'internal_review' },
|
|
119
|
+
{ name: 'Resubmit to Client Review', fromStatusKey: 'revision_requested', toStatusKey: 'client_review' },
|
|
120
|
+
{ name: 'Reopen Shot', fromStatusKey: 'approved', toStatusKey: 'in_production' },
|
|
121
|
+
{ name: 'Reopen Lead Review', fromStatusKey: 'approved', toStatusKey: 'internal_review' },
|
|
122
|
+
{ name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' },
|
|
123
|
+
{ name: 'Restore to Bidding', fromStatusKey: 'canceled', toStatusKey: 'bidding' },
|
|
124
|
+
{ name: 'Restore to Production', fromStatusKey: 'canceled', toStatusKey: 'in_production' }
|
|
112
125
|
],
|
|
113
126
|
taskTypes: [
|
|
114
127
|
{ key: 'vfx_task', label: 'VFX Task', description: 'Department task (e.g. Matchmove, Animation, FX, Comp)', icon: 'film' },
|
|
@@ -126,27 +139,36 @@ export const DEFAULT_CREATIVE_WORKFLOW: Workflow = {
|
|
|
126
139
|
name: 'Creative Production Workflow',
|
|
127
140
|
description: 'Production pipeline for creative deliverables from briefing and concepts to internal review, client feedback, and final delivery.',
|
|
128
141
|
statuses: [
|
|
129
|
-
{ key: 'briefing', label: 'Briefing & Scoping',
|
|
130
|
-
{ key: 'concept', label: 'Concept & Storyboarding',
|
|
131
|
-
{ key: 'in_production', label: 'In Production',
|
|
132
|
-
{ key: 'internal_review', label: 'Internal Review',
|
|
133
|
-
{ key: 'client_review', label: 'Client Review',
|
|
134
|
-
{ key: 'revision_requested', label: 'Revision Requested',
|
|
135
|
-
{ key: 'approved', label: 'Approved',
|
|
136
|
-
{ key: 'delivered', label: 'Delivered',
|
|
142
|
+
{ key: 'briefing', label: 'Briefing & Scoping', category: 'not_started' },
|
|
143
|
+
{ key: 'concept', label: 'Concept & Storyboarding', category: 'in_progress' },
|
|
144
|
+
{ key: 'in_production', label: 'In Production', category: 'in_progress' },
|
|
145
|
+
{ key: 'internal_review', label: 'Internal Review', category: 'in_progress' },
|
|
146
|
+
{ key: 'client_review', label: 'Client Review', category: 'in_progress' },
|
|
147
|
+
{ key: 'revision_requested', label: 'Revision Requested', category: 'in_progress' },
|
|
148
|
+
{ key: 'approved', label: 'Approved', category: 'completed' },
|
|
149
|
+
{ key: 'delivered', label: 'Delivered', category: 'completed' },
|
|
137
150
|
DEFAULT_STATUS_DEFINITIONS.canceled
|
|
138
151
|
],
|
|
139
152
|
transitions: [
|
|
140
153
|
{ name: 'Start Concepts', fromStatusKey: 'briefing', toStatusKey: 'concept' },
|
|
141
154
|
{ name: 'Approve Concept for Production', fromStatusKey: 'concept', toStatusKey: 'in_production' },
|
|
155
|
+
{ name: 'Return to Briefing', fromStatusKey: 'concept', toStatusKey: 'briefing' },
|
|
142
156
|
{ name: 'Submit for Internal Review', fromStatusKey: 'in_production', toStatusKey: 'internal_review' },
|
|
157
|
+
{ name: 'Submit for Client Review', fromStatusKey: 'in_production', toStatusKey: 'client_review' },
|
|
143
158
|
{ name: 'Submit for Client Review', fromStatusKey: 'internal_review', toStatusKey: 'client_review' },
|
|
144
159
|
{ name: 'Request Revisions (Internal)', fromStatusKey: 'internal_review', toStatusKey: 'revision_requested' },
|
|
160
|
+
{ name: 'Return to Production', fromStatusKey: 'internal_review', toStatusKey: 'in_production' },
|
|
161
|
+
{ name: 'Approve from Lead Review', fromStatusKey: 'internal_review', toStatusKey: 'approved' },
|
|
145
162
|
{ name: 'Request Revisions (Client)', fromStatusKey: 'client_review', toStatusKey: 'revision_requested' },
|
|
163
|
+
{ name: 'Return to Internal Review', fromStatusKey: 'client_review', toStatusKey: 'internal_review' },
|
|
146
164
|
{ name: 'Address Revisions', fromStatusKey: 'revision_requested', toStatusKey: 'in_production' },
|
|
165
|
+
{ name: 'Resubmit to Lead Review', fromStatusKey: 'revision_requested', toStatusKey: 'internal_review' },
|
|
166
|
+
{ name: 'Resubmit to Client Review', fromStatusKey: 'revision_requested', toStatusKey: 'client_review' },
|
|
147
167
|
{ name: 'Approve Work', fromStatusKey: 'client_review', toStatusKey: 'approved' },
|
|
148
168
|
{ name: 'Deliver Final Assets', fromStatusKey: 'approved', toStatusKey: 'delivered' },
|
|
149
|
-
{ name: '
|
|
169
|
+
{ name: 'Reopen Deliverable', fromStatusKey: 'delivered', toStatusKey: 'in_production' },
|
|
170
|
+
{ name: 'Cancel', fromStatusKey: '*', toStatusKey: 'canceled' },
|
|
171
|
+
{ name: 'Restore', fromStatusKey: 'canceled', toStatusKey: 'briefing' }
|
|
150
172
|
],
|
|
151
173
|
taskTypes: [
|
|
152
174
|
{ key: 'deliverable', label: 'Deliverable', description: 'Finished asset, cutdown, or packaged export', icon: 'package' },
|
|
@@ -211,6 +233,41 @@ export function getAllowedNextStatuses(
|
|
|
211
233
|
return [allStatusKeys[currentIndex + 1]];
|
|
212
234
|
}
|
|
213
235
|
|
|
236
|
+
export function getAllowedTransitions(
|
|
237
|
+
workflow: Workflow | undefined,
|
|
238
|
+
currentStatus: string
|
|
239
|
+
): string[] {
|
|
240
|
+
if (!workflow || !workflow.statuses || workflow.statuses.length === 0) {
|
|
241
|
+
return Object.keys(DEFAULT_STATUS_DEFINITIONS);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const allStatusKeys = workflow.statuses.map((s) => s.key);
|
|
245
|
+
if (!allStatusKeys.includes(currentStatus)) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (workflow.transitions && workflow.transitions.length > 0) {
|
|
250
|
+
const matches = workflow.transitions.filter(
|
|
251
|
+
(t) => t.fromStatusKey === '*' || t.fromStatusKey === currentStatus
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const allowed = new Set<string>();
|
|
255
|
+
for (const t of matches) {
|
|
256
|
+
if (t.toStatusKey === '*') {
|
|
257
|
+
allStatusKeys.filter((k) => k !== currentStatus).forEach((k) => allowed.add(k));
|
|
258
|
+
} else if (t.toStatusKey !== currentStatus && allStatusKeys.includes(t.toStatusKey)) {
|
|
259
|
+
allowed.add(t.toStatusKey);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return Array.from(allowed);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const next = getAllowedNextStatuses(workflow, currentStatus);
|
|
267
|
+
const prev = getAllowedPreviousStatuses(workflow, currentStatus);
|
|
268
|
+
return Array.from(new Set([...next, ...prev]));
|
|
269
|
+
}
|
|
270
|
+
|
|
214
271
|
export function getAllowedPreviousStatuses(
|
|
215
272
|
workflow: Workflow | undefined,
|
|
216
273
|
currentStatus: string
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|