@theromans/convex-dobty 0.1.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 (133) hide show
  1. package/README.md +24 -0
  2. package/dist/client/index.d.ts +287 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +215 -0
  5. package/dist/client/index.js.map +1 -0
  6. package/dist/component/_generated/api.d.ts +23 -0
  7. package/dist/component/_generated/api.d.ts.map +1 -0
  8. package/dist/component/_generated/api.js +15 -0
  9. package/dist/component/_generated/api.js.map +1 -0
  10. package/dist/component/_generated/dataModel.d.ts +28 -0
  11. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  12. package/dist/component/_generated/dataModel.js +11 -0
  13. package/dist/component/_generated/dataModel.js.map +1 -0
  14. package/dist/component/_generated/server.d.ts +23 -0
  15. package/dist/component/_generated/server.d.ts.map +1 -0
  16. package/dist/component/_generated/server.js +18 -0
  17. package/dist/component/_generated/server.js.map +1 -0
  18. package/dist/component/assessment.d.ts +141 -0
  19. package/dist/component/assessment.d.ts.map +1 -0
  20. package/dist/component/assessment.js +498 -0
  21. package/dist/component/assessment.js.map +1 -0
  22. package/dist/component/convex.config.d.ts +3 -0
  23. package/dist/component/convex.config.d.ts.map +1 -0
  24. package/dist/component/convex.config.js +3 -0
  25. package/dist/component/convex.config.js.map +1 -0
  26. package/dist/component/metrics.d.ts +145 -0
  27. package/dist/component/metrics.d.ts.map +1 -0
  28. package/dist/component/metrics.js +425 -0
  29. package/dist/component/metrics.js.map +1 -0
  30. package/dist/component/potentialAnalysis.d.ts +510 -0
  31. package/dist/component/potentialAnalysis.d.ts.map +1 -0
  32. package/dist/component/potentialAnalysis.js +1035 -0
  33. package/dist/component/potentialAnalysis.js.map +1 -0
  34. package/dist/component/schema.d.ts +342 -0
  35. package/dist/component/schema.d.ts.map +1 -0
  36. package/dist/component/schema.js +194 -0
  37. package/dist/component/schema.js.map +1 -0
  38. package/dist/component/scoring.d.ts +6 -0
  39. package/dist/component/scoring.d.ts.map +1 -0
  40. package/dist/component/scoring.js +394 -0
  41. package/dist/component/scoring.js.map +1 -0
  42. package/dist/data/dm/cvr.d.ts +12 -0
  43. package/dist/data/dm/cvr.d.ts.map +1 -0
  44. package/dist/data/dm/cvr.js +71 -0
  45. package/dist/data/dm/cvr.js.map +1 -0
  46. package/dist/data/dm/guidelines.d.ts +9 -0
  47. package/dist/data/dm/guidelines.d.ts.map +1 -0
  48. package/dist/data/dm/guidelines.js +22 -0
  49. package/dist/data/dm/guidelines.js.map +1 -0
  50. package/dist/data/dm/methods.d.ts +10 -0
  51. package/dist/data/dm/methods.d.ts.map +1 -0
  52. package/dist/data/dm/methods.js +50 -0
  53. package/dist/data/dm/methods.js.map +1 -0
  54. package/dist/data/dm/systems.d.ts +10 -0
  55. package/dist/data/dm/systems.d.ts.map +1 -0
  56. package/dist/data/dm/systems.js +52 -0
  57. package/dist/data/dm/systems.js.map +1 -0
  58. package/dist/data/domains.d.ts +20 -0
  59. package/dist/data/domains.d.ts.map +1 -0
  60. package/dist/data/domains.js +264 -0
  61. package/dist/data/domains.js.map +1 -0
  62. package/dist/data/ek/cvr.d.ts +12 -0
  63. package/dist/data/ek/cvr.d.ts.map +1 -0
  64. package/dist/data/ek/cvr.js +58 -0
  65. package/dist/data/ek/cvr.js.map +1 -0
  66. package/dist/data/ek/guidelines.d.ts +9 -0
  67. package/dist/data/ek/guidelines.d.ts.map +1 -0
  68. package/dist/data/ek/guidelines.js +26 -0
  69. package/dist/data/ek/guidelines.js.map +1 -0
  70. package/dist/data/ek/methods.d.ts +9 -0
  71. package/dist/data/ek/methods.d.ts.map +1 -0
  72. package/dist/data/ek/methods.js +35 -0
  73. package/dist/data/ek/methods.js.map +1 -0
  74. package/dist/data/ek/systems.d.ts +9 -0
  75. package/dist/data/ek/systems.d.ts.map +1 -0
  76. package/dist/data/ek/systems.js +23 -0
  77. package/dist/data/ek/systems.js.map +1 -0
  78. package/dist/data/influence-areas.d.ts +26 -0
  79. package/dist/data/influence-areas.d.ts.map +1 -0
  80. package/dist/data/influence-areas.js +143 -0
  81. package/dist/data/influence-areas.js.map +1 -0
  82. package/dist/data/km/cvr.d.ts +13 -0
  83. package/dist/data/km/cvr.d.ts.map +1 -0
  84. package/dist/data/km/cvr.js +65 -0
  85. package/dist/data/km/cvr.js.map +1 -0
  86. package/dist/data/km/guidelines.d.ts +9 -0
  87. package/dist/data/km/guidelines.d.ts.map +1 -0
  88. package/dist/data/km/guidelines.js +20 -0
  89. package/dist/data/km/guidelines.js.map +1 -0
  90. package/dist/data/km/methods.d.ts +9 -0
  91. package/dist/data/km/methods.d.ts.map +1 -0
  92. package/dist/data/km/methods.js +28 -0
  93. package/dist/data/km/methods.js.map +1 -0
  94. package/dist/data/km/systems.d.ts +9 -0
  95. package/dist/data/km/systems.d.ts.map +1 -0
  96. package/dist/data/km/systems.js +18 -0
  97. package/dist/data/km/systems.js.map +1 -0
  98. package/dist/data/stammdaten.d.ts +7 -0
  99. package/dist/data/stammdaten.d.ts.map +1 -0
  100. package/dist/data/stammdaten.js +94 -0
  101. package/dist/data/stammdaten.js.map +1 -0
  102. package/dist/data/types.d.ts +113 -0
  103. package/dist/data/types.d.ts.map +1 -0
  104. package/dist/data/types.js +23 -0
  105. package/dist/data/types.js.map +1 -0
  106. package/package.json +38 -0
  107. package/src/client/index.ts +442 -0
  108. package/src/component/_generated/api.ts +35 -0
  109. package/src/component/_generated/component.ts +50 -0
  110. package/src/component/_generated/dataModel.ts +42 -0
  111. package/src/component/_generated/server.ts +48 -0
  112. package/src/component/assessment.ts +536 -0
  113. package/src/component/convex.config.ts +3 -0
  114. package/src/component/metrics.ts +479 -0
  115. package/src/component/potentialAnalysis.ts +1118 -0
  116. package/src/component/schema.ts +209 -0
  117. package/src/component/scoring.ts +485 -0
  118. package/src/data/dm/cvr.ts +75 -0
  119. package/src/data/dm/guidelines.ts +24 -0
  120. package/src/data/dm/methods.ts +53 -0
  121. package/src/data/dm/systems.ts +55 -0
  122. package/src/data/domains.ts +275 -0
  123. package/src/data/ek/cvr.ts +62 -0
  124. package/src/data/ek/guidelines.ts +28 -0
  125. package/src/data/ek/methods.ts +39 -0
  126. package/src/data/ek/systems.ts +25 -0
  127. package/src/data/influence-areas.ts +162 -0
  128. package/src/data/km/cvr.ts +69 -0
  129. package/src/data/km/guidelines.ts +22 -0
  130. package/src/data/km/methods.ts +30 -0
  131. package/src/data/km/systems.ts +20 -0
  132. package/src/data/stammdaten.ts +96 -0
  133. package/src/data/types.ts +183 -0
@@ -0,0 +1,1035 @@
1
+ /**
2
+ * 5DoBT Potential Value Analysis — CRUD + Compute functions.
3
+ * Implements the 7-step process (FR-D-PA.1 through FR-D-PA.7).
4
+ */
5
+ import { mutation, query } from "./_generated/server.js";
6
+ import { v } from "convex/values";
7
+ import { isValidSubArea } from "../data/influence-areas.js";
8
+ import { DEFAULT_SENSITIVITY, DEFAULT_LIFETIME } from "../data/types.js";
9
+ // ── Analysis Lifecycle ──
10
+ /** Create a new 5DoBT Potential Analysis */
11
+ export const createAnalysis = mutation({
12
+ args: {
13
+ workspaceId: v.string(),
14
+ userId: v.string(),
15
+ name: v.optional(v.string()),
16
+ },
17
+ handler: async (ctx, args) => {
18
+ const now = Date.now();
19
+ const id = await ctx.db.insert("potentialAnalyses", {
20
+ workspaceId: args.workspaceId,
21
+ name: args.name,
22
+ currentStep: 1,
23
+ status: "draft",
24
+ createdBy: args.userId,
25
+ createdAt: now,
26
+ updatedAt: now,
27
+ });
28
+ return { analysisId: id };
29
+ },
30
+ });
31
+ /** Get a full analysis with all step data */
32
+ export const getAnalysis = query({
33
+ args: { analysisId: v.id("potentialAnalyses") },
34
+ handler: async (ctx, args) => {
35
+ const analysis = await ctx.db.get(args.analysisId);
36
+ if (!analysis)
37
+ return null;
38
+ const charter = await ctx.db
39
+ .query("paProjectCharters")
40
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
41
+ .first();
42
+ const definitionsOfDone = await ctx.db
43
+ .query("paDefinitionsOfDone")
44
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
45
+ .collect();
46
+ const influenceAreas = await ctx.db
47
+ .query("paInfluenceAreas")
48
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
49
+ .collect();
50
+ const pointsOfChanges = await ctx.db
51
+ .query("paPointsOfChanges")
52
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
53
+ .collect();
54
+ const fieldsOfAction = await ctx.db
55
+ .query("paFieldsOfAction")
56
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
57
+ .collect();
58
+ const potentialStatuses = await ctx.db
59
+ .query("paPotentialStatuses")
60
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
61
+ .collect();
62
+ const factsFigures = await ctx.db
63
+ .query("paFactsFigures")
64
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
65
+ .collect();
66
+ const calculations = await ctx.db
67
+ .query("paCalculations")
68
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
69
+ .collect();
70
+ const summary = await ctx.db
71
+ .query("paCalculationSummaries")
72
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
73
+ .first();
74
+ return {
75
+ ...analysis,
76
+ charter,
77
+ definitionsOfDone: definitionsOfDone.sort((a, b) => a.number - b.number),
78
+ influenceAreas,
79
+ pointsOfChanges,
80
+ fieldsOfAction,
81
+ potentialStatuses,
82
+ factsFigures,
83
+ calculations,
84
+ summary,
85
+ };
86
+ },
87
+ });
88
+ /** Get analysis status summary */
89
+ export const getAnalysisStatus = query({
90
+ args: { analysisId: v.id("potentialAnalyses") },
91
+ handler: async (ctx, args) => {
92
+ const analysis = await ctx.db.get(args.analysisId);
93
+ if (!analysis)
94
+ return null;
95
+ const charter = await ctx.db
96
+ .query("paProjectCharters")
97
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
98
+ .first();
99
+ const dods = await ctx.db
100
+ .query("paDefinitionsOfDone")
101
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
102
+ .collect();
103
+ const areas = await ctx.db
104
+ .query("paInfluenceAreas")
105
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
106
+ .collect();
107
+ const pocs = await ctx.db
108
+ .query("paPointsOfChanges")
109
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
110
+ .collect();
111
+ const foas = await ctx.db
112
+ .query("paFieldsOfAction")
113
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
114
+ .collect();
115
+ const statuses = await ctx.db
116
+ .query("paPotentialStatuses")
117
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
118
+ .collect();
119
+ // Per-dimension counts
120
+ const dimensionCounts = {};
121
+ for (const foa of foas) {
122
+ if (!dimensionCounts[foa.dobt_dimension]) {
123
+ dimensionCounts[foa.dobt_dimension] = { foas: 0, sp: 0, nsp: 0 };
124
+ }
125
+ dimensionCounts[foa.dobt_dimension].foas++;
126
+ if (foa.influence_type === "SP")
127
+ dimensionCounts[foa.dobt_dimension].sp++;
128
+ else
129
+ dimensionCounts[foa.dobt_dimension].nsp++;
130
+ }
131
+ // Status lifecycle counts
132
+ const statusCounts = {};
133
+ for (const s of statuses) {
134
+ statusCounts[s.status] = (statusCounts[s.status] ?? 0) + 1;
135
+ }
136
+ return {
137
+ currentStep: analysis.currentStep,
138
+ status: analysis.status,
139
+ name: analysis.name,
140
+ steps: {
141
+ 1: { complete: !!charter, label: "Project Charter" },
142
+ 2: { complete: dods.length > 0, count: dods.length, label: "Definition of Done" },
143
+ 3: { complete: areas.length > 0, count: areas.length, label: "Influence Areas" },
144
+ 4: { complete: pocs.length > 0, count: pocs.length, label: "Points of Changes" },
145
+ 5: { complete: foas.length > 0, count: foas.length, label: "Fields of Action" },
146
+ 6: { complete: statuses.length > 0, count: statuses.length, label: "Potential Status" },
147
+ 7: { complete: false, label: "Calculation Summary" },
148
+ },
149
+ dimensionCounts,
150
+ statusCounts,
151
+ totals: {
152
+ sp: foas.filter((f) => f.influence_type === "SP").length,
153
+ nsp: foas.filter((f) => f.influence_type === "NSP").length,
154
+ },
155
+ };
156
+ },
157
+ });
158
+ // ── Step I: Project Charter ──
159
+ /** Create or update Project Charter */
160
+ export const updateProjectCharter = mutation({
161
+ args: {
162
+ analysisId: v.id("potentialAnalyses"),
163
+ name: v.string(),
164
+ date: v.string(),
165
+ business_case: v.string(),
166
+ problem_statement: v.string(),
167
+ goal: v.string(),
168
+ scope_in: v.string(),
169
+ scope_out: v.string(),
170
+ objectives: v.any(),
171
+ conditions: v.optional(v.string()),
172
+ risks: v.optional(v.any()),
173
+ costs: v.optional(v.string()),
174
+ milestones: v.optional(v.any()),
175
+ project_team: v.optional(v.any()),
176
+ },
177
+ handler: async (ctx, args) => {
178
+ const analysis = await ctx.db.get(args.analysisId);
179
+ if (!analysis)
180
+ throw new Error("Analysis not found");
181
+ const existing = await ctx.db
182
+ .query("paProjectCharters")
183
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
184
+ .first();
185
+ const { analysisId, ...charterData } = args;
186
+ if (existing) {
187
+ await ctx.db.replace(existing._id, { analysisId, ...charterData });
188
+ return { charterId: existing._id, updated: true };
189
+ }
190
+ else {
191
+ const id = await ctx.db.insert("paProjectCharters", { analysisId, ...charterData });
192
+ // Update analysis name from charter
193
+ await ctx.db.patch(args.analysisId, {
194
+ name: args.name,
195
+ status: "in_progress",
196
+ updatedAt: Date.now(),
197
+ });
198
+ return { charterId: id, updated: false };
199
+ }
200
+ },
201
+ });
202
+ // ── Step II: Definition of Done ──
203
+ /** Add a Definition of Done item */
204
+ export const addDefinitionOfDone = mutation({
205
+ args: {
206
+ analysisId: v.id("potentialAnalyses"),
207
+ definition: v.string(),
208
+ description: v.string(),
209
+ effect_type: v.string(),
210
+ },
211
+ handler: async (ctx, args) => {
212
+ const analysis = await ctx.db.get(args.analysisId);
213
+ if (!analysis)
214
+ throw new Error("Analysis not found");
215
+ const validTypes = ["organizational", "process_method", "systemic"];
216
+ if (!validTypes.includes(args.effect_type)) {
217
+ throw new Error(`effect_type must be one of: ${validTypes.join(", ")}`);
218
+ }
219
+ // Auto-increment number
220
+ const existing = await ctx.db
221
+ .query("paDefinitionsOfDone")
222
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
223
+ .collect();
224
+ const nextNumber = existing.length > 0
225
+ ? Math.max(...existing.map((d) => d.number)) + 1
226
+ : 1;
227
+ const id = await ctx.db.insert("paDefinitionsOfDone", {
228
+ analysisId: args.analysisId,
229
+ number: nextNumber,
230
+ definition: args.definition,
231
+ description: args.description,
232
+ effect_type: args.effect_type,
233
+ });
234
+ await ctx.db.patch(args.analysisId, { updatedAt: Date.now() });
235
+ return { dodId: id, number: nextNumber };
236
+ },
237
+ });
238
+ /** Update a Definition of Done item */
239
+ export const updateDefinitionOfDone = mutation({
240
+ args: {
241
+ dodId: v.id("paDefinitionsOfDone"),
242
+ definition: v.optional(v.string()),
243
+ description: v.optional(v.string()),
244
+ effect_type: v.optional(v.string()),
245
+ },
246
+ handler: async (ctx, args) => {
247
+ const dod = await ctx.db.get(args.dodId);
248
+ if (!dod)
249
+ throw new Error("Definition of Done not found");
250
+ if (args.effect_type) {
251
+ const validTypes = ["organizational", "process_method", "systemic"];
252
+ if (!validTypes.includes(args.effect_type)) {
253
+ throw new Error(`effect_type must be one of: ${validTypes.join(", ")}`);
254
+ }
255
+ }
256
+ const update = {};
257
+ if (args.definition !== undefined)
258
+ update.definition = args.definition;
259
+ if (args.description !== undefined)
260
+ update.description = args.description;
261
+ if (args.effect_type !== undefined)
262
+ update.effect_type = args.effect_type;
263
+ await ctx.db.patch(args.dodId, update);
264
+ await ctx.db.patch(dod.analysisId, { updatedAt: Date.now() });
265
+ },
266
+ });
267
+ /** Delete a Definition of Done item */
268
+ export const deleteDefinitionOfDone = mutation({
269
+ args: { dodId: v.id("paDefinitionsOfDone") },
270
+ handler: async (ctx, args) => {
271
+ const dod = await ctx.db.get(args.dodId);
272
+ if (!dod)
273
+ return;
274
+ await ctx.db.delete(args.dodId);
275
+ await ctx.db.patch(dod.analysisId, { updatedAt: Date.now() });
276
+ },
277
+ });
278
+ // ── Step III: Influence Areas ──
279
+ /** Select an influence area */
280
+ export const selectInfluenceArea = mutation({
281
+ args: {
282
+ analysisId: v.id("potentialAnalyses"),
283
+ dobt_dimension: v.string(),
284
+ sub_area_key: v.string(),
285
+ potential_description: v.string(),
286
+ },
287
+ handler: async (ctx, args) => {
288
+ const analysis = await ctx.db.get(args.analysisId);
289
+ if (!analysis)
290
+ throw new Error("Analysis not found");
291
+ if (!isValidSubArea(args.dobt_dimension, args.sub_area_key)) {
292
+ throw new Error(`Invalid sub-area: ${args.dobt_dimension}/${args.sub_area_key}`);
293
+ }
294
+ // Check if already selected
295
+ const existing = await ctx.db
296
+ .query("paInfluenceAreas")
297
+ .withIndex("by_analysis_dimension", (q) => q.eq("analysisId", args.analysisId).eq("dobt_dimension", args.dobt_dimension))
298
+ .collect();
299
+ const duplicate = existing.find((a) => a.sub_area_key === args.sub_area_key);
300
+ if (duplicate) {
301
+ // Update description
302
+ await ctx.db.patch(duplicate._id, { potential_description: args.potential_description });
303
+ return { areaId: duplicate._id, updated: true };
304
+ }
305
+ const id = await ctx.db.insert("paInfluenceAreas", {
306
+ analysisId: args.analysisId,
307
+ dobt_dimension: args.dobt_dimension,
308
+ sub_area_key: args.sub_area_key,
309
+ potential_description: args.potential_description,
310
+ });
311
+ await ctx.db.patch(args.analysisId, { updatedAt: Date.now() });
312
+ return { areaId: id, updated: false };
313
+ },
314
+ });
315
+ /** Deselect an influence area */
316
+ export const deselectInfluenceArea = mutation({
317
+ args: { areaId: v.id("paInfluenceAreas") },
318
+ handler: async (ctx, args) => {
319
+ const area = await ctx.db.get(args.areaId);
320
+ if (!area)
321
+ return;
322
+ await ctx.db.delete(args.areaId);
323
+ await ctx.db.patch(area.analysisId, { updatedAt: Date.now() });
324
+ },
325
+ });
326
+ // ── Step IV: Points of Changes ──
327
+ /** Add a Point of Change */
328
+ export const addPointOfChange = mutation({
329
+ args: {
330
+ analysisId: v.id("potentialAnalyses"),
331
+ influence_area_ref: v.id("paInfluenceAreas"),
332
+ poc_description: v.string(),
333
+ definition_of_done_refs: v.any(),
334
+ },
335
+ handler: async (ctx, args) => {
336
+ const analysis = await ctx.db.get(args.analysisId);
337
+ if (!analysis)
338
+ throw new Error("Analysis not found");
339
+ const area = await ctx.db.get(args.influence_area_ref);
340
+ if (!area)
341
+ throw new Error("Influence area not found");
342
+ const id = await ctx.db.insert("paPointsOfChanges", {
343
+ analysisId: args.analysisId,
344
+ influence_area_ref: args.influence_area_ref,
345
+ poc_description: args.poc_description,
346
+ definition_of_done_refs: args.definition_of_done_refs,
347
+ });
348
+ await ctx.db.patch(args.analysisId, { updatedAt: Date.now() });
349
+ return { pocId: id };
350
+ },
351
+ });
352
+ /** Delete a Point of Change */
353
+ export const deletePointOfChange = mutation({
354
+ args: { pocId: v.id("paPointsOfChanges") },
355
+ handler: async (ctx, args) => {
356
+ const poc = await ctx.db.get(args.pocId);
357
+ if (!poc)
358
+ return;
359
+ await ctx.db.delete(args.pocId);
360
+ await ctx.db.patch(poc.analysisId, { updatedAt: Date.now() });
361
+ },
362
+ });
363
+ // ── Step V: Fields of Action ──
364
+ /** Add a Field of Action */
365
+ export const addFieldOfAction = mutation({
366
+ args: {
367
+ analysisId: v.id("potentialAnalyses"),
368
+ poc_ref: v.id("paPointsOfChanges"),
369
+ potential_description: v.string(),
370
+ dobt_dimension: v.string(),
371
+ influence_type: v.string(),
372
+ foa_description: v.string(),
373
+ kpi_influence: v.string(),
374
+ },
375
+ handler: async (ctx, args) => {
376
+ const analysis = await ctx.db.get(args.analysisId);
377
+ if (!analysis)
378
+ throw new Error("Analysis not found");
379
+ const validDimensions = ["sales", "efficiency", "direct_cost", "risk", "innovation"];
380
+ if (!validDimensions.includes(args.dobt_dimension)) {
381
+ throw new Error(`dobt_dimension must be one of: ${validDimensions.join(", ")}`);
382
+ }
383
+ if (args.influence_type !== "SP" && args.influence_type !== "NSP") {
384
+ throw new Error("influence_type must be SP or NSP");
385
+ }
386
+ const poc = await ctx.db.get(args.poc_ref);
387
+ if (!poc)
388
+ throw new Error("Point of Change not found");
389
+ const id = await ctx.db.insert("paFieldsOfAction", {
390
+ analysisId: args.analysisId,
391
+ poc_ref: args.poc_ref,
392
+ potential_description: args.potential_description,
393
+ dobt_dimension: args.dobt_dimension,
394
+ influence_type: args.influence_type,
395
+ foa_description: args.foa_description,
396
+ kpi_influence: args.kpi_influence,
397
+ });
398
+ await ctx.db.patch(args.analysisId, { updatedAt: Date.now() });
399
+ return { foaId: id };
400
+ },
401
+ });
402
+ /** Delete a Field of Action */
403
+ export const deleteFieldOfAction = mutation({
404
+ args: { foaId: v.id("paFieldsOfAction") },
405
+ handler: async (ctx, args) => {
406
+ const foa = await ctx.db.get(args.foaId);
407
+ if (!foa)
408
+ return;
409
+ // Also delete associated potential status
410
+ const status = await ctx.db
411
+ .query("paPotentialStatuses")
412
+ .withIndex("by_foa", (q) => q.eq("foa_ref", args.foaId))
413
+ .first();
414
+ if (status)
415
+ await ctx.db.delete(status._id);
416
+ await ctx.db.delete(args.foaId);
417
+ await ctx.db.patch(foa.analysisId, { updatedAt: Date.now() });
418
+ },
419
+ });
420
+ // ── Step VI: Potential Status ──
421
+ /** Set or update potential status for an FOA */
422
+ export const setPotentialStatus = mutation({
423
+ args: {
424
+ analysisId: v.id("potentialAnalyses"),
425
+ foa_ref: v.id("paFieldsOfAction"),
426
+ status: v.string(),
427
+ validation_level: v.optional(v.string()),
428
+ estimation_value: v.optional(v.number()),
429
+ estimation_risk: v.optional(v.number()),
430
+ estimation_explanation: v.optional(v.string()),
431
+ calculation_ref: v.optional(v.id("paCalculations")),
432
+ calculation_result: v.optional(v.number()),
433
+ committed_value_result: v.optional(v.number()),
434
+ committed_responsible_business_owner: v.optional(v.string()),
435
+ measured_potential_value: v.optional(v.number()),
436
+ },
437
+ handler: async (ctx, args) => {
438
+ const validStatuses = ["validated", "estimated", "calculated", "committed", "measured"];
439
+ if (!validStatuses.includes(args.status)) {
440
+ throw new Error(`status must be one of: ${validStatuses.join(", ")}`);
441
+ }
442
+ // Compute estimation_result for estimated status
443
+ let estimation_result;
444
+ if (args.status === "estimated" && args.estimation_value !== undefined && args.estimation_risk !== undefined) {
445
+ estimation_result = args.estimation_value * (1 - args.estimation_risk);
446
+ }
447
+ const data = {
448
+ analysisId: args.analysisId,
449
+ foa_ref: args.foa_ref,
450
+ status: args.status,
451
+ validation_level: args.validation_level,
452
+ estimation_value: args.estimation_value,
453
+ estimation_risk: args.estimation_risk,
454
+ estimation_explanation: args.estimation_explanation,
455
+ estimation_result,
456
+ calculation_ref: args.calculation_ref,
457
+ calculation_result: args.calculation_result,
458
+ committed_value_result: args.committed_value_result,
459
+ committed_responsible_business_owner: args.committed_responsible_business_owner,
460
+ measured_potential_value: args.measured_potential_value,
461
+ };
462
+ const existing = await ctx.db
463
+ .query("paPotentialStatuses")
464
+ .withIndex("by_foa", (q) => q.eq("foa_ref", args.foa_ref))
465
+ .first();
466
+ let statusId;
467
+ if (existing) {
468
+ await ctx.db.replace(existing._id, data);
469
+ statusId = existing._id;
470
+ }
471
+ else {
472
+ statusId = await ctx.db.insert("paPotentialStatuses", data);
473
+ }
474
+ await ctx.db.patch(args.analysisId, { updatedAt: Date.now() });
475
+ return { statusId, estimation_result };
476
+ },
477
+ });
478
+ // ── Step VII: Facts & Figures ──
479
+ /** Add a Facts & Figures entry */
480
+ export const addFactFigure = mutation({
481
+ args: {
482
+ analysisId: v.id("potentialAnalyses"),
483
+ name: v.string(),
484
+ unit: v.string(),
485
+ value: v.number(),
486
+ },
487
+ handler: async (ctx, args) => {
488
+ const id = await ctx.db.insert("paFactsFigures", {
489
+ analysisId: args.analysisId,
490
+ name: args.name,
491
+ unit: args.unit,
492
+ value: args.value,
493
+ });
494
+ await ctx.db.patch(args.analysisId, { updatedAt: Date.now() });
495
+ return { factId: id };
496
+ },
497
+ });
498
+ /** Delete a Facts & Figures entry */
499
+ export const deleteFactFigure = mutation({
500
+ args: { factId: v.id("paFactsFigures") },
501
+ handler: async (ctx, args) => {
502
+ const fact = await ctx.db.get(args.factId);
503
+ if (!fact)
504
+ return;
505
+ await ctx.db.delete(args.factId);
506
+ await ctx.db.patch(fact.analysisId, { updatedAt: Date.now() });
507
+ },
508
+ });
509
+ /** Add a calculation entry */
510
+ export const addCalculation = mutation({
511
+ args: {
512
+ analysisId: v.id("potentialAnalyses"),
513
+ dobt_dimension: v.string(),
514
+ potential_description: v.string(),
515
+ assumptions: v.any(),
516
+ formula: v.string(),
517
+ result: v.number(),
518
+ },
519
+ handler: async (ctx, args) => {
520
+ const id = await ctx.db.insert("paCalculations", {
521
+ analysisId: args.analysisId,
522
+ dobt_dimension: args.dobt_dimension,
523
+ potential_description: args.potential_description,
524
+ assumptions: args.assumptions,
525
+ formula: args.formula,
526
+ result: args.result,
527
+ });
528
+ await ctx.db.patch(args.analysisId, { updatedAt: Date.now() });
529
+ return { calculationId: id };
530
+ },
531
+ });
532
+ /** Delete a calculation entry */
533
+ export const deleteCalculation = mutation({
534
+ args: { calcId: v.id("paCalculations") },
535
+ handler: async (ctx, args) => {
536
+ const calc = await ctx.db.get(args.calcId);
537
+ if (!calc)
538
+ return;
539
+ await ctx.db.delete(args.calcId);
540
+ await ctx.db.patch(calc.analysisId, { updatedAt: Date.now() });
541
+ },
542
+ });
543
+ // ── Step VII: Calculation Summary ──
544
+ /** Compute and cache the Calculation Summary */
545
+ export const computeCalculationSummary = mutation({
546
+ args: {
547
+ analysisId: v.id("potentialAnalyses"),
548
+ sensitivity: v.optional(v.any()),
549
+ lifetime: v.optional(v.any()),
550
+ },
551
+ handler: async (ctx, args) => {
552
+ const analysis = await ctx.db.get(args.analysisId);
553
+ if (!analysis)
554
+ throw new Error("Analysis not found");
555
+ const sensitivity = args.sensitivity ?? DEFAULT_SENSITIVITY;
556
+ const lifetime = args.lifetime ?? DEFAULT_LIFETIME;
557
+ // Load all FOAs and their statuses
558
+ const foas = await ctx.db
559
+ .query("paFieldsOfAction")
560
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
561
+ .collect();
562
+ const statuses = await ctx.db
563
+ .query("paPotentialStatuses")
564
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
565
+ .collect();
566
+ const statusByFoa = new Map(statuses.map((s) => [s.foa_ref.toString(), s]));
567
+ // Build dimension totals matrix
568
+ const dimensions = ["sales", "efficiency", "direct_cost", "risk", "innovation"];
569
+ const dimension_totals = {};
570
+ for (const dim of dimensions) {
571
+ dimension_totals[dim] = { sp_total: 0, nsp_total: 0, total: 0 };
572
+ }
573
+ const validated_potentials = [];
574
+ for (const foa of foas) {
575
+ const ps = statusByFoa.get(foa._id.toString());
576
+ if (!ps)
577
+ continue;
578
+ // Skip validated (qualitative only)
579
+ if (ps.status === "validated") {
580
+ validated_potentials.push({
581
+ foa_ref: foa._id.toString(),
582
+ potential_description: foa.potential_description,
583
+ dobt_dimension: foa.dobt_dimension,
584
+ validation_level: ps.validation_level ?? "medium",
585
+ });
586
+ continue;
587
+ }
588
+ // Get the monetary value based on status
589
+ let value = 0;
590
+ if (ps.status === "estimated") {
591
+ value = ps.estimation_result ?? 0;
592
+ }
593
+ else if (ps.status === "calculated") {
594
+ value = ps.calculation_result ?? 0;
595
+ }
596
+ else if (ps.status === "committed") {
597
+ value = ps.committed_value_result ?? 0;
598
+ }
599
+ else if (ps.status === "measured") {
600
+ value = ps.measured_potential_value ?? 0;
601
+ }
602
+ const dt = dimension_totals[foa.dobt_dimension];
603
+ if (!dt)
604
+ continue;
605
+ if (foa.influence_type === "SP") {
606
+ dt.sp_total += value;
607
+ }
608
+ else {
609
+ dt.nsp_total += value;
610
+ }
611
+ dt.total += value;
612
+ }
613
+ // Grand totals
614
+ let grand_total_sp = 0;
615
+ let grand_total_nsp = 0;
616
+ let grand_total = 0;
617
+ for (const dt of Object.values(dimension_totals)) {
618
+ grand_total_sp += dt.sp_total;
619
+ grand_total_nsp += dt.nsp_total;
620
+ grand_total += dt.total;
621
+ }
622
+ // Sensitivity analytics — margin effects from SP totals only
623
+ const sensitivityMap = {
624
+ sales: sensitivity.revenue_to_margin,
625
+ efficiency: sensitivity.efficiency_to_margin,
626
+ direct_cost: sensitivity.direct_cost_to_margin,
627
+ risk: sensitivity.risk_to_margin,
628
+ innovation: sensitivity.innovation_to_margin,
629
+ };
630
+ const margin_effects = {};
631
+ let total_margin_effect = 0;
632
+ for (const dim of dimensions) {
633
+ const me = dimension_totals[dim].sp_total * (sensitivityMap[dim] ?? 0);
634
+ margin_effects[dim] = Math.round(me);
635
+ total_margin_effect += Math.round(me);
636
+ }
637
+ // 3-year projection
638
+ const years = ["year_1", "year_2", "year_3"];
639
+ const yearRates = [lifetime.year_1, lifetime.year_2, lifetime.year_3];
640
+ const projection = {};
641
+ const projectionTotals = {};
642
+ for (const dim of dimensions) {
643
+ projectionTotals[dim] = 0;
644
+ }
645
+ let grandProjectionTotal = 0;
646
+ for (let yi = 0; yi < years.length; yi++) {
647
+ const yearKey = years[yi];
648
+ const rate = yearRates[yi];
649
+ projection[yearKey] = {};
650
+ let yearTotal = 0;
651
+ for (const dim of dimensions) {
652
+ const val = Math.round(margin_effects[dim] * rate);
653
+ projection[yearKey][dim] = val;
654
+ projectionTotals[dim] += val;
655
+ yearTotal += val;
656
+ }
657
+ projection[yearKey].total = yearTotal;
658
+ grandProjectionTotal += yearTotal;
659
+ }
660
+ projection.total = { ...projectionTotals, grand_total: grandProjectionTotal };
661
+ // Store summary
662
+ const summaryData = {
663
+ analysisId: args.analysisId,
664
+ dimension_totals,
665
+ grand_total_sp: Math.round(grand_total_sp),
666
+ grand_total_nsp: Math.round(grand_total_nsp),
667
+ grand_total: Math.round(grand_total),
668
+ sensitivity,
669
+ margin_effects,
670
+ total_margin_effect,
671
+ lifetime,
672
+ projection,
673
+ validated_potentials,
674
+ calculatedAt: Date.now(),
675
+ };
676
+ const existing = await ctx.db
677
+ .query("paCalculationSummaries")
678
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
679
+ .first();
680
+ if (existing) {
681
+ await ctx.db.replace(existing._id, summaryData);
682
+ return { summaryId: existing._id };
683
+ }
684
+ else {
685
+ const id = await ctx.db.insert("paCalculationSummaries", summaryData);
686
+ return { summaryId: id };
687
+ }
688
+ },
689
+ });
690
+ // ── Step Navigation ──
691
+ /** Advance to the next step with validation */
692
+ export const advanceStep = mutation({
693
+ args: {
694
+ analysisId: v.id("potentialAnalyses"),
695
+ targetStep: v.number(),
696
+ },
697
+ handler: async (ctx, args) => {
698
+ const analysis = await ctx.db.get(args.analysisId);
699
+ if (!analysis)
700
+ throw new Error("Analysis not found");
701
+ if (args.targetStep < 1 || args.targetStep > 7) {
702
+ throw new Error("targetStep must be between 1 and 7");
703
+ }
704
+ // Allow going back without validation
705
+ if (args.targetStep <= analysis.currentStep) {
706
+ return { currentStep: analysis.currentStep };
707
+ }
708
+ // Validate each step that must be complete
709
+ for (let step = analysis.currentStep; step < args.targetStep; step++) {
710
+ if (step === 1) {
711
+ const charter = await ctx.db
712
+ .query("paProjectCharters")
713
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
714
+ .first();
715
+ if (!charter)
716
+ throw new Error("Step I: Project Charter is required before advancing");
717
+ }
718
+ else if (step === 2) {
719
+ const dods = await ctx.db
720
+ .query("paDefinitionsOfDone")
721
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
722
+ .collect();
723
+ if (dods.length === 0)
724
+ throw new Error("Step II: At least one Definition of Done is required");
725
+ }
726
+ else if (step === 3) {
727
+ const areas = await ctx.db
728
+ .query("paInfluenceAreas")
729
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
730
+ .collect();
731
+ if (areas.length === 0)
732
+ throw new Error("Step III: At least one Influence Area must be selected");
733
+ }
734
+ else if (step === 4) {
735
+ const pocs = await ctx.db
736
+ .query("paPointsOfChanges")
737
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
738
+ .collect();
739
+ if (pocs.length === 0)
740
+ throw new Error("Step IV: At least one Point of Change is required");
741
+ }
742
+ else if (step === 5) {
743
+ const foas = await ctx.db
744
+ .query("paFieldsOfAction")
745
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
746
+ .collect();
747
+ if (foas.length === 0)
748
+ throw new Error("Step V: At least one Field of Action is required");
749
+ }
750
+ else if (step === 6) {
751
+ const statuses = await ctx.db
752
+ .query("paPotentialStatuses")
753
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
754
+ .collect();
755
+ if (statuses.length === 0)
756
+ throw new Error("Step VI: At least one Potential Status must be set");
757
+ }
758
+ }
759
+ const newStatus = args.targetStep === 7 ? "completed" : "in_progress";
760
+ await ctx.db.patch(args.analysisId, {
761
+ currentStep: args.targetStep,
762
+ status: newStatus,
763
+ updatedAt: Date.now(),
764
+ });
765
+ return { currentStep: args.targetStep };
766
+ },
767
+ });
768
+ /** List analyses for a workspace */
769
+ export const listAnalyses = query({
770
+ args: { workspaceId: v.string() },
771
+ handler: async (ctx, args) => {
772
+ return await ctx.db
773
+ .query("potentialAnalyses")
774
+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
775
+ .collect();
776
+ },
777
+ });
778
+ // ── AI Tool Queries (Section 7) ──
779
+ /** Suggest influence areas based on analysis context (AI tool data provider) */
780
+ export const suggestInfluenceAreas = query({
781
+ args: {
782
+ analysisId: v.id("potentialAnalyses"),
783
+ },
784
+ handler: async (ctx, args) => {
785
+ const analysis = await ctx.db.get(args.analysisId);
786
+ if (!analysis)
787
+ return null;
788
+ const charter = await ctx.db
789
+ .query("paProjectCharters")
790
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
791
+ .first();
792
+ const dods = await ctx.db
793
+ .query("paDefinitionsOfDone")
794
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
795
+ .collect();
796
+ // Already selected areas
797
+ const selected = await ctx.db
798
+ .query("paInfluenceAreas")
799
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
800
+ .collect();
801
+ const selectedKeys = new Set(selected.map((a) => `${a.dobt_dimension}/${a.sub_area_key}`));
802
+ // Return all available areas with selection state + context for AI reasoning
803
+ const { DOBT_DIMENSIONS } = await import("../data/influence-areas.js");
804
+ const availableAreas = DOBT_DIMENSIONS.flatMap((dim) => dim.subAreas.map((sa) => ({
805
+ dimension: dim.key,
806
+ dimensionLabel: dim.label,
807
+ subAreaKey: sa.key,
808
+ subAreaLabel: sa.label,
809
+ alreadySelected: selectedKeys.has(`${dim.key}/${sa.key}`),
810
+ })));
811
+ return {
812
+ charter: charter
813
+ ? {
814
+ name: charter.name,
815
+ business_case: charter.business_case,
816
+ problem_statement: charter.problem_statement,
817
+ goal: charter.goal,
818
+ scope_in: charter.scope_in,
819
+ }
820
+ : null,
821
+ definitionsOfDone: dods.map((d) => ({
822
+ definition: d.definition,
823
+ description: d.description,
824
+ effect_type: d.effect_type,
825
+ })),
826
+ availableAreas,
827
+ selectedCount: selected.length,
828
+ };
829
+ },
830
+ });
831
+ /** Provide estimation context for a Field of Action (AI tool data provider) */
832
+ export const estimatePotential = query({
833
+ args: {
834
+ analysisId: v.id("potentialAnalyses"),
835
+ foaId: v.optional(v.id("paFieldsOfAction")),
836
+ },
837
+ handler: async (ctx, args) => {
838
+ const analysis = await ctx.db.get(args.analysisId);
839
+ if (!analysis)
840
+ return null;
841
+ // Load all FOAs with their statuses for context
842
+ const foas = await ctx.db
843
+ .query("paFieldsOfAction")
844
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
845
+ .collect();
846
+ const statuses = await ctx.db
847
+ .query("paPotentialStatuses")
848
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
849
+ .collect();
850
+ const statusByFoa = new Map(statuses.map((s) => [s.foa_ref.toString(), s]));
851
+ // Build context with existing estimations for reference
852
+ const existingEstimations = foas
853
+ .filter((f) => {
854
+ const s = statusByFoa.get(f._id.toString());
855
+ return s && (s.status === "estimated" || s.status === "calculated");
856
+ })
857
+ .map((f) => {
858
+ const s = statusByFoa.get(f._id.toString());
859
+ return {
860
+ dimension: f.dobt_dimension,
861
+ influence_type: f.influence_type,
862
+ description: f.foa_description,
863
+ value: s.estimation_result ?? s.calculation_result ?? 0,
864
+ };
865
+ });
866
+ // Target FOA details if specified
867
+ let targetFoa = null;
868
+ if (args.foaId) {
869
+ const foa = await ctx.db.get(args.foaId);
870
+ if (foa) {
871
+ const ps = statusByFoa.get(foa._id.toString());
872
+ targetFoa = {
873
+ ...foa,
874
+ currentStatus: ps ?? null,
875
+ };
876
+ }
877
+ }
878
+ // Facts & figures for financial context
879
+ const facts = await ctx.db
880
+ .query("paFactsFigures")
881
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
882
+ .collect();
883
+ return {
884
+ targetFoa,
885
+ existingEstimations,
886
+ factsFigures: facts,
887
+ totalFoas: foas.length,
888
+ estimatedCount: statuses.filter((s) => s.status !== "validated").length,
889
+ };
890
+ },
891
+ });
892
+ /** Get full calculation summary (AI tool data provider) */
893
+ export const getCalculationSummary = query({
894
+ args: {
895
+ analysisId: v.id("potentialAnalyses"),
896
+ },
897
+ handler: async (ctx, args) => {
898
+ const summary = await ctx.db
899
+ .query("paCalculationSummaries")
900
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
901
+ .first();
902
+ if (!summary)
903
+ return null;
904
+ // Include FOA count breakdown by dimension
905
+ const foas = await ctx.db
906
+ .query("paFieldsOfAction")
907
+ .withIndex("by_analysis", (q) => q.eq("analysisId", args.analysisId))
908
+ .collect();
909
+ const foasByDimension = {};
910
+ for (const foa of foas) {
911
+ if (!foasByDimension[foa.dobt_dimension]) {
912
+ foasByDimension[foa.dobt_dimension] = { sp: 0, nsp: 0 };
913
+ }
914
+ if (foa.influence_type === "SP")
915
+ foasByDimension[foa.dobt_dimension].sp++;
916
+ else
917
+ foasByDimension[foa.dobt_dimension].nsp++;
918
+ }
919
+ return {
920
+ ...summary,
921
+ foasByDimension,
922
+ };
923
+ },
924
+ });
925
+ // ── Dashboard Query (Section 8) ──
926
+ /** Get 5DoBT Potential Analysis dashboard data */
927
+ export const getPotentialAnalysisDashboard = query({
928
+ args: {
929
+ workspaceId: v.string(),
930
+ analysisId: v.optional(v.id("potentialAnalyses")),
931
+ },
932
+ handler: async (ctx, args) => {
933
+ // If no specific analysis, get the most recent one
934
+ let analysis;
935
+ if (args.analysisId) {
936
+ analysis = await ctx.db.get(args.analysisId);
937
+ }
938
+ else {
939
+ const analyses = await ctx.db
940
+ .query("potentialAnalyses")
941
+ .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
942
+ .collect();
943
+ if (analyses.length === 0)
944
+ return null;
945
+ // Most recent by updatedAt
946
+ analysis = analyses.sort((a, b) => b.updatedAt - a.updatedAt)[0];
947
+ }
948
+ if (!analysis)
949
+ return null;
950
+ const analysisId = analysis._id;
951
+ // Step progress
952
+ const charter = await ctx.db
953
+ .query("paProjectCharters")
954
+ .withIndex("by_analysis", (q) => q.eq("analysisId", analysisId))
955
+ .first();
956
+ const dods = await ctx.db
957
+ .query("paDefinitionsOfDone")
958
+ .withIndex("by_analysis", (q) => q.eq("analysisId", analysisId))
959
+ .collect();
960
+ const areas = await ctx.db
961
+ .query("paInfluenceAreas")
962
+ .withIndex("by_analysis", (q) => q.eq("analysisId", analysisId))
963
+ .collect();
964
+ const pocs = await ctx.db
965
+ .query("paPointsOfChanges")
966
+ .withIndex("by_analysis", (q) => q.eq("analysisId", analysisId))
967
+ .collect();
968
+ const foas = await ctx.db
969
+ .query("paFieldsOfAction")
970
+ .withIndex("by_analysis", (q) => q.eq("analysisId", analysisId))
971
+ .collect();
972
+ const statuses = await ctx.db
973
+ .query("paPotentialStatuses")
974
+ .withIndex("by_analysis", (q) => q.eq("analysisId", analysisId))
975
+ .collect();
976
+ const summary = await ctx.db
977
+ .query("paCalculationSummaries")
978
+ .withIndex("by_analysis", (q) => q.eq("analysisId", analysisId))
979
+ .first();
980
+ // Dimension distribution
981
+ const dimensionDistribution = {};
982
+ for (const foa of foas) {
983
+ if (!dimensionDistribution[foa.dobt_dimension]) {
984
+ dimensionDistribution[foa.dobt_dimension] = { sp: 0, nsp: 0, total: 0 };
985
+ }
986
+ dimensionDistribution[foa.dobt_dimension].total++;
987
+ if (foa.influence_type === "SP")
988
+ dimensionDistribution[foa.dobt_dimension].sp++;
989
+ else
990
+ dimensionDistribution[foa.dobt_dimension].nsp++;
991
+ }
992
+ // Status lifecycle counts
993
+ const statusLifecycle = {};
994
+ for (const s of statuses) {
995
+ statusLifecycle[s.status] = (statusLifecycle[s.status] ?? 0) + 1;
996
+ }
997
+ return {
998
+ analysis: {
999
+ _id: analysis._id,
1000
+ name: analysis.name,
1001
+ currentStep: analysis.currentStep,
1002
+ status: analysis.status,
1003
+ createdAt: analysis.createdAt,
1004
+ updatedAt: analysis.updatedAt,
1005
+ },
1006
+ stepProgress: {
1007
+ 1: { complete: !!charter, label: "Project Charter" },
1008
+ 2: { complete: dods.length > 0, count: dods.length, label: "Definition of Done" },
1009
+ 3: { complete: areas.length > 0, count: areas.length, label: "Influence Areas" },
1010
+ 4: { complete: pocs.length > 0, count: pocs.length, label: "Points of Changes" },
1011
+ 5: { complete: foas.length > 0, count: foas.length, label: "Fields of Action" },
1012
+ 6: { complete: statuses.length > 0, count: statuses.length, label: "Potential Status" },
1013
+ 7: { complete: !!summary, label: "Calculation Summary" },
1014
+ },
1015
+ dimensionDistribution,
1016
+ statusLifecycle,
1017
+ totals: {
1018
+ sp: foas.filter((f) => f.influence_type === "SP").length,
1019
+ nsp: foas.filter((f) => f.influence_type === "NSP").length,
1020
+ },
1021
+ calculationSummary: summary
1022
+ ? {
1023
+ dimension_totals: summary.dimension_totals,
1024
+ grand_total_sp: summary.grand_total_sp,
1025
+ grand_total_nsp: summary.grand_total_nsp,
1026
+ grand_total: summary.grand_total,
1027
+ margin_effects: summary.margin_effects,
1028
+ total_margin_effect: summary.total_margin_effect,
1029
+ projection: summary.projection,
1030
+ }
1031
+ : null,
1032
+ };
1033
+ },
1034
+ });
1035
+ //# sourceMappingURL=potentialAnalysis.js.map