@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.
- package/README.md +24 -0
- package/dist/client/index.d.ts +287 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +215 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +23 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +15 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +28 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +23 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +18 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/assessment.d.ts +141 -0
- package/dist/component/assessment.d.ts.map +1 -0
- package/dist/component/assessment.js +498 -0
- package/dist/component/assessment.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/metrics.d.ts +145 -0
- package/dist/component/metrics.d.ts.map +1 -0
- package/dist/component/metrics.js +425 -0
- package/dist/component/metrics.js.map +1 -0
- package/dist/component/potentialAnalysis.d.ts +510 -0
- package/dist/component/potentialAnalysis.d.ts.map +1 -0
- package/dist/component/potentialAnalysis.js +1035 -0
- package/dist/component/potentialAnalysis.js.map +1 -0
- package/dist/component/schema.d.ts +342 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +194 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/scoring.d.ts +6 -0
- package/dist/component/scoring.d.ts.map +1 -0
- package/dist/component/scoring.js +394 -0
- package/dist/component/scoring.js.map +1 -0
- package/dist/data/dm/cvr.d.ts +12 -0
- package/dist/data/dm/cvr.d.ts.map +1 -0
- package/dist/data/dm/cvr.js +71 -0
- package/dist/data/dm/cvr.js.map +1 -0
- package/dist/data/dm/guidelines.d.ts +9 -0
- package/dist/data/dm/guidelines.d.ts.map +1 -0
- package/dist/data/dm/guidelines.js +22 -0
- package/dist/data/dm/guidelines.js.map +1 -0
- package/dist/data/dm/methods.d.ts +10 -0
- package/dist/data/dm/methods.d.ts.map +1 -0
- package/dist/data/dm/methods.js +50 -0
- package/dist/data/dm/methods.js.map +1 -0
- package/dist/data/dm/systems.d.ts +10 -0
- package/dist/data/dm/systems.d.ts.map +1 -0
- package/dist/data/dm/systems.js +52 -0
- package/dist/data/dm/systems.js.map +1 -0
- package/dist/data/domains.d.ts +20 -0
- package/dist/data/domains.d.ts.map +1 -0
- package/dist/data/domains.js +264 -0
- package/dist/data/domains.js.map +1 -0
- package/dist/data/ek/cvr.d.ts +12 -0
- package/dist/data/ek/cvr.d.ts.map +1 -0
- package/dist/data/ek/cvr.js +58 -0
- package/dist/data/ek/cvr.js.map +1 -0
- package/dist/data/ek/guidelines.d.ts +9 -0
- package/dist/data/ek/guidelines.d.ts.map +1 -0
- package/dist/data/ek/guidelines.js +26 -0
- package/dist/data/ek/guidelines.js.map +1 -0
- package/dist/data/ek/methods.d.ts +9 -0
- package/dist/data/ek/methods.d.ts.map +1 -0
- package/dist/data/ek/methods.js +35 -0
- package/dist/data/ek/methods.js.map +1 -0
- package/dist/data/ek/systems.d.ts +9 -0
- package/dist/data/ek/systems.d.ts.map +1 -0
- package/dist/data/ek/systems.js +23 -0
- package/dist/data/ek/systems.js.map +1 -0
- package/dist/data/influence-areas.d.ts +26 -0
- package/dist/data/influence-areas.d.ts.map +1 -0
- package/dist/data/influence-areas.js +143 -0
- package/dist/data/influence-areas.js.map +1 -0
- package/dist/data/km/cvr.d.ts +13 -0
- package/dist/data/km/cvr.d.ts.map +1 -0
- package/dist/data/km/cvr.js +65 -0
- package/dist/data/km/cvr.js.map +1 -0
- package/dist/data/km/guidelines.d.ts +9 -0
- package/dist/data/km/guidelines.d.ts.map +1 -0
- package/dist/data/km/guidelines.js +20 -0
- package/dist/data/km/guidelines.js.map +1 -0
- package/dist/data/km/methods.d.ts +9 -0
- package/dist/data/km/methods.d.ts.map +1 -0
- package/dist/data/km/methods.js +28 -0
- package/dist/data/km/methods.js.map +1 -0
- package/dist/data/km/systems.d.ts +9 -0
- package/dist/data/km/systems.d.ts.map +1 -0
- package/dist/data/km/systems.js +18 -0
- package/dist/data/km/systems.js.map +1 -0
- package/dist/data/stammdaten.d.ts +7 -0
- package/dist/data/stammdaten.d.ts.map +1 -0
- package/dist/data/stammdaten.js +94 -0
- package/dist/data/stammdaten.js.map +1 -0
- package/dist/data/types.d.ts +113 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +23 -0
- package/dist/data/types.js.map +1 -0
- package/package.json +38 -0
- package/src/client/index.ts +442 -0
- package/src/component/_generated/api.ts +35 -0
- package/src/component/_generated/component.ts +50 -0
- package/src/component/_generated/dataModel.ts +42 -0
- package/src/component/_generated/server.ts +48 -0
- package/src/component/assessment.ts +536 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/metrics.ts +479 -0
- package/src/component/potentialAnalysis.ts +1118 -0
- package/src/component/schema.ts +209 -0
- package/src/component/scoring.ts +485 -0
- package/src/data/dm/cvr.ts +75 -0
- package/src/data/dm/guidelines.ts +24 -0
- package/src/data/dm/methods.ts +53 -0
- package/src/data/dm/systems.ts +55 -0
- package/src/data/domains.ts +275 -0
- package/src/data/ek/cvr.ts +62 -0
- package/src/data/ek/guidelines.ts +28 -0
- package/src/data/ek/methods.ts +39 -0
- package/src/data/ek/systems.ts +25 -0
- package/src/data/influence-areas.ts +162 -0
- package/src/data/km/cvr.ts +69 -0
- package/src/data/km/guidelines.ts +22 -0
- package/src/data/km/methods.ts +30 -0
- package/src/data/km/systems.ts +20 -0
- package/src/data/stammdaten.ts +96 -0
- package/src/data/types.ts +183 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
export default defineSchema({
|
|
5
|
+
/** Static question catalog — seeded at init from static data */
|
|
6
|
+
questions: defineTable({
|
|
7
|
+
questionKey: v.string(),
|
|
8
|
+
domain: v.string(),
|
|
9
|
+
dimension: v.string(),
|
|
10
|
+
category: v.string(),
|
|
11
|
+
itemKey: v.string(),
|
|
12
|
+
label: v.string(),
|
|
13
|
+
labelEn: v.optional(v.string()),
|
|
14
|
+
answerSchema: v.string(),
|
|
15
|
+
options: v.optional(v.any()),
|
|
16
|
+
pmNpm: v.optional(v.string()),
|
|
17
|
+
sortOrder: v.number(),
|
|
18
|
+
})
|
|
19
|
+
.index("by_domain", ["domain"])
|
|
20
|
+
.index("by_domain_dimension", ["domain", "dimension"])
|
|
21
|
+
.index("by_questionKey", ["questionKey"]),
|
|
22
|
+
|
|
23
|
+
/** Per-workspace, per-question answers */
|
|
24
|
+
answers: defineTable({
|
|
25
|
+
workspaceId: v.string(),
|
|
26
|
+
questionKey: v.string(),
|
|
27
|
+
value: v.any(),
|
|
28
|
+
source: v.string(),
|
|
29
|
+
confidence: v.optional(v.number()),
|
|
30
|
+
extractedFrom: v.optional(v.string()),
|
|
31
|
+
answeredBy: v.optional(v.string()),
|
|
32
|
+
verifiedBy: v.optional(v.string()),
|
|
33
|
+
verifiedAt: v.optional(v.number()),
|
|
34
|
+
createdAt: v.number(),
|
|
35
|
+
updatedAt: v.number(),
|
|
36
|
+
})
|
|
37
|
+
.index("by_workspace", ["workspaceId"])
|
|
38
|
+
.index("by_workspace_question", ["workspaceId", "questionKey"]),
|
|
39
|
+
|
|
40
|
+
/** Current computed scores per workspace per domain */
|
|
41
|
+
metrics: defineTable({
|
|
42
|
+
workspaceId: v.string(),
|
|
43
|
+
domain: v.string(),
|
|
44
|
+
data: v.any(),
|
|
45
|
+
computedAt: v.number(),
|
|
46
|
+
})
|
|
47
|
+
.index("by_workspace", ["workspaceId"])
|
|
48
|
+
.index("by_workspace_domain", ["workspaceId", "domain"]),
|
|
49
|
+
|
|
50
|
+
/** Addon configuration — single record per key */
|
|
51
|
+
config: defineTable({
|
|
52
|
+
key: v.string(),
|
|
53
|
+
enabled: v.boolean(),
|
|
54
|
+
mode: v.string(),
|
|
55
|
+
financialInputs: v.optional(v.any()),
|
|
56
|
+
sensitivity: v.optional(v.any()),
|
|
57
|
+
lifetime: v.optional(v.any()),
|
|
58
|
+
workspaceTypes: v.optional(v.any()),
|
|
59
|
+
snapshotPolicy: v.optional(v.string()),
|
|
60
|
+
snapshotThreshold: v.optional(v.number()),
|
|
61
|
+
updatedAt: v.number(),
|
|
62
|
+
})
|
|
63
|
+
.index("by_key", ["key"]),
|
|
64
|
+
|
|
65
|
+
/** Historical snapshots — immutable after creation */
|
|
66
|
+
snapshots: defineTable({
|
|
67
|
+
workspaceId: v.string(),
|
|
68
|
+
domain: v.string(),
|
|
69
|
+
dimensionScores: v.any(),
|
|
70
|
+
overallMaturity: v.any(),
|
|
71
|
+
monetaryPotential: v.optional(v.any()),
|
|
72
|
+
financialInputs: v.optional(v.any()),
|
|
73
|
+
snapshotAt: v.number(),
|
|
74
|
+
snapshotType: v.string(),
|
|
75
|
+
})
|
|
76
|
+
.index("by_workspace", ["workspaceId"])
|
|
77
|
+
.index("by_workspace_domain", ["workspaceId", "domain"]),
|
|
78
|
+
|
|
79
|
+
// ── 5DoBT Potential Analysis Tables ──
|
|
80
|
+
|
|
81
|
+
/** Top-level analysis record */
|
|
82
|
+
potentialAnalyses: defineTable({
|
|
83
|
+
workspaceId: v.string(),
|
|
84
|
+
name: v.optional(v.string()),
|
|
85
|
+
currentStep: v.number(),
|
|
86
|
+
status: v.string(),
|
|
87
|
+
createdBy: v.string(),
|
|
88
|
+
createdAt: v.number(),
|
|
89
|
+
updatedAt: v.number(),
|
|
90
|
+
})
|
|
91
|
+
.index("by_workspace", ["workspaceId"]),
|
|
92
|
+
|
|
93
|
+
/** Step I: Project Charter */
|
|
94
|
+
paProjectCharters: defineTable({
|
|
95
|
+
analysisId: v.id("potentialAnalyses"),
|
|
96
|
+
name: v.string(),
|
|
97
|
+
date: v.string(),
|
|
98
|
+
business_case: v.string(),
|
|
99
|
+
problem_statement: v.string(),
|
|
100
|
+
goal: v.string(),
|
|
101
|
+
scope_in: v.string(),
|
|
102
|
+
scope_out: v.string(),
|
|
103
|
+
objectives: v.any(),
|
|
104
|
+
conditions: v.optional(v.string()),
|
|
105
|
+
risks: v.optional(v.any()),
|
|
106
|
+
costs: v.optional(v.string()),
|
|
107
|
+
milestones: v.optional(v.any()),
|
|
108
|
+
project_team: v.optional(v.any()),
|
|
109
|
+
})
|
|
110
|
+
.index("by_analysis", ["analysisId"]),
|
|
111
|
+
|
|
112
|
+
/** Step II: Definition of Done items */
|
|
113
|
+
paDefinitionsOfDone: defineTable({
|
|
114
|
+
analysisId: v.id("potentialAnalyses"),
|
|
115
|
+
number: v.number(),
|
|
116
|
+
definition: v.string(),
|
|
117
|
+
description: v.string(),
|
|
118
|
+
effect_type: v.string(),
|
|
119
|
+
})
|
|
120
|
+
.index("by_analysis", ["analysisId"]),
|
|
121
|
+
|
|
122
|
+
/** Step III: Influence Area selections */
|
|
123
|
+
paInfluenceAreas: defineTable({
|
|
124
|
+
analysisId: v.id("potentialAnalyses"),
|
|
125
|
+
dobt_dimension: v.string(),
|
|
126
|
+
sub_area_key: v.string(),
|
|
127
|
+
potential_description: v.string(),
|
|
128
|
+
})
|
|
129
|
+
.index("by_analysis", ["analysisId"])
|
|
130
|
+
.index("by_analysis_dimension", ["analysisId", "dobt_dimension"]),
|
|
131
|
+
|
|
132
|
+
/** Step IV: Points of Changes */
|
|
133
|
+
paPointsOfChanges: defineTable({
|
|
134
|
+
analysisId: v.id("potentialAnalyses"),
|
|
135
|
+
influence_area_ref: v.id("paInfluenceAreas"),
|
|
136
|
+
poc_description: v.string(),
|
|
137
|
+
definition_of_done_refs: v.any(),
|
|
138
|
+
})
|
|
139
|
+
.index("by_analysis", ["analysisId"]),
|
|
140
|
+
|
|
141
|
+
/** Step V: Fields of Action */
|
|
142
|
+
paFieldsOfAction: defineTable({
|
|
143
|
+
analysisId: v.id("potentialAnalyses"),
|
|
144
|
+
poc_ref: v.id("paPointsOfChanges"),
|
|
145
|
+
potential_description: v.string(),
|
|
146
|
+
dobt_dimension: v.string(),
|
|
147
|
+
influence_type: v.string(),
|
|
148
|
+
foa_description: v.string(),
|
|
149
|
+
kpi_influence: v.string(),
|
|
150
|
+
})
|
|
151
|
+
.index("by_analysis", ["analysisId"])
|
|
152
|
+
.index("by_analysis_dimension", ["analysisId", "dobt_dimension"]),
|
|
153
|
+
|
|
154
|
+
/** Step VI: Potential Status per FOA */
|
|
155
|
+
paPotentialStatuses: defineTable({
|
|
156
|
+
analysisId: v.id("potentialAnalyses"),
|
|
157
|
+
foa_ref: v.id("paFieldsOfAction"),
|
|
158
|
+
status: v.string(),
|
|
159
|
+
validation_level: v.optional(v.string()),
|
|
160
|
+
estimation_value: v.optional(v.number()),
|
|
161
|
+
estimation_risk: v.optional(v.number()),
|
|
162
|
+
estimation_explanation: v.optional(v.string()),
|
|
163
|
+
estimation_result: v.optional(v.number()),
|
|
164
|
+
calculation_ref: v.optional(v.id("paCalculations")),
|
|
165
|
+
calculation_result: v.optional(v.number()),
|
|
166
|
+
committed_value_result: v.optional(v.number()),
|
|
167
|
+
committed_responsible_business_owner: v.optional(v.string()),
|
|
168
|
+
measured_potential_value: v.optional(v.number()),
|
|
169
|
+
})
|
|
170
|
+
.index("by_analysis", ["analysisId"])
|
|
171
|
+
.index("by_foa", ["foa_ref"]),
|
|
172
|
+
|
|
173
|
+
/** Step VII: Facts & Figures */
|
|
174
|
+
paFactsFigures: defineTable({
|
|
175
|
+
analysisId: v.id("potentialAnalyses"),
|
|
176
|
+
name: v.string(),
|
|
177
|
+
unit: v.string(),
|
|
178
|
+
value: v.number(),
|
|
179
|
+
})
|
|
180
|
+
.index("by_analysis", ["analysisId"]),
|
|
181
|
+
|
|
182
|
+
/** Step VII: Calculations per dimension */
|
|
183
|
+
paCalculations: defineTable({
|
|
184
|
+
analysisId: v.id("potentialAnalyses"),
|
|
185
|
+
dobt_dimension: v.string(),
|
|
186
|
+
potential_description: v.string(),
|
|
187
|
+
assumptions: v.any(),
|
|
188
|
+
formula: v.string(),
|
|
189
|
+
result: v.number(),
|
|
190
|
+
})
|
|
191
|
+
.index("by_analysis", ["analysisId"]),
|
|
192
|
+
|
|
193
|
+
/** Step VII: Computed Calculation Summary (cached) */
|
|
194
|
+
paCalculationSummaries: defineTable({
|
|
195
|
+
analysisId: v.id("potentialAnalyses"),
|
|
196
|
+
dimension_totals: v.any(),
|
|
197
|
+
grand_total_sp: v.number(),
|
|
198
|
+
grand_total_nsp: v.number(),
|
|
199
|
+
grand_total: v.number(),
|
|
200
|
+
sensitivity: v.any(),
|
|
201
|
+
margin_effects: v.any(),
|
|
202
|
+
total_margin_effect: v.number(),
|
|
203
|
+
lifetime: v.any(),
|
|
204
|
+
projection: v.any(),
|
|
205
|
+
validated_potentials: v.any(),
|
|
206
|
+
calculatedAt: v.number(),
|
|
207
|
+
})
|
|
208
|
+
.index("by_analysis", ["analysisId"]),
|
|
209
|
+
});
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { internalMutation } from "./_generated/server.js";
|
|
2
|
+
import { internal } from "./_generated/api.js";
|
|
3
|
+
import { v } from "convex/values";
|
|
4
|
+
import { DOMAINS, getDomain, getMaturityLevel } from "../data/domains.js";
|
|
5
|
+
import { KM_METHODS } from "../data/km/methods.js";
|
|
6
|
+
import { KM_SYSTEMS } from "../data/km/systems.js";
|
|
7
|
+
import { KM_GUIDELINES } from "../data/km/guidelines.js";
|
|
8
|
+
import { KM_CVR_WEIGHTS, KM_CVR_BUILDUP, KM_IMPLEMENTATION_FACTORS } from "../data/km/cvr.js";
|
|
9
|
+
import { EK_METHODS } from "../data/ek/methods.js";
|
|
10
|
+
import { EK_SYSTEMS } from "../data/ek/systems.js";
|
|
11
|
+
import { EK_GUIDELINES } from "../data/ek/guidelines.js";
|
|
12
|
+
import { EK_CVR_WEIGHTS, EK_CVR_BUILDUP, EK_IMPLEMENTATION_FACTORS } from "../data/ek/cvr.js";
|
|
13
|
+
import { DM_METHODS } from "../data/dm/methods.js";
|
|
14
|
+
import { DM_SYSTEMS } from "../data/dm/systems.js";
|
|
15
|
+
import { DM_GUIDELINES } from "../data/dm/guidelines.js";
|
|
16
|
+
import { DM_CVR_WEIGHTS, DM_CVR_BUILDUP, DM_IMPLEMENTATION_FACTORS } from "../data/dm/cvr.js";
|
|
17
|
+
import {
|
|
18
|
+
VALUE_CATEGORIES,
|
|
19
|
+
DEFAULT_SENSITIVITY,
|
|
20
|
+
DEFAULT_LIFETIME,
|
|
21
|
+
} from "../data/types.js";
|
|
22
|
+
import type {
|
|
23
|
+
ItemDef,
|
|
24
|
+
DomainKey,
|
|
25
|
+
CvrWeightTable,
|
|
26
|
+
CvrBuildupTable,
|
|
27
|
+
ImplementationFactorTable,
|
|
28
|
+
ValueCategory,
|
|
29
|
+
FinancialInputs,
|
|
30
|
+
SensitivityConfig,
|
|
31
|
+
LifetimeConfig,
|
|
32
|
+
Sub5Value,
|
|
33
|
+
CvrWeightRow,
|
|
34
|
+
} from "../data/types.js";
|
|
35
|
+
|
|
36
|
+
// ── Static Data Lookup ──
|
|
37
|
+
|
|
38
|
+
function getItemsForDomain(domain: DomainKey): {
|
|
39
|
+
methods: ItemDef[];
|
|
40
|
+
systems: ItemDef[];
|
|
41
|
+
guidelines: ItemDef[];
|
|
42
|
+
} {
|
|
43
|
+
switch (domain) {
|
|
44
|
+
case "KM":
|
|
45
|
+
return { methods: KM_METHODS, systems: KM_SYSTEMS, guidelines: KM_GUIDELINES };
|
|
46
|
+
case "EK":
|
|
47
|
+
return { methods: EK_METHODS, systems: EK_SYSTEMS, guidelines: EK_GUIDELINES };
|
|
48
|
+
case "DM":
|
|
49
|
+
return { methods: DM_METHODS, systems: DM_SYSTEMS, guidelines: DM_GUIDELINES };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getCvrData(domain: DomainKey): {
|
|
54
|
+
weights: CvrWeightTable;
|
|
55
|
+
buildup: CvrBuildupTable;
|
|
56
|
+
factors: ImplementationFactorTable;
|
|
57
|
+
} {
|
|
58
|
+
switch (domain) {
|
|
59
|
+
case "KM":
|
|
60
|
+
return { weights: KM_CVR_WEIGHTS, buildup: KM_CVR_BUILDUP, factors: KM_IMPLEMENTATION_FACTORS };
|
|
61
|
+
case "EK":
|
|
62
|
+
return { weights: EK_CVR_WEIGHTS, buildup: EK_CVR_BUILDUP, factors: EK_IMPLEMENTATION_FACTORS };
|
|
63
|
+
case "DM":
|
|
64
|
+
return { weights: DM_CVR_WEIGHTS, buildup: DM_CVR_BUILDUP, factors: DM_IMPLEMENTATION_FACTORS };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Scoring Helpers ──
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Derive an anchoring level (0-3) from sub5 answer.
|
|
72
|
+
* 0 = not known/used, 1 = basic, 2 = intermediate, 3 = fully anchored
|
|
73
|
+
*/
|
|
74
|
+
function sub5ToLevel(val: Sub5Value | null): number {
|
|
75
|
+
if (!val) return 0;
|
|
76
|
+
if (!val.bekannt) return 0;
|
|
77
|
+
if (!val.in_anwendung) return 0;
|
|
78
|
+
let level = 1; // known + in use
|
|
79
|
+
if (val.definiert) level = 2;
|
|
80
|
+
if (val.verankert && val.verankert !== null) {
|
|
81
|
+
// System-anchored is highest
|
|
82
|
+
if (val.verankert === "system_anchored") level = 3;
|
|
83
|
+
else if (val.verankert === "process_anchored") level = Math.max(level, 2);
|
|
84
|
+
}
|
|
85
|
+
if (val.schulungen && level >= 2) level = 3;
|
|
86
|
+
return level;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compute item score using the level-based formula.
|
|
91
|
+
* Returns { score: 0-4, maxPotential, methodShare }
|
|
92
|
+
*/
|
|
93
|
+
function computeItemScore(
|
|
94
|
+
item: ItemDef,
|
|
95
|
+
level: number,
|
|
96
|
+
complexity: number,
|
|
97
|
+
): { score: number; maxPotential: number } {
|
|
98
|
+
const [w1, w2, w3] = item.weights;
|
|
99
|
+
const complexityIdx = complexity - 1; // 0-3
|
|
100
|
+
const complexityInfluence = item.complexityRelevance[complexityIdx] ?? 1;
|
|
101
|
+
const maxPotential = (w1 + w2 + w3) * complexityInfluence;
|
|
102
|
+
|
|
103
|
+
if (maxPotential === 0) {
|
|
104
|
+
return { score: 4, maxPotential: 0 }; // not relevant for this complexity
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let score: number;
|
|
108
|
+
if (level === 0) {
|
|
109
|
+
score = 0;
|
|
110
|
+
} else if (level === 1) {
|
|
111
|
+
score = (w1 * complexityInfluence) / maxPotential * 4;
|
|
112
|
+
} else if (level === 2) {
|
|
113
|
+
score = ((w1 + w2) * complexityInfluence) / maxPotential * 4;
|
|
114
|
+
} else {
|
|
115
|
+
score = 4; // level 3 = full potential
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { score, maxPotential };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Aggregate item scores using SUMPRODUCT: SUM(score * share)
|
|
123
|
+
* where share = maxPotential / SUM(allMaxPotentials)
|
|
124
|
+
*/
|
|
125
|
+
function aggregateSumproduct(
|
|
126
|
+
items: Array<{ score: number; maxPotential: number }>,
|
|
127
|
+
): number {
|
|
128
|
+
const totalPotential = items.reduce((sum, i) => sum + i.maxPotential, 0);
|
|
129
|
+
if (totalPotential === 0) return 0;
|
|
130
|
+
return items.reduce(
|
|
131
|
+
(sum, i) => sum + i.score * (i.maxPotential / totalPotential),
|
|
132
|
+
0,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Aggregate item scores using AVERAGE
|
|
138
|
+
*/
|
|
139
|
+
function aggregateAverage(scores: number[]): number {
|
|
140
|
+
if (scores.length === 0) return 0;
|
|
141
|
+
return scores.reduce((sum, s) => sum + s, 0) / scores.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Main Recompute ──
|
|
145
|
+
|
|
146
|
+
/** Full recompute: answers → item scores → dimension scores → overall → CVR → metrics table */
|
|
147
|
+
export const recomputeMetrics = internalMutation({
|
|
148
|
+
args: {
|
|
149
|
+
workspaceId: v.string(),
|
|
150
|
+
domain: v.string(),
|
|
151
|
+
},
|
|
152
|
+
handler: async (ctx, args) => {
|
|
153
|
+
const domain = args.domain as DomainKey;
|
|
154
|
+
const domainDef = getDomain(domain);
|
|
155
|
+
if (!domainDef) return;
|
|
156
|
+
|
|
157
|
+
// Load config for financial inputs and complexity
|
|
158
|
+
const config = await ctx.db
|
|
159
|
+
.query("config")
|
|
160
|
+
.withIndex("by_key", (q) => q.eq("key", "dobt-addon"))
|
|
161
|
+
.first();
|
|
162
|
+
|
|
163
|
+
const financialInputs = (config?.financialInputs as FinancialInputs) ?? {
|
|
164
|
+
revenue: 0,
|
|
165
|
+
cost_volume: 0,
|
|
166
|
+
manufacturing_costs: 0,
|
|
167
|
+
complexity: 3 as const,
|
|
168
|
+
};
|
|
169
|
+
const sensitivity = (config?.sensitivity as SensitivityConfig) ?? DEFAULT_SENSITIVITY;
|
|
170
|
+
const lifetime = (config?.lifetime as LifetimeConfig) ?? DEFAULT_LIFETIME;
|
|
171
|
+
const complexity = financialInputs.complexity || 3;
|
|
172
|
+
|
|
173
|
+
// Load answers for this workspace
|
|
174
|
+
const answers = await ctx.db
|
|
175
|
+
.query("answers")
|
|
176
|
+
.withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId))
|
|
177
|
+
.collect();
|
|
178
|
+
const answerMap = new Map(answers.map((a) => [a.questionKey, a]));
|
|
179
|
+
|
|
180
|
+
// Get items
|
|
181
|
+
const { methods, systems, guidelines } = getItemsForDomain(domain);
|
|
182
|
+
|
|
183
|
+
// Compute dimension scores
|
|
184
|
+
const dimensionScores: Record<string, { status: number; target: number; gap: number }> = {};
|
|
185
|
+
|
|
186
|
+
for (const dim of domainDef.dimensions) {
|
|
187
|
+
let score: number;
|
|
188
|
+
|
|
189
|
+
if (dim.scoringSource === "expert") {
|
|
190
|
+
// Direct expert score
|
|
191
|
+
const qKey = `${domain.toLowerCase()}_expert_${dim.key}`;
|
|
192
|
+
const answer = answerMap.get(qKey);
|
|
193
|
+
score = answer ? Number(answer.value) || 0 : 0;
|
|
194
|
+
|
|
195
|
+
// Handle PM/NPM split for EK expert dimensions
|
|
196
|
+
if (dim.pmNpmSplit) {
|
|
197
|
+
const pmKey = `${domain.toLowerCase()}_expert_${dim.key}_pm`;
|
|
198
|
+
const npmKey = `${domain.toLowerCase()}_expert_${dim.key}_npm`;
|
|
199
|
+
const pmAnswer = answerMap.get(pmKey);
|
|
200
|
+
const npmAnswer = answerMap.get(npmKey);
|
|
201
|
+
if (pmAnswer || npmAnswer) {
|
|
202
|
+
const pmScore = pmAnswer ? Number(pmAnswer.value) || 0 : 0;
|
|
203
|
+
const npmScore = npmAnswer ? Number(npmAnswer.value) || 0 : 0;
|
|
204
|
+
score = (pmScore + npmScore) / 2;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
// Methods/systems/guidelines scoring
|
|
209
|
+
let items: ItemDef[];
|
|
210
|
+
if (dim.scoringSource === "methods") {
|
|
211
|
+
items = methods.filter((m) => m.dimension === dim.key);
|
|
212
|
+
} else if (dim.scoringSource === "systems") {
|
|
213
|
+
items = systems.filter((s) => s.dimension === dim.key);
|
|
214
|
+
// Include methods assigned to this dimension (e.g., DM technologies in dm_systeme)
|
|
215
|
+
items.push(...methods.filter((m) => m.dimension === dim.key));
|
|
216
|
+
} else {
|
|
217
|
+
items = guidelines.filter((g) => g.dimension === dim.key);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (dim.pmNpmSplit && domain === "EK") {
|
|
221
|
+
// EK PM/NPM split: compute separately then average
|
|
222
|
+
// "both" items participate in both PM and NPM groups
|
|
223
|
+
const pmItems = items.filter((i) => i.pmNpm === "pm" || i.pmNpm === "both");
|
|
224
|
+
const npmItems = items.filter((i) => i.pmNpm === "npm" || i.pmNpm === "both");
|
|
225
|
+
|
|
226
|
+
const computeGroupScore = (groupItems: ItemDef[]): number => {
|
|
227
|
+
const scored = groupItems.map((item) => {
|
|
228
|
+
const qKey = `${domain.toLowerCase()}_${item.category}_${item.key}`;
|
|
229
|
+
const answer = answerMap.get(qKey);
|
|
230
|
+
const level = answer ? sub5ToLevel(answer.value as Sub5Value) : 0;
|
|
231
|
+
return computeItemScore(item, level, complexity);
|
|
232
|
+
});
|
|
233
|
+
return aggregateSumproduct(scored);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const pmScore = pmItems.length > 0 ? computeGroupScore(pmItems) : 0;
|
|
237
|
+
const npmScore = npmItems.length > 0 ? computeGroupScore(npmItems) : 0;
|
|
238
|
+
score = (pmScore + npmScore) / 2;
|
|
239
|
+
} else {
|
|
240
|
+
// Standard scoring
|
|
241
|
+
const scored = items.map((item) => {
|
|
242
|
+
const qKey = `${domain.toLowerCase()}_${item.category}_${item.key}`;
|
|
243
|
+
const answer = answerMap.get(qKey);
|
|
244
|
+
const level = answer ? sub5ToLevel(answer.value as Sub5Value) : 0;
|
|
245
|
+
return computeItemScore(item, level, complexity);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (dim.aggregationMethod === "average") {
|
|
249
|
+
score = aggregateAverage(scored.map((s) => s.score));
|
|
250
|
+
} else {
|
|
251
|
+
score = aggregateSumproduct(scored);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
dimensionScores[dim.key] = {
|
|
257
|
+
status: Math.round(score * 100) / 100,
|
|
258
|
+
target: 4, // target is always World Class
|
|
259
|
+
gap: Math.round((4 - score) * 100) / 100,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Overall maturity
|
|
264
|
+
const dimSum = Object.values(dimensionScores).reduce((sum, d) => sum + d.status, 0);
|
|
265
|
+
const overallScore = (dimSum / domainDef.maxScore) * 4;
|
|
266
|
+
const overallMaturity = {
|
|
267
|
+
overall_score: Math.round(overallScore * 100) / 100,
|
|
268
|
+
maturity_level: getMaturityLevel(overallScore),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// CVR monetization
|
|
272
|
+
const { weights: cvrWeights, buildup: cvrBuildup, factors: implFactors } = getCvrData(domain);
|
|
273
|
+
const monetaryBasis = computeMonetaryBasis(domain, financialInputs);
|
|
274
|
+
const monetaryPotential = computeCvrMonetization(
|
|
275
|
+
domain,
|
|
276
|
+
dimensionScores,
|
|
277
|
+
cvrWeights,
|
|
278
|
+
cvrBuildup,
|
|
279
|
+
implFactors,
|
|
280
|
+
monetaryBasis,
|
|
281
|
+
sensitivity,
|
|
282
|
+
lifetime,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Progress stats
|
|
286
|
+
const domainPrefix = domain.toLowerCase() + "_";
|
|
287
|
+
const domainAnswers = answers.filter((a) => a.questionKey.startsWith(domainPrefix));
|
|
288
|
+
const allQuestions = await ctx.db
|
|
289
|
+
.query("questions")
|
|
290
|
+
.withIndex("by_domain", (q) => q.eq("domain", domain))
|
|
291
|
+
.collect();
|
|
292
|
+
const progress = {
|
|
293
|
+
total: allQuestions.length,
|
|
294
|
+
answered: domainAnswers.length,
|
|
295
|
+
progressPct: allQuestions.length > 0 ? Math.round((domainAnswers.length / allQuestions.length) * 100) : 0,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Store metrics
|
|
299
|
+
const data = {
|
|
300
|
+
dimensionScores,
|
|
301
|
+
overallMaturity,
|
|
302
|
+
monetaryPotential,
|
|
303
|
+
progress,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const existing = await ctx.db
|
|
307
|
+
.query("metrics")
|
|
308
|
+
.withIndex("by_workspace_domain", (q) =>
|
|
309
|
+
q.eq("workspaceId", args.workspaceId).eq("domain", domain),
|
|
310
|
+
)
|
|
311
|
+
.first();
|
|
312
|
+
|
|
313
|
+
if (existing) {
|
|
314
|
+
await ctx.db.patch(existing._id, { data, computedAt: Date.now() });
|
|
315
|
+
} else {
|
|
316
|
+
await ctx.db.insert("metrics", {
|
|
317
|
+
workspaceId: args.workspaceId,
|
|
318
|
+
domain,
|
|
319
|
+
data,
|
|
320
|
+
computedAt: Date.now(),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Snapshot policy check
|
|
325
|
+
const snapshotPolicy = (config?.snapshotPolicy as string) ?? "auto";
|
|
326
|
+
let shouldSnapshot = false;
|
|
327
|
+
|
|
328
|
+
if (snapshotPolicy === "auto") {
|
|
329
|
+
shouldSnapshot = true;
|
|
330
|
+
} else if (snapshotPolicy === "threshold" && existing) {
|
|
331
|
+
// Only snapshot when score changes by more than threshold %
|
|
332
|
+
const threshold = (config?.snapshotThreshold as number) ?? 5;
|
|
333
|
+
const oldData = existing.data as Record<string, unknown>;
|
|
334
|
+
const oldMaturity = oldData.overallMaturity as { overall_score: number } | undefined;
|
|
335
|
+
const oldScore = oldMaturity?.overall_score ?? 0;
|
|
336
|
+
const newScore = overallMaturity.overall_score;
|
|
337
|
+
const changePct = oldScore > 0 ? Math.abs(newScore - oldScore) / oldScore * 100 : 100;
|
|
338
|
+
shouldSnapshot = changePct >= threshold;
|
|
339
|
+
}
|
|
340
|
+
// "manual" and "periodic" don't auto-snapshot from recompute
|
|
341
|
+
|
|
342
|
+
if (shouldSnapshot) {
|
|
343
|
+
await ctx.scheduler.runAfter(0, internal.metrics.createSnapshot, {
|
|
344
|
+
workspaceId: args.workspaceId,
|
|
345
|
+
domain,
|
|
346
|
+
snapshotType: snapshotPolicy === "auto" ? "auto" : "threshold",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ── CVR Helpers ──
|
|
353
|
+
|
|
354
|
+
function computeMonetaryBasis(
|
|
355
|
+
domain: DomainKey,
|
|
356
|
+
fi: FinancialInputs,
|
|
357
|
+
): Record<ValueCategory, number> {
|
|
358
|
+
if (domain === "EK") {
|
|
359
|
+
// EK uses material-based factors
|
|
360
|
+
const directMat = fi.direct_material ?? 0;
|
|
361
|
+
const indirectMat = fi.indirect_material ?? 0;
|
|
362
|
+
return {
|
|
363
|
+
overhead: (fi.headcount_ek ?? 0) * 80000,
|
|
364
|
+
direct_cost: directMat * 0.01,
|
|
365
|
+
additional_business: (directMat + indirectMat) * 0.02,
|
|
366
|
+
innovation: (directMat + indirectMat) * 0.02,
|
|
367
|
+
risk: (directMat + indirectMat) * 0.0025,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// KM and DM use standard factors
|
|
371
|
+
const headcount = domain === "KM" ? (fi.headcount_km ?? 0) : 0;
|
|
372
|
+
return {
|
|
373
|
+
overhead: headcount * 80000,
|
|
374
|
+
direct_cost: fi.manufacturing_costs * 0.01,
|
|
375
|
+
additional_business: fi.revenue * 0.02,
|
|
376
|
+
innovation: fi.revenue * 0.02,
|
|
377
|
+
risk: fi.cost_volume * 0.0025,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function computeCvrMonetization(
|
|
382
|
+
domain: DomainKey,
|
|
383
|
+
dimensionScores: Record<string, { status: number; target: number; gap: number }>,
|
|
384
|
+
cvrWeights: CvrWeightTable,
|
|
385
|
+
cvrBuildup: CvrBuildupTable,
|
|
386
|
+
implFactors: ImplementationFactorTable,
|
|
387
|
+
monetaryBasis: Record<ValueCategory, number>,
|
|
388
|
+
sensitivity: SensitivityConfig,
|
|
389
|
+
lifetime: LifetimeConfig,
|
|
390
|
+
): Record<string, unknown> {
|
|
391
|
+
const result: Record<string, unknown> = {};
|
|
392
|
+
const totals = {
|
|
393
|
+
total_utilized_monetary: 0,
|
|
394
|
+
total_unused_monetary: 0,
|
|
395
|
+
total_secured_margin: 0,
|
|
396
|
+
total_margin_effect: 0,
|
|
397
|
+
lifetime: { year_1: 0, year_2: 0, year_3: 0, total_3yr: 0 },
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// For EK, compute CVR separately with PM and NPM factors, then average.
|
|
401
|
+
// For KM/DM, use the single domain factor key.
|
|
402
|
+
const factorKeys = domain === "EK" ? ["EK_PM", "EK_NPM"] : [domain];
|
|
403
|
+
|
|
404
|
+
for (const vc of VALUE_CATEGORIES) {
|
|
405
|
+
// Sum CVR across all dimensions for this value category
|
|
406
|
+
let totalCvrMax = 0;
|
|
407
|
+
let utilized = 0;
|
|
408
|
+
let improvementDelta = 0;
|
|
409
|
+
|
|
410
|
+
for (const [dimKey, dimData] of Object.entries(dimensionScores)) {
|
|
411
|
+
const cvrMax = cvrWeights[dimKey]?.[vc] ?? 0;
|
|
412
|
+
totalCvrMax += cvrMax;
|
|
413
|
+
|
|
414
|
+
// Utilization: score * cvr_max
|
|
415
|
+
utilized += dimData.status * cvrMax;
|
|
416
|
+
|
|
417
|
+
// Improvement: (target - status) * cvr_max
|
|
418
|
+
improvementDelta += dimData.gap * cvrMax;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const maxUtilized = totalCvrMax * 4;
|
|
422
|
+
const utilizationRatio = maxUtilized > 0 ? utilized / maxUtilized : 0;
|
|
423
|
+
const basis = monetaryBasis[vc];
|
|
424
|
+
const utilizedMonetary = utilizationRatio * basis;
|
|
425
|
+
const unusedMonetary = (1 - utilizationRatio) * basis;
|
|
426
|
+
|
|
427
|
+
const improvementRate = maxUtilized > 0 ? improvementDelta / maxUtilized : 0;
|
|
428
|
+
const monetaryImprovement = improvementRate * basis;
|
|
429
|
+
|
|
430
|
+
// Average implementation factor across PM/NPM (or single factor for KM/DM)
|
|
431
|
+
const implFactor = factorKeys.reduce(
|
|
432
|
+
(sum, fk) => sum + (implFactors[fk]?.[vc] ?? 0),
|
|
433
|
+
0,
|
|
434
|
+
) / factorKeys.length;
|
|
435
|
+
const securedMargin = implFactor * monetaryImprovement;
|
|
436
|
+
|
|
437
|
+
// Sensitivity analytics
|
|
438
|
+
const marginConversion = getMarginConversionFactor(vc, sensitivity);
|
|
439
|
+
const marginEffect = securedMargin * marginConversion;
|
|
440
|
+
|
|
441
|
+
// Lifetime projection
|
|
442
|
+
const ltYear1 = marginEffect * lifetime.year_1;
|
|
443
|
+
const ltYear2 = marginEffect * lifetime.year_2;
|
|
444
|
+
const ltYear3 = marginEffect * lifetime.year_3;
|
|
445
|
+
|
|
446
|
+
result[vc] = {
|
|
447
|
+
monetary_basis: Math.round(basis),
|
|
448
|
+
utilization_ratio: Math.round(utilizationRatio * 10000) / 10000,
|
|
449
|
+
utilized_monetary: Math.round(utilizedMonetary),
|
|
450
|
+
unused_monetary: Math.round(unusedMonetary),
|
|
451
|
+
improvement_rate: Math.round(improvementRate * 10000) / 10000,
|
|
452
|
+
monetary_improvement: Math.round(monetaryImprovement),
|
|
453
|
+
secured_margin: Math.round(securedMargin),
|
|
454
|
+
margin_effect: Math.round(marginEffect),
|
|
455
|
+
lifetime: {
|
|
456
|
+
year_1: Math.round(ltYear1),
|
|
457
|
+
year_2: Math.round(ltYear2),
|
|
458
|
+
year_3: Math.round(ltYear3),
|
|
459
|
+
total_3yr: Math.round(ltYear1 + ltYear2 + ltYear3),
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
totals.total_utilized_monetary += Math.round(utilizedMonetary);
|
|
464
|
+
totals.total_unused_monetary += Math.round(unusedMonetary);
|
|
465
|
+
totals.total_secured_margin += Math.round(securedMargin);
|
|
466
|
+
totals.total_margin_effect += Math.round(marginEffect);
|
|
467
|
+
totals.lifetime.year_1 += Math.round(ltYear1);
|
|
468
|
+
totals.lifetime.year_2 += Math.round(ltYear2);
|
|
469
|
+
totals.lifetime.year_3 += Math.round(ltYear3);
|
|
470
|
+
totals.lifetime.total_3yr += Math.round(ltYear1 + ltYear2 + ltYear3);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
result.totals = totals;
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getMarginConversionFactor(vc: ValueCategory, s: SensitivityConfig): number {
|
|
478
|
+
switch (vc) {
|
|
479
|
+
case "overhead": return s.efficiency_to_margin;
|
|
480
|
+
case "direct_cost": return s.direct_cost_to_margin;
|
|
481
|
+
case "additional_business": return s.revenue_to_margin;
|
|
482
|
+
case "innovation": return s.innovation_to_margin;
|
|
483
|
+
case "risk": return s.risk_to_margin;
|
|
484
|
+
}
|
|
485
|
+
}
|