@things-factory/kpi 9.0.21 → 9.0.23
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/client/charts/kpi-boxplot-chart.ts +163 -0
- package/client/charts/kpi-radar-chart.ts +128 -0
- package/client/pages/kpi/kpi-list-page.ts +180 -22
- package/client/pages/kpi-category/kpi-category-list-page.ts +76 -3
- package/client/pages/kpi-category/kpi-category-value-calculator.ts +233 -0
- package/client/pages/kpi-dashboard/kpi-dashboard.ts +188 -0
- package/client/pages/kpi-metric/kpi-metric-list-page.ts +13 -1
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +43 -1
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.ts +3 -13
- package/client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.ts +13 -1
- package/client/pages/kpi-value/kpi-value-list-page.ts +45 -1
- package/dist-client/charts/kpi-boxplot-chart.d.ts +22 -0
- package/dist-client/charts/kpi-boxplot-chart.js +198 -0
- package/dist-client/charts/kpi-boxplot-chart.js.map +1 -0
- package/dist-client/charts/kpi-radar-chart.d.ts +16 -0
- package/dist-client/charts/kpi-radar-chart.js +138 -0
- package/dist-client/charts/kpi-radar-chart.js.map +1 -0
- package/dist-client/pages/kpi/kpi-list-page.d.ts +2 -1
- package/dist-client/pages/kpi/kpi-list-page.js +180 -22
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +3 -0
- package/dist-client/pages/kpi-category/kpi-category-list-page.js +71 -3
- package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.d.ts +13 -0
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.js +256 -0
- package/dist-client/pages/kpi-category/kpi-category-value-calculator.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +11 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +185 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +13 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +4 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +39 -2
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js +3 -13
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +13 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -0
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +45 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/calculator/evaluator.d.ts +8 -0
- package/dist-server/calculator/evaluator.js +42 -0
- package/dist-server/calculator/evaluator.js.map +1 -0
- package/dist-server/calculator/functions.d.ts +3 -0
- package/dist-server/calculator/functions.js +62 -0
- package/dist-server/calculator/functions.js.map +1 -0
- package/dist-server/calculator/index.d.ts +4 -0
- package/dist-server/calculator/index.js +8 -0
- package/dist-server/calculator/index.js.map +1 -0
- package/dist-server/calculator/parser.d.ts +21 -0
- package/dist-server/calculator/parser.js +121 -0
- package/dist-server/calculator/parser.js.map +1 -0
- package/dist-server/calculator/provider.d.ts +8 -0
- package/dist-server/calculator/provider.js +13 -0
- package/dist-server/calculator/provider.js.map +1 -0
- package/dist-server/controllers/kpi-metric-value-provider.d.ts +11 -0
- package/dist-server/controllers/kpi-metric-value-provider.js +63 -0
- package/dist-server/controllers/kpi-metric-value-provider.js.map +1 -0
- package/dist-server/controllers/kpi-value-provider.d.ts +11 -0
- package/dist-server/controllers/kpi-value-provider.js +46 -0
- package/dist-server/controllers/kpi-value-provider.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/service/kpi/aggregate-kpi.js +4 -4
- package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
- package/dist-server/service/kpi/kpi-grade.types.d.ts +11 -10
- package/dist-server/service/kpi/kpi-grade.types.js.map +1 -1
- package/dist-server/service/kpi/kpi-history.d.ts +2 -2
- package/dist-server/service/kpi/kpi-history.js.map +1 -1
- package/dist-server/service/kpi/kpi-mutation.d.ts +2 -0
- package/dist-server/service/kpi/kpi-mutation.js +126 -4
- package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
- package/dist-server/service/kpi/kpi-type.d.ts +8 -5
- package/dist-server/service/kpi/kpi-type.js +22 -8
- package/dist-server/service/kpi/kpi-type.js.map +1 -1
- package/dist-server/service/kpi/kpi.d.ts +6 -3
- package/dist-server/service/kpi/kpi.js +29 -9
- package/dist-server/service/kpi/kpi.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.js +3 -3
- package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-query.d.ts +13 -0
- package/dist-server/service/kpi-category/kpi-category-query.js +180 -0
- package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-type.d.ts +3 -0
- package/dist-server/service/kpi-category/kpi-category-type.js +16 -1
- package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category.d.ts +2 -0
- package/dist-server/service/kpi-category/kpi-category.js +10 -1
- package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +5 -3
- package/dist-server/service/kpi-metric/kpi-metric-type.js +5 -3
- package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric.d.ts +2 -8
- package/dist-server/service/kpi-metric/kpi-metric.js +3 -14
- package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +67 -45
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js +3 -2
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +2 -1
- package/dist-server/service/kpi-value/kpi-value-mutation.js +114 -6
- package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.d.ts +0 -2
- package/dist-server/service/kpi-value/kpi-value-query.js +0 -12
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-score.service.d.ts +26 -0
- package/dist-server/service/kpi-value/kpi-value-score.service.js +97 -0
- package/dist-server/service/kpi-value/kpi-value-score.service.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -0
- package/dist-server/service/kpi-value/kpi-value-type.js +14 -0
- package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value.d.ts +1 -0
- package/dist-server/service/kpi-value/kpi-value.js +9 -1
- package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
- package/dist-server/service/utils/value-date-util.d.ts +3 -0
- package/dist-server/service/utils/value-date-util.js +76 -0
- package/dist-server/service/utils/value-date-util.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/calculator/evaluator.ts +45 -0
- package/server/calculator/functions.ts +67 -0
- package/server/calculator/index.ts +4 -0
- package/server/calculator/parser.ts +128 -0
- package/server/calculator/provider.ts +10 -0
- package/server/controllers/kpi-metric-value-provider.ts +66 -0
- package/server/controllers/kpi-value-provider.ts +51 -0
- package/server/service/kpi/aggregate-kpi.ts +4 -4
- package/server/service/kpi/kpi-grade.types.ts +11 -10
- package/server/service/kpi/kpi-history.ts +2 -2
- package/server/service/kpi/kpi-mutation.ts +128 -4
- package/server/service/kpi/kpi-type.ts +21 -9
- package/server/service/kpi/kpi.ts +32 -10
- package/server/service/kpi-category/kpi-category-mutation.ts +3 -3
- package/server/service/kpi-category/kpi-category-query.ts +175 -1
- package/server/service/kpi-category/kpi-category-type.ts +17 -6
- package/server/service/kpi-category/kpi-category.ts +10 -1
- package/server/service/kpi-metric/kpi-metric-type.ts +7 -5
- package/server/service/kpi-metric/kpi-metric.ts +3 -15
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +67 -47
- package/server/service/kpi-metric-value/kpi-metric-value.ts +4 -2
- package/server/service/kpi-value/kpi-value-mutation.ts +110 -6
- package/server/service/kpi-value/kpi-value-query.ts +2 -8
- package/server/service/kpi-value/kpi-value-score.service.ts +112 -0
- package/server/service/kpi-value/kpi-value-type.ts +12 -0
- package/server/service/kpi-value/kpi-value.ts +8 -1
- package/server/service/utils/value-date-util.ts +72 -0
- package/dist-server/service/kpi-value/kpi-value-grade.service.d.ts +0 -34
- package/dist-server/service/kpi-value/kpi-value-grade.service.js +0 -117
- package/dist-server/service/kpi-value/kpi-value-grade.service.js.map +0 -1
- package/server/service/kpi-value/kpi-value-grade.service.ts +0 -127
|
@@ -5,7 +5,7 @@ import { ObjectType, Field, InputType, Int, ID, registerEnumType } from 'type-gr
|
|
|
5
5
|
import { ObjectRef, ScalarObject } from '@things-factory/shell'
|
|
6
6
|
|
|
7
7
|
import { Kpi, KpiStatus, KpiVizType } from './kpi'
|
|
8
|
-
import {
|
|
8
|
+
import { KpiScores } from './kpi-grade.types'
|
|
9
9
|
|
|
10
10
|
@InputType({ description: 'Input type for creating a new KPI. Used in mutations to provide KPI details.' })
|
|
11
11
|
export class NewKpi {
|
|
@@ -15,8 +15,8 @@ export class NewKpi {
|
|
|
15
15
|
@Field({ nullable: true, description: 'Detailed description of the KPI.' })
|
|
16
16
|
description?: string
|
|
17
17
|
|
|
18
|
-
@Field(type =>
|
|
19
|
-
|
|
18
|
+
@Field(type => ObjectRef, { nullable: true, description: 'Reference to the category to which this KPI belongs.' })
|
|
19
|
+
category?: ObjectRef
|
|
20
20
|
|
|
21
21
|
@Field({ nullable: true, description: 'Calculation formula for the KPI, using metric codes and operators.' })
|
|
22
22
|
formula?: string
|
|
@@ -57,9 +57,15 @@ export class NewKpi {
|
|
|
57
57
|
|
|
58
58
|
@Field(type => ScalarObject, {
|
|
59
59
|
nullable: true,
|
|
60
|
-
description: '
|
|
60
|
+
description: 'Score lookup table for this KPI version'
|
|
61
61
|
})
|
|
62
|
-
grades?:
|
|
62
|
+
grades?: KpiScores
|
|
63
|
+
|
|
64
|
+
@Field({
|
|
65
|
+
nullable: true,
|
|
66
|
+
description: 'Score calculation formula for this KPI. Converts KPI value to performance score (0-1).'
|
|
67
|
+
})
|
|
68
|
+
scoreFormula?: string
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
@InputType({ description: 'Input type for updating an existing KPI. Used in mutations to patch KPI details.' })
|
|
@@ -73,8 +79,8 @@ export class KpiPatch {
|
|
|
73
79
|
@Field({ nullable: true, description: 'Detailed description of the KPI.' })
|
|
74
80
|
description?: string
|
|
75
81
|
|
|
76
|
-
@Field(type =>
|
|
77
|
-
|
|
82
|
+
@Field(type => ObjectRef, { nullable: true, description: 'Reference to the category to which this KPI belongs.' })
|
|
83
|
+
category?: ObjectRef
|
|
78
84
|
|
|
79
85
|
@Field({ nullable: true, description: 'Calculation formula for the KPI, using metric codes and operators.' })
|
|
80
86
|
formula?: string
|
|
@@ -118,9 +124,15 @@ export class KpiPatch {
|
|
|
118
124
|
|
|
119
125
|
@Field(type => ScalarObject, {
|
|
120
126
|
nullable: true,
|
|
121
|
-
description: '
|
|
127
|
+
description: 'Score lookup table for this KPI version'
|
|
128
|
+
})
|
|
129
|
+
grades?: KpiScores
|
|
130
|
+
|
|
131
|
+
@Field({
|
|
132
|
+
nullable: true,
|
|
133
|
+
description: 'Score calculation formula for this KPI. Converts KPI value to performance score (0-1).'
|
|
122
134
|
})
|
|
123
|
-
|
|
135
|
+
scoreFormula?: string
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
@ObjectType()
|
|
@@ -19,7 +19,7 @@ import { KpiCategory } from '../kpi-category/kpi-category'
|
|
|
19
19
|
import { config } from '@things-factory/env'
|
|
20
20
|
import { ScalarObject } from '@things-factory/shell'
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { KpiScores } from './kpi-grade.types'
|
|
23
23
|
|
|
24
24
|
const ORMCONFIG = config.get('ormconfig', {})
|
|
25
25
|
const DATABASE_TYPE = ORMCONFIG.type
|
|
@@ -53,7 +53,9 @@ export enum KpiPeriodType {
|
|
|
53
53
|
WEEK = 'WEEK',
|
|
54
54
|
MONTH = 'MONTH',
|
|
55
55
|
QUARTER = 'QUARTER',
|
|
56
|
-
|
|
56
|
+
YEAR = 'YEAR',
|
|
57
|
+
RANGE = 'RANGE',
|
|
58
|
+
ALLTIME = 'ALLTIME'
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
registerEnumType(KpiStatus, {
|
|
@@ -68,7 +70,7 @@ registerEnumType(KpiVizType, {
|
|
|
68
70
|
|
|
69
71
|
registerEnumType(KpiPeriodType, {
|
|
70
72
|
name: 'KpiPeriodType',
|
|
71
|
-
description: 'Aggregation period type for KPI (DAY, WEEK, MONTH, QUARTER, RANGE)'
|
|
73
|
+
description: 'Aggregation period type for KPI (DAY, WEEK, MONTH, QUARTER, RANGE, ALLTIME)'
|
|
72
74
|
})
|
|
73
75
|
|
|
74
76
|
@Entity()
|
|
@@ -182,6 +184,33 @@ export class Kpi {
|
|
|
182
184
|
@Field({ nullable: true, description: 'Weight for aggregation in parent category.' })
|
|
183
185
|
weight?: number
|
|
184
186
|
|
|
187
|
+
@Column({ type: 'simple-json', nullable: true })
|
|
188
|
+
@Field(type => ScalarObject, {
|
|
189
|
+
nullable: true,
|
|
190
|
+
description:
|
|
191
|
+
'Performance index lookup table for complex transformations. @deprecated 향후 제거 예정. performanceFormula 사용 권장.'
|
|
192
|
+
})
|
|
193
|
+
grades?: KpiScores
|
|
194
|
+
|
|
195
|
+
@Column({
|
|
196
|
+
nullable: true,
|
|
197
|
+
type:
|
|
198
|
+
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
199
|
+
? 'longtext'
|
|
200
|
+
: DATABASE_TYPE == 'oracle'
|
|
201
|
+
? 'clob'
|
|
202
|
+
: DATABASE_TYPE == 'mssql'
|
|
203
|
+
? 'nvarchar'
|
|
204
|
+
: 'varchar',
|
|
205
|
+
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
206
|
+
})
|
|
207
|
+
@Field({
|
|
208
|
+
nullable: true,
|
|
209
|
+
description:
|
|
210
|
+
'Score calculation formula for this KPI. Converts KPI value to performance score (0-100). For complex mappings, use grades lookup table instead. Example: "if(value >= 90, 100, if(value >= 80, 85, 70))" or "value * 0.8 + 20"'
|
|
211
|
+
})
|
|
212
|
+
scoreFormula?: string
|
|
213
|
+
|
|
185
214
|
@CreateDateColumn()
|
|
186
215
|
@Field({ nullable: true, description: 'Timestamp when this KPI was created.' })
|
|
187
216
|
createdAt?: Date
|
|
@@ -212,11 +241,4 @@ export class Kpi {
|
|
|
212
241
|
|
|
213
242
|
@Field(type => String, { nullable: true, description: 'Thumbnail image or file path for this KPI.' })
|
|
214
243
|
thumbnail?: string
|
|
215
|
-
|
|
216
|
-
@Column({ type: 'simple-json', nullable: true })
|
|
217
|
-
@Field(type => ScalarObject, {
|
|
218
|
-
nullable: true,
|
|
219
|
-
description: 'Grade configuration for this KPI version'
|
|
220
|
-
})
|
|
221
|
-
grades?: KpiGrades
|
|
222
244
|
}
|
|
@@ -10,14 +10,14 @@ export class KpiCategoryMutation {
|
|
|
10
10
|
@Directive('@transaction')
|
|
11
11
|
@Mutation(returns => KpiCategory, { description: 'Create a new KPI category with the provided details.' })
|
|
12
12
|
async createKpiCategory(
|
|
13
|
-
@Arg('
|
|
14
|
-
|
|
13
|
+
@Arg('kpiCategory', { description: 'Input object containing details for the new KPI category.' })
|
|
14
|
+
kpiCategory: NewKpiCategory,
|
|
15
15
|
@Ctx() context: ResolverContext
|
|
16
16
|
): Promise<KpiCategory> {
|
|
17
17
|
const { domain, user, tx } = context.state
|
|
18
18
|
|
|
19
19
|
const result = await getRepository(KpiCategory, tx).save({
|
|
20
|
-
...
|
|
20
|
+
...kpiCategory,
|
|
21
21
|
domain,
|
|
22
22
|
creator: user,
|
|
23
23
|
updater: user
|
|
@@ -1,9 +1,44 @@
|
|
|
1
|
-
import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx, Directive } from 'type-graphql'
|
|
1
|
+
import { Resolver, Query, FieldResolver, Float, Root, Args, Arg, Ctx, Directive, ObjectType, Field } from 'type-graphql'
|
|
2
2
|
import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
|
|
3
3
|
import { User } from '@things-factory/auth-base'
|
|
4
4
|
import { KpiCategory } from './kpi-category'
|
|
5
5
|
import { KpiCategoryList } from './kpi-category-type'
|
|
6
6
|
import { Kpi } from '../kpi/kpi'
|
|
7
|
+
import { KpiValue } from '../kpi-value/kpi-value'
|
|
8
|
+
import { parseFormula } from '../../calculator/parser'
|
|
9
|
+
import { evaluateFormula } from '../../calculator/evaluator'
|
|
10
|
+
import { builtinFunctions } from '../../calculator/functions'
|
|
11
|
+
import { KpiValueProvider } from '../../controllers/kpi-value-provider'
|
|
12
|
+
import { getDefaultValueDate } from '../utils/value-date-util'
|
|
13
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
14
|
+
import { KpiValueScoreService } from '../kpi-value/kpi-value-score.service'
|
|
15
|
+
|
|
16
|
+
@ObjectType()
|
|
17
|
+
class KpiValueItem {
|
|
18
|
+
@Field()
|
|
19
|
+
kpiId: string
|
|
20
|
+
|
|
21
|
+
@Field(type => Float, { nullable: true })
|
|
22
|
+
value: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@ObjectType()
|
|
26
|
+
class KpiCategoryValueResult {
|
|
27
|
+
@Field(type => Float, { nullable: true })
|
|
28
|
+
value?: number
|
|
29
|
+
|
|
30
|
+
@Field(type => Float, { nullable: true, description: 'Performance score for this category (0-1 range)' })
|
|
31
|
+
score?: number
|
|
32
|
+
|
|
33
|
+
@Field({ nullable: true })
|
|
34
|
+
valueDate?: string
|
|
35
|
+
|
|
36
|
+
@Field({ nullable: true })
|
|
37
|
+
group?: string
|
|
38
|
+
|
|
39
|
+
@Field(type => [KpiValueItem])
|
|
40
|
+
kpiValues: KpiValueItem[]
|
|
41
|
+
}
|
|
7
42
|
|
|
8
43
|
@Resolver(KpiCategory)
|
|
9
44
|
export class KpiCategoryQuery {
|
|
@@ -41,6 +76,145 @@ export class KpiCategoryQuery {
|
|
|
41
76
|
return { items, total }
|
|
42
77
|
}
|
|
43
78
|
|
|
79
|
+
@Query(returns => KpiCategoryValueResult, { description: '카테고리 단위 KPI 실적값 집계/조회' })
|
|
80
|
+
async calculateKpiValue(
|
|
81
|
+
@Arg('categoryId') categoryId: string,
|
|
82
|
+
@Arg('valueDate', { nullable: true }) valueDate: string,
|
|
83
|
+
@Arg('group', { nullable: true }) group: string,
|
|
84
|
+
@Ctx() context: ResolverContext
|
|
85
|
+
): Promise<KpiCategoryValueResult> {
|
|
86
|
+
const { domain, tx } = context.state
|
|
87
|
+
// 1. 카테고리 정보 조회 (formula 포함)
|
|
88
|
+
const category = await getRepository(KpiCategory).findOne({
|
|
89
|
+
where: { domain: { id: domain.id }, id: categoryId }
|
|
90
|
+
})
|
|
91
|
+
if (!category) return { value: null, score: null, kpiValues: [] }
|
|
92
|
+
|
|
93
|
+
// 기본 계산 기준일 설정 (카테고리 periodType 사용)
|
|
94
|
+
const defaultDate = valueDate || getDefaultValueDate(category.periodType || KpiPeriodType.DAY, 'last')
|
|
95
|
+
|
|
96
|
+
// 2. 카테고리 formula가 있으면 formula로 계산
|
|
97
|
+
if (category.formula) {
|
|
98
|
+
const ast = parseFormula(category.formula)
|
|
99
|
+
const provider = new KpiValueProvider({
|
|
100
|
+
valueDate: defaultDate,
|
|
101
|
+
group,
|
|
102
|
+
domainId: domain.id,
|
|
103
|
+
tx
|
|
104
|
+
})
|
|
105
|
+
const evalContext = { functions: builtinFunctions, provider }
|
|
106
|
+
const value = await evaluateFormula(ast, evalContext)
|
|
107
|
+
// 카테고리 formula 결과를 score로 변환 (0-1 범위)
|
|
108
|
+
const score = value !== null && value !== undefined && !isNaN(value) ? Math.max(0, Math.min(1, value)) : null
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
value,
|
|
112
|
+
score,
|
|
113
|
+
valueDate: defaultDate,
|
|
114
|
+
group,
|
|
115
|
+
kpiValues: [] // 카테고리 formula 기반이므로 개별 KPI 값은 제공하지 않음
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 3. formula가 없으면 KPI score들의 가중 평균으로 category score 계산
|
|
120
|
+
const kpis = await getRepository(Kpi).find({
|
|
121
|
+
where: { domain: { id: domain.id }, category: { id: categoryId } }
|
|
122
|
+
})
|
|
123
|
+
if (!kpis.length) return { value: null, score: null, kpiValues: [] }
|
|
124
|
+
|
|
125
|
+
let totalWeight = 0
|
|
126
|
+
let weightedScoreSum = 0
|
|
127
|
+
const kpiValues: KpiValueItem[] = []
|
|
128
|
+
|
|
129
|
+
for (const kpi of kpis) {
|
|
130
|
+
// KPI value 계산
|
|
131
|
+
let value = null
|
|
132
|
+
if (kpi.formula) {
|
|
133
|
+
const ast = parseFormula(kpi.formula)
|
|
134
|
+
const provider = new KpiValueProvider({
|
|
135
|
+
valueDate: defaultDate,
|
|
136
|
+
group,
|
|
137
|
+
domainId: domain.id,
|
|
138
|
+
tx
|
|
139
|
+
})
|
|
140
|
+
const evalContext = { functions: builtinFunctions, provider }
|
|
141
|
+
value = await evaluateFormula(ast, evalContext)
|
|
142
|
+
} else {
|
|
143
|
+
const kpiValue = await getRepository(KpiValue).findOne({
|
|
144
|
+
where: {
|
|
145
|
+
kpi: { id: kpi.id },
|
|
146
|
+
valueDate: defaultDate,
|
|
147
|
+
group: group ?? '',
|
|
148
|
+
domain: { id: domain.id }
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
value = kpiValue?.value ?? 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// KPI score 계산
|
|
155
|
+
const scoreService = new KpiValueScoreService()
|
|
156
|
+
let kpiScore = null
|
|
157
|
+
if (value !== null && value !== undefined) {
|
|
158
|
+
let scoreResult = await scoreService.calculateScoreFromFormula(kpi, value)
|
|
159
|
+
if (!scoreResult) {
|
|
160
|
+
scoreResult = scoreService.calculateScoreFromLookup(kpi, value)
|
|
161
|
+
}
|
|
162
|
+
kpiScore = scoreResult?.score ?? null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const weight = kpi.weight ?? 1
|
|
166
|
+
if (kpiScore !== null) {
|
|
167
|
+
weightedScoreSum += kpiScore * weight
|
|
168
|
+
totalWeight += weight
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
kpiValues.push({ kpiId: kpi.id, value })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// category score 계산 (KPI score들의 가중 평균)
|
|
175
|
+
const categoryScore = totalWeight ? weightedScoreSum / totalWeight : null
|
|
176
|
+
|
|
177
|
+
// category value는 기존 방식대로 계산 (호환성 유지)
|
|
178
|
+
let totalValueWeight = 0
|
|
179
|
+
let weightedValueSum = 0
|
|
180
|
+
for (const kpi of kpis) {
|
|
181
|
+
let value = null
|
|
182
|
+
if (kpi.formula) {
|
|
183
|
+
const ast = parseFormula(kpi.formula)
|
|
184
|
+
const provider = new KpiValueProvider({
|
|
185
|
+
valueDate: defaultDate,
|
|
186
|
+
group,
|
|
187
|
+
domainId: domain.id,
|
|
188
|
+
tx
|
|
189
|
+
})
|
|
190
|
+
const evalContext = { functions: builtinFunctions, provider }
|
|
191
|
+
value = await evaluateFormula(ast, evalContext)
|
|
192
|
+
} else {
|
|
193
|
+
const kpiValue = await getRepository(KpiValue).findOne({
|
|
194
|
+
where: {
|
|
195
|
+
kpi: { id: kpi.id },
|
|
196
|
+
valueDate: defaultDate,
|
|
197
|
+
group: group ?? '',
|
|
198
|
+
domain: { id: domain.id }
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
value = kpiValue?.value ?? 0
|
|
202
|
+
}
|
|
203
|
+
const weight = kpi.weight ?? 1
|
|
204
|
+
weightedValueSum += (value ?? 0) * weight
|
|
205
|
+
totalValueWeight += weight
|
|
206
|
+
}
|
|
207
|
+
const resultValue = totalValueWeight ? weightedValueSum / totalValueWeight : null
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
value: resultValue,
|
|
211
|
+
score: categoryScore,
|
|
212
|
+
valueDate: defaultDate,
|
|
213
|
+
group,
|
|
214
|
+
kpiValues
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
44
218
|
@FieldResolver(type => [Kpi])
|
|
45
219
|
async kpis(@Root() kpiCategory: KpiCategory): Promise<Kpi[]> {
|
|
46
220
|
return await getRepository(Kpi).find({
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
|
3
|
-
import { ObjectType, Field, InputType, Int, ID, registerEnumType } from 'type-graphql'
|
|
4
|
-
|
|
5
|
-
import { ObjectRef, ScalarObject } from '@things-factory/shell'
|
|
1
|
+
import { ObjectType, Field, InputType, Int, ID } from 'type-graphql'
|
|
6
2
|
|
|
7
3
|
import { KpiCategory } from './kpi-category'
|
|
4
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
8
5
|
|
|
9
6
|
@InputType({
|
|
10
7
|
description: 'Input type for creating a new KPI category. Used in mutations to provide category details.'
|
|
@@ -24,6 +21,13 @@ export class NewKpiCategory {
|
|
|
24
21
|
|
|
25
22
|
@Field({ nullable: true, description: 'Weight for aggregation in higher-level summary.' })
|
|
26
23
|
weight?: number
|
|
24
|
+
|
|
25
|
+
@Field(type => KpiPeriodType, {
|
|
26
|
+
nullable: true,
|
|
27
|
+
description:
|
|
28
|
+
'Calculation formula for category score using KPI score variables. Example: "avg(sales_score, profit_score)" or "sales_score * 0.6 + profit_score * 0.4". If not provided, weighted average of child KPI scores will be used.'
|
|
29
|
+
})
|
|
30
|
+
periodType?: KpiPeriodType
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
@InputType({
|
|
@@ -42,12 +46,19 @@ export class KpiCategoryPatch {
|
|
|
42
46
|
@Field({ nullable: true, description: 'Whether this category is active (usable) or not.' })
|
|
43
47
|
active?: boolean
|
|
44
48
|
|
|
45
|
-
@Field({
|
|
49
|
+
@Field({
|
|
50
|
+
nullable: true,
|
|
51
|
+
description:
|
|
52
|
+
'Calculation formula for category score using KPI score variables. Example: "avg(sales_score, profit_score)" or "sales_score * 0.6 + profit_score * 0.4". If not provided, weighted average of child KPI scores will be used.'
|
|
53
|
+
})
|
|
46
54
|
formula?: string
|
|
47
55
|
|
|
48
56
|
@Field({ nullable: true, description: 'Weight for aggregation in higher-level summary.' })
|
|
49
57
|
weight?: number
|
|
50
58
|
|
|
59
|
+
@Field(type => KpiPeriodType, { nullable: true, description: 'Aggregation period type for this category.' })
|
|
60
|
+
periodType?: KpiPeriodType
|
|
61
|
+
|
|
51
62
|
@Field({ nullable: true, description: 'Custom flag for update operations (internal use).' })
|
|
52
63
|
cuFlag?: string
|
|
53
64
|
}
|
|
@@ -14,6 +14,7 @@ import { ObjectType, Field, Int, ID } from 'type-graphql'
|
|
|
14
14
|
import { Domain } from '@things-factory/shell'
|
|
15
15
|
import { User } from '@things-factory/auth-base'
|
|
16
16
|
import { Kpi } from '../kpi/kpi'
|
|
17
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
17
18
|
|
|
18
19
|
@Entity()
|
|
19
20
|
@Index('ix_kpi_category_0', (kpiCategory: KpiCategory) => [kpiCategory.domain, kpiCategory.name], {
|
|
@@ -49,13 +50,21 @@ export class KpiCategory {
|
|
|
49
50
|
active?: boolean
|
|
50
51
|
|
|
51
52
|
@Column({ nullable: true })
|
|
52
|
-
@Field({
|
|
53
|
+
@Field({
|
|
54
|
+
nullable: true,
|
|
55
|
+
description:
|
|
56
|
+
'Calculation formula for category score using KPI score variables. Example: "avg(sales_score, profit_score)" or "sales_score * 0.6 + profit_score * 0.4". If not provided, weighted average of child KPI scores will be used.'
|
|
57
|
+
})
|
|
53
58
|
formula?: string
|
|
54
59
|
|
|
55
60
|
@Column({ type: 'float', nullable: true, default: 1 })
|
|
56
61
|
@Field({ nullable: true, description: 'Weight for aggregation in higher-level summary.' })
|
|
57
62
|
weight?: number
|
|
58
63
|
|
|
64
|
+
@Column({ default: 'DAY' })
|
|
65
|
+
@Field(type => KpiPeriodType, { description: 'Aggregation period type for this category.' })
|
|
66
|
+
periodType: KpiPeriodType
|
|
67
|
+
|
|
59
68
|
@ManyToOne(type => User, { nullable: true })
|
|
60
69
|
@Field(type => User, { nullable: true, description: 'User who created this KPI category.' })
|
|
61
70
|
creator?: User
|
|
@@ -2,7 +2,9 @@ import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
|
|
|
2
2
|
import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
|
3
3
|
import { ObjectType, Field, InputType, Int, ID, registerEnumType } from 'type-graphql'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
6
|
+
import { KpiMetricCollectType } from './kpi-metric'
|
|
7
|
+
import { KpiMetric } from './kpi-metric'
|
|
6
8
|
|
|
7
9
|
@InputType({ description: 'Input type for creating a new KPI metric. Used in mutations to provide metric details.' })
|
|
8
10
|
export class NewKpiMetric {
|
|
@@ -41,8 +43,8 @@ export class NewKpiMetric {
|
|
|
41
43
|
@Field({ nullable: true, description: 'Schedule ID for the registered cron job.' })
|
|
42
44
|
scheduleId?: string
|
|
43
45
|
|
|
44
|
-
@Field(type =>
|
|
45
|
-
periodType?:
|
|
46
|
+
@Field(type => KpiPeriodType, { nullable: true, description: 'Aggregation period type for this metric.' })
|
|
47
|
+
periodType?: KpiPeriodType
|
|
46
48
|
|
|
47
49
|
@Field(type => KpiMetricCollectType, { nullable: true, description: '데이터 수집 방식' })
|
|
48
50
|
collectType?: KpiMetricCollectType
|
|
@@ -89,8 +91,8 @@ export class KpiMetricPatch {
|
|
|
89
91
|
@Field({ nullable: true, description: 'Schedule ID for the registered cron job.' })
|
|
90
92
|
scheduleId?: string
|
|
91
93
|
|
|
92
|
-
@Field(type =>
|
|
93
|
-
periodType?:
|
|
94
|
+
@Field(type => KpiPeriodType, { nullable: true, description: 'Aggregation period type for this metric.' })
|
|
95
|
+
periodType?: KpiPeriodType
|
|
94
96
|
|
|
95
97
|
@Field({ nullable: true })
|
|
96
98
|
cuFlag?: string
|
|
@@ -14,19 +14,7 @@ import { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'
|
|
|
14
14
|
import { Domain } from '@things-factory/shell'
|
|
15
15
|
import { User } from '@things-factory/auth-base'
|
|
16
16
|
import { DataSet } from '@things-factory/dataset'
|
|
17
|
-
|
|
18
|
-
export enum KpiMetricPeriodType {
|
|
19
|
-
DAY = 'DAY',
|
|
20
|
-
WEEK = 'WEEK',
|
|
21
|
-
MONTH = 'MONTH',
|
|
22
|
-
QUARTER = 'QUARTER',
|
|
23
|
-
RANGE = 'RANGE'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
registerEnumType(KpiMetricPeriodType, {
|
|
27
|
-
name: 'KpiMetricPeriodType',
|
|
28
|
-
description: 'Aggregation period type for metric (DAY, WEEK, MONTH, QUARTER, RANGE)'
|
|
29
|
-
})
|
|
17
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
30
18
|
|
|
31
19
|
export enum KpiMetricCollectType {
|
|
32
20
|
AUTO = 'AUTO', // 데이터셋 등 자동 수집
|
|
@@ -110,8 +98,8 @@ export class KpiMetric {
|
|
|
110
98
|
collectType: KpiMetricCollectType
|
|
111
99
|
|
|
112
100
|
@Column({ default: 'DAY' })
|
|
113
|
-
@Field(type =>
|
|
114
|
-
periodType:
|
|
101
|
+
@Field(type => KpiPeriodType, { description: 'Aggregation period type for this metric.' })
|
|
102
|
+
periodType: KpiPeriodType
|
|
115
103
|
|
|
116
104
|
@CreateDateColumn()
|
|
117
105
|
@Field({ nullable: true, description: 'Timestamp when this KPI metric was created.' })
|
|
@@ -8,37 +8,7 @@ import { KpiMetricValue } from './kpi-metric-value'
|
|
|
8
8
|
import { NewKpiMetricValue, KpiMetricValuePatch } from './kpi-metric-value-type'
|
|
9
9
|
import { KpiMetric } from '../kpi-metric/kpi-metric'
|
|
10
10
|
import { ScalarObject } from '@things-factory/shell'
|
|
11
|
-
|
|
12
|
-
function getISOWeek(date: Date): number {
|
|
13
|
-
const tmp = new Date(date.getTime())
|
|
14
|
-
tmp.setHours(0, 0, 0, 0)
|
|
15
|
-
tmp.setDate(tmp.getDate() + 4 - (tmp.getDay() || 7))
|
|
16
|
-
const yearStart = new Date(tmp.getFullYear(), 0, 1)
|
|
17
|
-
const weekNo = Math.ceil(((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7)
|
|
18
|
-
return weekNo
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getDefaultValueDate(periodType: KpiPeriodType): string {
|
|
22
|
-
const now = new Date()
|
|
23
|
-
switch (periodType) {
|
|
24
|
-
case KpiPeriodType.DAY:
|
|
25
|
-
return now.toISOString().slice(0, 10)
|
|
26
|
-
case KpiPeriodType.MONTH:
|
|
27
|
-
return now.toISOString().slice(0, 7)
|
|
28
|
-
case KpiPeriodType.QUARTER: {
|
|
29
|
-
const year = now.getFullYear()
|
|
30
|
-
const quarter = Math.floor(now.getMonth() / 3) + 1
|
|
31
|
-
return `${year}-Q${quarter}`
|
|
32
|
-
}
|
|
33
|
-
case KpiPeriodType.WEEK: {
|
|
34
|
-
const year = now.getFullYear()
|
|
35
|
-
const week = getISOWeek(now)
|
|
36
|
-
return `${year}-W${week}`
|
|
37
|
-
}
|
|
38
|
-
default:
|
|
39
|
-
return now.toISOString().slice(0, 10)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
11
|
+
import { getDefaultValueDate } from '../utils/value-date-util'
|
|
42
12
|
|
|
43
13
|
@Resolver(KpiMetricValue)
|
|
44
14
|
export class KpiMetricValueMutation {
|
|
@@ -55,11 +25,26 @@ export class KpiMetricValueMutation {
|
|
|
55
25
|
? await getRepository(KpiMetric).findOne({ where: { id: metricValue.metricId } })
|
|
56
26
|
: undefined
|
|
57
27
|
|
|
28
|
+
// valueDate 자동 생성/보정 로직 추가
|
|
29
|
+
let periodType = metricValue.periodType
|
|
30
|
+
if (!periodType && metric) {
|
|
31
|
+
periodType = metric.periodType
|
|
32
|
+
}
|
|
33
|
+
if (!periodType) {
|
|
34
|
+
periodType = KpiPeriodType.DAY
|
|
35
|
+
}
|
|
36
|
+
// periodType을 string으로 변환 후 KpiPeriodType으로 강제 변환
|
|
37
|
+
periodType = KpiPeriodType[String(periodType) as keyof typeof KpiPeriodType]
|
|
38
|
+
let valueDate = metricValue.valueDate
|
|
39
|
+
if (!valueDate || !isValueDateValidForPeriodType(valueDate, periodType)) {
|
|
40
|
+
valueDate = getDefaultValueDate(periodType)
|
|
41
|
+
}
|
|
42
|
+
|
|
58
43
|
const entity: Partial<KpiMetricValue> = {
|
|
59
44
|
metric,
|
|
60
45
|
metricId: metricValue.metricId,
|
|
61
|
-
valueDate
|
|
62
|
-
periodType
|
|
46
|
+
valueDate,
|
|
47
|
+
periodType,
|
|
63
48
|
value: metricValue.value,
|
|
64
49
|
group: metricValue.group,
|
|
65
50
|
unit: metricValue.unit,
|
|
@@ -86,24 +71,40 @@ export class KpiMetricValueMutation {
|
|
|
86
71
|
const { domain, user, tx } = context.state
|
|
87
72
|
const metric = await getRepository(KpiMetric).findOne({ where: { name: metricName, domain: { id: domain.id } } })
|
|
88
73
|
if (!metric) throw new Error(`Metric not found: ${metricName}`)
|
|
89
|
-
const periodType =
|
|
90
|
-
const
|
|
74
|
+
const periodType = metric.periodType || KpiPeriodType.DAY
|
|
75
|
+
const valueDate = getDefaultValueDate(periodType)
|
|
91
76
|
if (value == null && meta == null) {
|
|
92
77
|
throw new Error('Either value or meta must be provided.')
|
|
93
78
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
const repo = getRepository(KpiMetricValue, tx)
|
|
80
|
+
let entity = await repo.findOne({
|
|
81
|
+
where: {
|
|
82
|
+
metric: { id: metric.id },
|
|
83
|
+
valueDate,
|
|
84
|
+
periodType,
|
|
85
|
+
group,
|
|
86
|
+
domain: { id: domain.id }
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
if (entity) {
|
|
90
|
+
entity.value = value
|
|
91
|
+
entity.meta = meta
|
|
92
|
+
entity.updater = user
|
|
93
|
+
} else {
|
|
94
|
+
entity = repo.create({
|
|
95
|
+
metric,
|
|
96
|
+
metricId: metric.id,
|
|
97
|
+
value,
|
|
98
|
+
meta,
|
|
99
|
+
valueDate,
|
|
100
|
+
periodType,
|
|
101
|
+
group,
|
|
102
|
+
domain,
|
|
103
|
+
creator: user,
|
|
104
|
+
updater: user
|
|
105
|
+
})
|
|
105
106
|
}
|
|
106
|
-
return await
|
|
107
|
+
return await repo.save(entity)
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
@Directive('@transaction')
|
|
@@ -213,3 +214,22 @@ export class KpiMetricValueMutation {
|
|
|
213
214
|
return true
|
|
214
215
|
}
|
|
215
216
|
}
|
|
217
|
+
|
|
218
|
+
// valueDate와 periodType의 일치 여부를 검사하는 유틸리티 함수(임시, 아래에 추가)
|
|
219
|
+
function isValueDateValidForPeriodType(valueDate: string, periodType: KpiPeriodType): boolean {
|
|
220
|
+
if (!valueDate) return false
|
|
221
|
+
switch (periodType) {
|
|
222
|
+
case KpiPeriodType.DAY:
|
|
223
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(valueDate)
|
|
224
|
+
case KpiPeriodType.MONTH:
|
|
225
|
+
return /^\d{4}-\d{2}$/.test(valueDate)
|
|
226
|
+
case KpiPeriodType.QUARTER:
|
|
227
|
+
return /^\d{4}-Q[1-4]$/.test(valueDate)
|
|
228
|
+
case KpiPeriodType.WEEK:
|
|
229
|
+
return /^\d{4}-W\d{1,2}$/.test(valueDate)
|
|
230
|
+
case KpiPeriodType.ALLTIME:
|
|
231
|
+
return valueDate === 'ALLTIME'
|
|
232
|
+
default:
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
PrimaryGeneratedColumn,
|
|
4
4
|
Column,
|
|
5
5
|
ManyToOne,
|
|
6
|
+
Index,
|
|
6
7
|
JoinColumn,
|
|
7
8
|
CreateDateColumn,
|
|
8
9
|
UpdateDateColumn,
|
|
@@ -15,6 +16,7 @@ import { KpiPeriodType } from '../kpi/kpi'
|
|
|
15
16
|
import { User } from '@things-factory/auth-base'
|
|
16
17
|
|
|
17
18
|
@Entity()
|
|
19
|
+
@Index('ix_kpi_metric_value_latest', ['domain', 'metric', 'valueDate', 'group'], { unique: true })
|
|
18
20
|
@ObjectType({ description: 'Current value for each KPI metric (can be used for both state and history).' })
|
|
19
21
|
export class KpiMetricValue {
|
|
20
22
|
@PrimaryGeneratedColumn('uuid')
|
|
@@ -46,7 +48,7 @@ export class KpiMetricValue {
|
|
|
46
48
|
@Field({ nullable: true })
|
|
47
49
|
unit?: string
|
|
48
50
|
|
|
49
|
-
@Column(
|
|
51
|
+
@Column()
|
|
50
52
|
@Field({
|
|
51
53
|
description:
|
|
52
54
|
'Date or period for which this metric value is recorded (e.g., day: YYYY-MM-DD, month: YYYY-MM, range: YYYY-MM-DD~YYYY-MM-DD).'
|
|
@@ -57,7 +59,7 @@ export class KpiMetricValue {
|
|
|
57
59
|
@Field(type => KpiPeriodType)
|
|
58
60
|
periodType: KpiPeriodType
|
|
59
61
|
|
|
60
|
-
@Column({
|
|
62
|
+
@Column({ default: '' })
|
|
61
63
|
@Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
62
64
|
group?: string
|
|
63
65
|
|