@things-factory/kpi 9.0.18 → 9.0.19
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/bootstrap.ts +8 -0
- package/client/pages/kpi/kpi-list-page.ts +91 -11
- package/client/pages/kpi-category/kpi-category-list-page.ts +80 -8
- package/client/pages/kpi-history/kpi-history-list-page.ts +1 -1
- package/client/pages/kpi-metric/kpi-metric-list-page.ts +31 -7
- package/client/pages/kpi-metric-value/kpi-metric-value-importer.ts +65 -0
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +299 -0
- package/client/pages/{kpi-value/kpi-value-manual-entry-form.ts → kpi-metric-value/kpi-metric-value-manual-entry-form.ts} +18 -44
- package/client/pages/{kpi-value/kpi-value-manual-entry-page.ts → kpi-metric-value/kpi-metric-value-manual-entry-page.ts} +21 -21
- package/client/pages/kpi-value/kpi-value-list-page.ts +4 -6
- package/client/route.ts +6 -2
- package/dist-client/bootstrap.d.ts +2 -0
- package/dist-client/bootstrap.js +7 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/pages/kpi/kpi-list-page.d.ts +6 -0
- package/dist-client/pages/kpi/kpi-list-page.js +94 -11
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi-category/kpi-category-list-page.d.ts +5 -0
- package/dist-client/pages/kpi-category/kpi-category-list-page.js +83 -8
- package/dist-client/pages/kpi-category/kpi-category-list-page.js.map +1 -1
- package/dist-client/pages/kpi-history/kpi-history-list-page.js +1 -1
- package/dist-client/pages/kpi-history/kpi-history-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js +29 -5
- package/dist-client/pages/kpi-metric/kpi-metric-list-page.js.map +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.d.ts +23 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js +75 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-importer.js.map +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +61 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +301 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -0
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-form.d.ts → kpi-metric-value/kpi-metric-value-manual-entry-form.d.ts} +3 -5
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-form.js → kpi-metric-value/kpi-metric-value-manual-entry-form.js} +27 -56
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-form.js.map +1 -0
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-page.d.ts → kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts} +5 -5
- package/dist-client/pages/{kpi-value/kpi-value-manual-entry-page.js → kpi-metric-value/kpi-metric-value-manual-entry-page.js} +28 -28
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -0
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +4 -6
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +5 -2
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +6 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/kpi/aggregate-kpi.js +5 -7
- package/dist-server/service/kpi/aggregate-kpi.js.map +1 -1
- package/dist-server/service/kpi/kpi-history.d.ts +3 -1
- package/dist-server/service/kpi/kpi-history.js +10 -0
- package/dist-server/service/kpi/kpi-history.js.map +1 -1
- package/dist-server/service/kpi/kpi-mutation.js +1 -1
- package/dist-server/service/kpi/kpi-mutation.js.map +1 -1
- package/dist-server/service/kpi/kpi-type.d.ts +2 -0
- package/dist-server/service/kpi/kpi-type.js +8 -0
- package/dist-server/service/kpi/kpi-type.js.map +1 -1
- package/dist-server/service/kpi/kpi.d.ts +9 -0
- package/dist-server/service/kpi/kpi.js +23 -1
- package/dist-server/service/kpi/kpi.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.js +0 -8
- package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-type.d.ts +4 -2
- package/dist-server/service/kpi-category/kpi-category-type.js +16 -8
- package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category.d.ts +2 -2
- package/dist-server/service/kpi-category/kpi-category.js +8 -8
- package/dist-server/service/kpi-category/kpi-category.js.map +1 -1
- package/dist-server/service/kpi-metric/aggregate-kpi-metric.js +31 -74
- package/dist-server/service/kpi-metric/aggregate-kpi-metric.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-mutation.d.ts +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-mutation.js +15 -28
- package/dist-server/service/kpi-metric/kpi-metric-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric-type.d.ts +6 -4
- package/dist-server/service/kpi-metric/kpi-metric-type.js +20 -12
- package/dist-server/service/kpi-metric/kpi-metric-type.js.map +1 -1
- package/dist-server/service/kpi-metric/kpi-metric.d.ts +15 -2
- package/dist-server/service/kpi-metric/kpi-metric.js +34 -14
- package/dist-server/service/kpi-metric/kpi-metric.js.map +1 -1
- package/dist-server/service/kpi-metric-value/index.d.ts +6 -0
- package/dist-server/service/kpi-metric-value/index.js +10 -0
- package/dist-server/service/kpi-metric-value/index.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +11 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +229 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +13 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +95 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-type.d.ts +26 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js +112 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-type.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value.d.ts +23 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js +106 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value.js.map +1 -0
- package/dist-server/service/kpi-value/kpi-value-mutation.js +1 -2
- package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.js +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-type.d.ts +2 -4
- package/dist-server/service/kpi-value/kpi-value-type.js +4 -18
- package/dist-server/service/kpi-value/kpi-value-type.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value.d.ts +3 -3
- package/dist-server/service/kpi-value/kpi-value.js +13 -14
- package/dist-server/service/kpi-value/kpi-value.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/index.ts +6 -1
- package/server/service/kpi/aggregate-kpi.ts +5 -8
- package/server/service/kpi/kpi-history.ts +9 -1
- package/server/service/kpi/kpi-mutation.ts +1 -1
- package/server/service/kpi/kpi-type.ts +6 -0
- package/server/service/kpi/kpi.ts +21 -0
- package/server/service/kpi-category/kpi-category-mutation.ts +0 -10
- package/server/service/kpi-category/kpi-category-type.ts +12 -6
- package/server/service/kpi-category/kpi-category.ts +6 -6
- package/server/service/kpi-metric/aggregate-kpi-metric.ts +29 -69
- package/server/service/kpi-metric/kpi-metric-mutation.ts +15 -26
- package/server/service/kpi-metric/kpi-metric-type.ts +17 -12
- package/server/service/kpi-metric/kpi-metric.ts +32 -11
- package/server/service/kpi-metric-value/index.ts +7 -0
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +215 -0
- package/server/service/kpi-metric-value/kpi-metric-value-query.ts +60 -0
- package/server/service/kpi-metric-value/kpi-metric-value-type.ts +82 -0
- package/server/service/kpi-metric-value/kpi-metric-value.ts +91 -0
- package/server/service/kpi-value/kpi-value-mutation.ts +1 -2
- package/server/service/kpi-value/kpi-value-query.ts +1 -1
- package/server/service/kpi-value/kpi-value-type.ts +4 -16
- package/server/service/kpi-value/kpi-value.ts +14 -14
- package/things-factory.config.js +5 -3
- package/translations/en.json +8 -1
- package/translations/ja.json +8 -1
- package/translations/ko.json +9 -2
- package/translations/ms.json +9 -2
- package/translations/zh.json +8 -1
- package/dist-client/pages/kpi-value/kpi-value-manual-entry-form.js.map +0 -1
- package/dist-client/pages/kpi-value/kpi-value-manual-entry-page.js.map +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
|
|
2
|
+
import { In } from 'typeorm'
|
|
3
|
+
import { getRepository } from '@things-factory/shell'
|
|
4
|
+
import { Float } from 'type-graphql'
|
|
5
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
6
|
+
|
|
7
|
+
import { KpiMetricValue } from './kpi-metric-value'
|
|
8
|
+
import { NewKpiMetricValue, KpiMetricValuePatch } from './kpi-metric-value-type'
|
|
9
|
+
import { KpiMetric } from '../kpi-metric/kpi-metric'
|
|
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
|
+
}
|
|
42
|
+
|
|
43
|
+
@Resolver(KpiMetricValue)
|
|
44
|
+
export class KpiMetricValueMutation {
|
|
45
|
+
@Directive('@transaction')
|
|
46
|
+
@Mutation(returns => KpiMetricValue, { description: 'Create a new metric value with the provided details.' })
|
|
47
|
+
async createKpiMetricValue(
|
|
48
|
+
@Arg('metricValue', { description: 'Input object containing details for the new metric value.' })
|
|
49
|
+
metricValue: NewKpiMetricValue,
|
|
50
|
+
@Ctx() context: ResolverContext
|
|
51
|
+
): Promise<KpiMetricValue> {
|
|
52
|
+
const { domain, user, tx } = context.state
|
|
53
|
+
|
|
54
|
+
let metric = metricValue.metricId
|
|
55
|
+
? await getRepository(KpiMetric).findOne({ where: { id: metricValue.metricId } })
|
|
56
|
+
: undefined
|
|
57
|
+
|
|
58
|
+
const entity: Partial<KpiMetricValue> = {
|
|
59
|
+
metric,
|
|
60
|
+
metricId: metricValue.metricId,
|
|
61
|
+
valueDate: metricValue.valueDate,
|
|
62
|
+
periodType: metricValue.periodType,
|
|
63
|
+
value: metricValue.value,
|
|
64
|
+
group: metricValue.group,
|
|
65
|
+
unit: metricValue.unit,
|
|
66
|
+
meta: metricValue.meta,
|
|
67
|
+
domain: domain,
|
|
68
|
+
creator: user,
|
|
69
|
+
updater: user
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return await getRepository(KpiMetricValue, tx).save(entity)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Directive('@transaction')
|
|
76
|
+
@Mutation(returns => KpiMetricValue, { description: 'Record a metric value by metric name, value, meta, and group.' })
|
|
77
|
+
async recordKpiMetricValue(
|
|
78
|
+
@Arg('metricName', { description: 'Metric code/name.' }) metricName: string,
|
|
79
|
+
@Arg('value', type => Float, { nullable: true, description: 'Metric value (number).' }) value: number | null,
|
|
80
|
+
@Arg('meta', type => ScalarObject, { nullable: true, description: 'Extended or non-numeric information (JSON).' })
|
|
81
|
+
meta: any,
|
|
82
|
+
@Arg('group', { nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
83
|
+
group: string | null,
|
|
84
|
+
@Ctx() context: ResolverContext
|
|
85
|
+
): Promise<KpiMetricValue> {
|
|
86
|
+
const { domain, user, tx } = context.state
|
|
87
|
+
const metric = await getRepository(KpiMetric).findOne({ where: { name: metricName, domain: { id: domain.id } } })
|
|
88
|
+
if (!metric) throw new Error(`Metric not found: ${metricName}`)
|
|
89
|
+
const periodType = (metric.periodType as unknown as KpiPeriodType) || KpiPeriodType.DAY
|
|
90
|
+
const valueDateToUse = getDefaultValueDate(periodType)
|
|
91
|
+
if (value == null && meta == null) {
|
|
92
|
+
throw new Error('Either value or meta must be provided.')
|
|
93
|
+
}
|
|
94
|
+
const entity: Partial<KpiMetricValue> = {
|
|
95
|
+
metric,
|
|
96
|
+
metricId: metric.id,
|
|
97
|
+
value,
|
|
98
|
+
meta,
|
|
99
|
+
valueDate: valueDateToUse,
|
|
100
|
+
periodType,
|
|
101
|
+
group,
|
|
102
|
+
domain,
|
|
103
|
+
creator: user,
|
|
104
|
+
updater: user
|
|
105
|
+
}
|
|
106
|
+
return await getRepository(KpiMetricValue, tx).save(entity)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Directive('@transaction')
|
|
110
|
+
@Mutation(returns => KpiMetricValue, { description: 'To modify KpiMetricValue information' })
|
|
111
|
+
async updateKpiMetricValue(
|
|
112
|
+
@Arg('id') id: string,
|
|
113
|
+
@Arg('patch') patch: KpiMetricValuePatch,
|
|
114
|
+
@Ctx() context: ResolverContext
|
|
115
|
+
): Promise<KpiMetricValue> {
|
|
116
|
+
const { domain, user, tx } = context.state
|
|
117
|
+
|
|
118
|
+
const repository = getRepository(KpiMetricValue, tx)
|
|
119
|
+
const metricValue = await repository.findOne({
|
|
120
|
+
where: { domain: { id: domain.id }, id }
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
let metric = patch.metricId
|
|
124
|
+
? await getRepository(KpiMetric).findOne({ where: { id: patch.metricId } })
|
|
125
|
+
: metricValue.metric
|
|
126
|
+
|
|
127
|
+
const entity: any = {
|
|
128
|
+
...metricValue,
|
|
129
|
+
...patch,
|
|
130
|
+
metric,
|
|
131
|
+
updater: user
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return await repository.save(entity)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Directive('@transaction')
|
|
138
|
+
@Mutation(returns => [KpiMetricValue], { description: "To modify multiple KpiMetricValues' information" })
|
|
139
|
+
async updateMultipleKpiMetricValue(
|
|
140
|
+
@Arg('patches', type => [KpiMetricValuePatch]) patches: KpiMetricValuePatch[],
|
|
141
|
+
@Ctx() context: ResolverContext
|
|
142
|
+
): Promise<KpiMetricValue[]> {
|
|
143
|
+
const { domain, user, tx } = context.state
|
|
144
|
+
|
|
145
|
+
let results = []
|
|
146
|
+
const _createRecords = patches.filter((patch: any) => patch.cuFlag && patch.cuFlag.toUpperCase() === '+')
|
|
147
|
+
const _updateRecords = patches.filter((patch: any) => patch.cuFlag && patch.cuFlag.toUpperCase() === 'M')
|
|
148
|
+
const metricValueRepo = getRepository(KpiMetricValue, tx)
|
|
149
|
+
|
|
150
|
+
if (_createRecords.length > 0) {
|
|
151
|
+
for (let i = 0; i < _createRecords.length; i++) {
|
|
152
|
+
const newRecord = _createRecords[i]
|
|
153
|
+
const result = await metricValueRepo.save({
|
|
154
|
+
...newRecord,
|
|
155
|
+
domain,
|
|
156
|
+
creator: user,
|
|
157
|
+
updater: user
|
|
158
|
+
} as any)
|
|
159
|
+
results.push({ ...result, cuFlag: '+' })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (_updateRecords.length > 0) {
|
|
164
|
+
for (let i = 0; i < _updateRecords.length; i++) {
|
|
165
|
+
const updateRecord = _updateRecords[i]
|
|
166
|
+
const metricValue = await metricValueRepo.findOneBy({ id: updateRecord.id })
|
|
167
|
+
const result = await metricValueRepo.save({
|
|
168
|
+
...metricValue,
|
|
169
|
+
...updateRecord,
|
|
170
|
+
updater: user
|
|
171
|
+
} as any)
|
|
172
|
+
results.push({ ...result, cuFlag: 'M' })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return results
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Directive('@transaction')
|
|
180
|
+
@Mutation(returns => Boolean, { description: 'To delete KpiMetricValue' })
|
|
181
|
+
async deleteKpiMetricValue(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
|
|
182
|
+
const { domain, tx } = context.state
|
|
183
|
+
await getRepository(KpiMetricValue, tx).delete({ domain: { id: domain.id }, id })
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Directive('@transaction')
|
|
188
|
+
@Mutation(returns => Boolean, { description: 'To delete multiple KpiMetricValues' })
|
|
189
|
+
async deleteKpiMetricValues(
|
|
190
|
+
@Arg('ids', type => [String]) ids: string[],
|
|
191
|
+
@Ctx() context: ResolverContext
|
|
192
|
+
): Promise<boolean> {
|
|
193
|
+
const { domain, tx } = context.state
|
|
194
|
+
await getRepository(KpiMetricValue, tx).delete({
|
|
195
|
+
domain: { id: domain.id },
|
|
196
|
+
id: In(ids)
|
|
197
|
+
})
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Directive('@transaction')
|
|
202
|
+
@Mutation(returns => Boolean, { description: 'To import multiple KpiMetricValues' })
|
|
203
|
+
async importKpiMetricValues(
|
|
204
|
+
@Arg('metricValues', type => [KpiMetricValuePatch]) metricValues: KpiMetricValuePatch[],
|
|
205
|
+
@Ctx() context: ResolverContext
|
|
206
|
+
): Promise<boolean> {
|
|
207
|
+
const { domain, tx } = context.state
|
|
208
|
+
await Promise.all(
|
|
209
|
+
metricValues.map(async (metricValue: KpiMetricValuePatch) => {
|
|
210
|
+
await getRepository(KpiMetricValue, tx).save({ domain, ...metricValue } as any)
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Resolver, Query, Args, Arg, Ctx, FieldResolver, Root } from 'type-graphql'
|
|
2
|
+
import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
|
|
3
|
+
import { User } from '@things-factory/auth-base'
|
|
4
|
+
import { KpiMetricValue } from './kpi-metric-value'
|
|
5
|
+
import { KpiMetricValueList } from './kpi-metric-value-type'
|
|
6
|
+
import { KpiMetric } from '../kpi-metric/kpi-metric'
|
|
7
|
+
|
|
8
|
+
@Resolver(KpiMetricValue)
|
|
9
|
+
export class KpiMetricValueQuery {
|
|
10
|
+
@Query(returns => KpiMetricValueList, { description: 'To fetch multiple KpiMetricValues' })
|
|
11
|
+
async kpiMetricValues(
|
|
12
|
+
@Args(type => ListParam) params: ListParam,
|
|
13
|
+
@Ctx() context: ResolverContext
|
|
14
|
+
): Promise<KpiMetricValueList> {
|
|
15
|
+
const { domain } = context.state
|
|
16
|
+
const queryBuilder = getQueryBuilderFromListParams({
|
|
17
|
+
domain,
|
|
18
|
+
params,
|
|
19
|
+
repository: await getRepository(KpiMetricValue),
|
|
20
|
+
searchables: ['metricId', 'group', 'valueDate']
|
|
21
|
+
})
|
|
22
|
+
const [items, total] = await queryBuilder.getManyAndCount()
|
|
23
|
+
return { items, total }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Query(returns => KpiMetricValue, {
|
|
27
|
+
nullable: true,
|
|
28
|
+
description: 'Fetch a single metric value by its unique identifier.'
|
|
29
|
+
})
|
|
30
|
+
async kpiMetricValue(
|
|
31
|
+
@Arg('id', { description: 'Unique identifier of the metric value to fetch.' }) id: string,
|
|
32
|
+
@Ctx() context: ResolverContext
|
|
33
|
+
): Promise<KpiMetricValue | null> {
|
|
34
|
+
const { domain } = context.state
|
|
35
|
+
return await getRepository(KpiMetricValue).findOne({
|
|
36
|
+
where: { domain: { id: domain.id }, id }
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@FieldResolver(type => Domain)
|
|
41
|
+
async domain(@Root() metricValue: KpiMetricValue): Promise<Domain> {
|
|
42
|
+
return metricValue.domainId && (await getRepository(Domain).findOneBy({ id: metricValue.domainId }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@FieldResolver(type => User)
|
|
46
|
+
async updater(@Root() metricValue: KpiMetricValue): Promise<User> {
|
|
47
|
+
return metricValue.updaterId && (await getRepository(User).findOneBy({ id: metricValue.updaterId }))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@FieldResolver(type => User)
|
|
51
|
+
async creator(@Root() metricValue: KpiMetricValue): Promise<User> {
|
|
52
|
+
return metricValue.creatorId && (await getRepository(User).findOneBy({ id: metricValue.creatorId }))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@FieldResolver(type => KpiMetric)
|
|
56
|
+
async metric(@Root() metricValue: KpiMetricValue): Promise<KpiMetric | null> {
|
|
57
|
+
if (!metricValue.metricId) return null
|
|
58
|
+
return await getRepository(KpiMetric).findOneBy({ id: metricValue.metricId })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ObjectType, Field, ID, Float, InputType, Int } from 'type-graphql'
|
|
2
|
+
import { ScalarObject } from '@things-factory/shell'
|
|
3
|
+
import { KpiMetricValue } from './kpi-metric-value'
|
|
4
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
5
|
+
|
|
6
|
+
@InputType({
|
|
7
|
+
description: 'Input type for creating a new metric value. Used in mutations to provide metric value details.'
|
|
8
|
+
})
|
|
9
|
+
export class NewKpiMetricValue {
|
|
10
|
+
@Field(type => ID, { description: 'ID of the metric to which this value belongs.' })
|
|
11
|
+
metricId: string
|
|
12
|
+
|
|
13
|
+
@Field({ description: 'Date or period for which this metric value is recorded (e.g., day, month, quarter, range).' })
|
|
14
|
+
valueDate: string
|
|
15
|
+
|
|
16
|
+
@Field(type => KpiPeriodType, { description: 'Aggregation period type for this metric value.' })
|
|
17
|
+
periodType: KpiPeriodType
|
|
18
|
+
|
|
19
|
+
@Field(type => Float, { description: 'The value for this metric and period.' })
|
|
20
|
+
value: number
|
|
21
|
+
|
|
22
|
+
@Field({ nullable: true, description: 'Unit of measurement for this metric value.' })
|
|
23
|
+
unit?: string
|
|
24
|
+
|
|
25
|
+
@Field(type => ScalarObject, {
|
|
26
|
+
nullable: true,
|
|
27
|
+
description:
|
|
28
|
+
'Extended or non-numeric information related to this metric value, stored as JSON. Can include status, comments, or other metadata.'
|
|
29
|
+
})
|
|
30
|
+
meta?: any
|
|
31
|
+
|
|
32
|
+
@Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
33
|
+
group?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@InputType({
|
|
37
|
+
description: 'Input type for updating an existing metric value. Used in mutations to patch metric value details.'
|
|
38
|
+
})
|
|
39
|
+
export class KpiMetricValuePatch {
|
|
40
|
+
@Field(type => ID, { nullable: true, description: 'ID of the metric value to update.' })
|
|
41
|
+
id?: string
|
|
42
|
+
|
|
43
|
+
@Field(type => ID, { nullable: true, description: 'ID of the metric to which this value belongs.' })
|
|
44
|
+
metricId?: string
|
|
45
|
+
|
|
46
|
+
@Field({
|
|
47
|
+
nullable: true,
|
|
48
|
+
description: 'Date or period for which this metric value is recorded (e.g., day, month, quarter, range).'
|
|
49
|
+
})
|
|
50
|
+
valueDate?: string
|
|
51
|
+
|
|
52
|
+
@Field(type => KpiPeriodType, { nullable: true, description: 'Aggregation period type for this metric value.' })
|
|
53
|
+
periodType?: KpiPeriodType
|
|
54
|
+
|
|
55
|
+
@Field(type => Float, { nullable: true, description: 'The value for this metric and period.' })
|
|
56
|
+
value?: number
|
|
57
|
+
|
|
58
|
+
@Field({ nullable: true, description: 'Unit of measurement for this metric value.' })
|
|
59
|
+
unit?: string
|
|
60
|
+
|
|
61
|
+
@Field(type => ScalarObject, {
|
|
62
|
+
nullable: true,
|
|
63
|
+
description:
|
|
64
|
+
'Extended or non-numeric information related to this metric value, stored as JSON. Can include status, comments, or other metadata.'
|
|
65
|
+
})
|
|
66
|
+
meta?: any
|
|
67
|
+
|
|
68
|
+
@Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
69
|
+
group?: string
|
|
70
|
+
|
|
71
|
+
@Field({ nullable: true, description: 'Custom flag for update operations (internal use).' })
|
|
72
|
+
cuFlag?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ObjectType()
|
|
76
|
+
export class KpiMetricValueList {
|
|
77
|
+
@Field(type => [KpiMetricValue])
|
|
78
|
+
items: KpiMetricValue[]
|
|
79
|
+
|
|
80
|
+
@Field(type => Int)
|
|
81
|
+
total: number
|
|
82
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Entity,
|
|
3
|
+
PrimaryGeneratedColumn,
|
|
4
|
+
Column,
|
|
5
|
+
ManyToOne,
|
|
6
|
+
JoinColumn,
|
|
7
|
+
CreateDateColumn,
|
|
8
|
+
UpdateDateColumn,
|
|
9
|
+
RelationId
|
|
10
|
+
} from 'typeorm'
|
|
11
|
+
import { ObjectType, Field, ID, Float } from 'type-graphql'
|
|
12
|
+
import { Domain, ScalarObject } from '@things-factory/shell'
|
|
13
|
+
import { KpiMetric } from '../kpi-metric/kpi-metric'
|
|
14
|
+
import { KpiPeriodType } from '../kpi/kpi'
|
|
15
|
+
import { User } from '@things-factory/auth-base'
|
|
16
|
+
|
|
17
|
+
@Entity()
|
|
18
|
+
@ObjectType({ description: 'Current value for each KPI metric (can be used for both state and history).' })
|
|
19
|
+
export class KpiMetricValue {
|
|
20
|
+
@PrimaryGeneratedColumn('uuid')
|
|
21
|
+
@Field(type => ID)
|
|
22
|
+
id: string
|
|
23
|
+
|
|
24
|
+
@ManyToOne(() => Domain)
|
|
25
|
+
@Field(type => Domain, { nullable: true })
|
|
26
|
+
domain?: Domain
|
|
27
|
+
|
|
28
|
+
@RelationId((entity: KpiMetricValue) => entity.domain)
|
|
29
|
+
@Field({ nullable: true })
|
|
30
|
+
domainId?: string
|
|
31
|
+
|
|
32
|
+
@ManyToOne(() => KpiMetric, { nullable: false, onDelete: 'CASCADE' })
|
|
33
|
+
@JoinColumn({ name: 'metricId' })
|
|
34
|
+
@Field(type => KpiMetric)
|
|
35
|
+
metric: KpiMetric
|
|
36
|
+
|
|
37
|
+
@RelationId((entity: KpiMetricValue) => entity.metric)
|
|
38
|
+
@Field()
|
|
39
|
+
metricId: string
|
|
40
|
+
|
|
41
|
+
@Column('float')
|
|
42
|
+
@Field(type => Float)
|
|
43
|
+
value: number
|
|
44
|
+
|
|
45
|
+
@Column({ nullable: true })
|
|
46
|
+
@Field({ nullable: true })
|
|
47
|
+
unit?: string
|
|
48
|
+
|
|
49
|
+
@Column({ type: 'varchar', length: 20 })
|
|
50
|
+
@Field({
|
|
51
|
+
description:
|
|
52
|
+
'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).'
|
|
53
|
+
})
|
|
54
|
+
valueDate: string
|
|
55
|
+
|
|
56
|
+
@Column({ default: 'DAY' })
|
|
57
|
+
@Field(type => KpiPeriodType)
|
|
58
|
+
periodType: KpiPeriodType
|
|
59
|
+
|
|
60
|
+
@Column({ nullable: true })
|
|
61
|
+
@Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
62
|
+
group?: string
|
|
63
|
+
|
|
64
|
+
@Column({ type: 'simple-json', nullable: true })
|
|
65
|
+
@Field(type => ScalarObject, { nullable: true })
|
|
66
|
+
meta?: any
|
|
67
|
+
|
|
68
|
+
@CreateDateColumn()
|
|
69
|
+
@Field({ nullable: true })
|
|
70
|
+
createdAt?: Date
|
|
71
|
+
|
|
72
|
+
@UpdateDateColumn()
|
|
73
|
+
@Field({ nullable: true })
|
|
74
|
+
updatedAt?: Date
|
|
75
|
+
|
|
76
|
+
@ManyToOne(() => User, { nullable: true })
|
|
77
|
+
@Field(type => User, { nullable: true })
|
|
78
|
+
creator?: User
|
|
79
|
+
|
|
80
|
+
@RelationId((entity: KpiMetricValue) => entity.creator)
|
|
81
|
+
@Field({ nullable: true })
|
|
82
|
+
creatorId?: string
|
|
83
|
+
|
|
84
|
+
@ManyToOne(() => User, { nullable: true })
|
|
85
|
+
@Field(type => User, { nullable: true })
|
|
86
|
+
updater?: User
|
|
87
|
+
|
|
88
|
+
@RelationId((entity: KpiMetricValue) => entity.updater)
|
|
89
|
+
@Field({ nullable: true })
|
|
90
|
+
updaterId?: string
|
|
91
|
+
}
|
|
@@ -35,8 +35,7 @@ export class KpiValueMutation {
|
|
|
35
35
|
version: kpiValue.version,
|
|
36
36
|
valueDate: kpiValue.valueDate,
|
|
37
37
|
value: kpiValue.value,
|
|
38
|
-
|
|
39
|
-
groupType: kpiValue.groupType,
|
|
38
|
+
group: kpiValue.group,
|
|
40
39
|
source: kpiValue.source,
|
|
41
40
|
domain: domain,
|
|
42
41
|
creator: user,
|
|
@@ -27,14 +27,8 @@ export class NewKpiValue {
|
|
|
27
27
|
})
|
|
28
28
|
meta?: any
|
|
29
29
|
|
|
30
|
-
@Field({
|
|
31
|
-
|
|
32
|
-
description: 'ID of the group (organization, department, project, etc.) this value is associated with.'
|
|
33
|
-
})
|
|
34
|
-
groupId?: string
|
|
35
|
-
|
|
36
|
-
@Field({ nullable: true, description: 'Type of the group (e.g., USER, DEPT, PROJECT) for this value.' })
|
|
37
|
-
groupType?: string
|
|
30
|
+
@Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
31
|
+
group?: string
|
|
38
32
|
|
|
39
33
|
@Field(type => KpiValueInputType, {
|
|
40
34
|
nullable: true,
|
|
@@ -78,14 +72,8 @@ export class KpiValuePatch {
|
|
|
78
72
|
})
|
|
79
73
|
meta?: any
|
|
80
74
|
|
|
81
|
-
@Field({
|
|
82
|
-
|
|
83
|
-
description: 'ID of the group (organization, department, project, etc.) this value is associated with.'
|
|
84
|
-
})
|
|
85
|
-
groupId?: string
|
|
86
|
-
|
|
87
|
-
@Field({ nullable: true, description: 'Type of the group (e.g., USER, DEPT, PROJECT) for this value.' })
|
|
88
|
-
groupType?: string
|
|
75
|
+
@Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
76
|
+
group?: string
|
|
89
77
|
|
|
90
78
|
@Field(type => KpiValueInputType, {
|
|
91
79
|
nullable: true,
|
|
@@ -13,7 +13,7 @@ import { ObjectType, Field, Int, ID, registerEnumType, Float } from 'type-graphq
|
|
|
13
13
|
import { User } from '@things-factory/auth-base'
|
|
14
14
|
import { Domain, ScalarObject, json5Transformer } from '@things-factory/shell'
|
|
15
15
|
import { config } from '@things-factory/env'
|
|
16
|
-
import { Kpi } from '../kpi/kpi'
|
|
16
|
+
import { Kpi, KpiPeriodType } from '../kpi/kpi'
|
|
17
17
|
|
|
18
18
|
const ORMCONFIG = config.get('ormconfig', {})
|
|
19
19
|
const DATABASE_TYPE = ORMCONFIG.type
|
|
@@ -29,8 +29,8 @@ registerEnumType(KpiValueInputType, {
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
@Entity()
|
|
32
|
-
@Index('ix_kpi_value_latest', ['domain', 'kpi', 'valueDate', '
|
|
33
|
-
@Index('ix_kpi_value_latest_for_query', ['domain', 'kpi', 'valueDate', '
|
|
32
|
+
@Index('ix_kpi_value_latest', ['domain', 'kpi', 'valueDate', 'group', 'version'], { unique: true })
|
|
33
|
+
@Index('ix_kpi_value_latest_for_query', ['domain', 'kpi', 'valueDate', 'group'], { unique: false })
|
|
34
34
|
@ObjectType({ description: 'Entity for KpiValue' })
|
|
35
35
|
export class KpiValue {
|
|
36
36
|
@PrimaryGeneratedColumn('uuid')
|
|
@@ -60,8 +60,11 @@ export class KpiValue {
|
|
|
60
60
|
@Field(type => Int, { description: 'Version of the KPI definition at the time this value was calculated.' })
|
|
61
61
|
version: number
|
|
62
62
|
|
|
63
|
-
@Column(
|
|
64
|
-
@Field({
|
|
63
|
+
@Column()
|
|
64
|
+
@Field({
|
|
65
|
+
description:
|
|
66
|
+
'Date or period for which this KPI value is recorded (e.g., day: YYYY-MM-DD, month: YYYY-MM, quarter: YYYY-Qn, range: YYYY-MM-DD~YYYY-MM-DD).'
|
|
67
|
+
})
|
|
65
68
|
valueDate: string
|
|
66
69
|
|
|
67
70
|
@Column('float')
|
|
@@ -69,15 +72,8 @@ export class KpiValue {
|
|
|
69
72
|
value: number
|
|
70
73
|
|
|
71
74
|
@Column({ nullable: true })
|
|
72
|
-
@Field({
|
|
73
|
-
|
|
74
|
-
description: 'ID of the group (organization, department, project, etc.) this value is associated with.'
|
|
75
|
-
})
|
|
76
|
-
groupId?: string
|
|
77
|
-
|
|
78
|
-
@Column({ nullable: true })
|
|
79
|
-
@Field({ nullable: true, description: 'Type of the group (e.g., USER, DEPT, PROJECT) for this value.' })
|
|
80
|
-
groupType?: string
|
|
75
|
+
@Field({ nullable: true, description: 'Group key for this value (organization, line, user, etc.)' })
|
|
76
|
+
group?: string
|
|
81
77
|
|
|
82
78
|
@Column({ nullable: true })
|
|
83
79
|
@Field({
|
|
@@ -110,6 +106,10 @@ export class KpiValue {
|
|
|
110
106
|
})
|
|
111
107
|
meta?: any
|
|
112
108
|
|
|
109
|
+
@Column({ default: 'DAY' })
|
|
110
|
+
@Field(type => KpiPeriodType, { description: 'Aggregation period type for this KPI value.' })
|
|
111
|
+
periodType: KpiPeriodType
|
|
112
|
+
|
|
113
113
|
@CreateDateColumn()
|
|
114
114
|
@Field({ nullable: true, description: 'Timestamp when this KPI value record was created.' })
|
|
115
115
|
createdAt?: Date
|
package/things-factory.config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import route from './dist-client/route'
|
|
2
|
-
|
|
2
|
+
import bootstrap from './dist-client/bootstrap'
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
route,
|
|
@@ -10,8 +10,10 @@ export default {
|
|
|
10
10
|
{ tagname: 'kpi-category-list-page', page: 'kpi-category-list' },
|
|
11
11
|
{ tagname: 'kpi-metric-list-page', page: 'kpi-metric-list' },
|
|
12
12
|
{ tagname: 'kpi-value-list-page', page: 'kpi-value-list' },
|
|
13
|
+
{ tagname: 'kpi-metric-value-list-page', page: 'kpi-metric-value-list' },
|
|
13
14
|
{ tagname: 'kpi-value-manual-entry-page', page: 'kpi-value-manual-entry' },
|
|
15
|
+
{ tagname: 'kpi-metric-value-manual-entry-page', page: 'kpi-metric-value-manual-entry' },
|
|
14
16
|
{ tagname: 'kpi-history-list-page', page: 'kpi-history-list' }
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
+
],
|
|
18
|
+
bootstrap
|
|
17
19
|
}
|
package/translations/en.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"label.formula": "formula",
|
|
3
|
+
"label.available-variables": "available variables",
|
|
4
|
+
"label.operators": "operators",
|
|
5
|
+
"label.functions": "functions",
|
|
2
6
|
"title.kpi list": "KPI List",
|
|
3
7
|
"title.kpi category list": "KPI Catogory List",
|
|
4
8
|
"title.kpi grade list": "KPI Grade List",
|
|
5
9
|
"title.kpi metric list": "KPI Metric List",
|
|
6
10
|
"title.kpi value list": "KPI Value List",
|
|
11
|
+
"title.kpi metric value list": "KPI Metric Value List",
|
|
7
12
|
"title.kpi performance board": "KPI Performance Board",
|
|
8
13
|
"title.kpi management center": "KPI Management Center",
|
|
9
14
|
"title.kpi overview": "KPI Overview",
|
|
10
15
|
"title.kpi value manual entry": "KPI Value Manual Entry",
|
|
11
|
-
"
|
|
16
|
+
"title.kpi metric value manual entry": "KPI Metric Value Manual Entry",
|
|
17
|
+
"button.enter-kpi-value": "Enter KPI Value",
|
|
18
|
+
"button.enter-kpi-metric-value": "Enter KPI Metric Value"
|
|
12
19
|
}
|
package/translations/ja.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"label.formula": "산식",
|
|
3
|
+
"label.available-variables": "사용 가능한 변수",
|
|
4
|
+
"label.operators": "연산자",
|
|
5
|
+
"label.functions": "함수",
|
|
2
6
|
"title.kpi list": "KPI リスト",
|
|
3
7
|
"title.kpi category list": "KPI カテゴリ リスト",
|
|
4
8
|
"title.kpi grade list": "KPI グレード リスト",
|
|
5
9
|
"title.kpi metric list": "KPI メトリック リスト",
|
|
6
10
|
"title.kpi value list": "KPI 値 リスト",
|
|
11
|
+
"title.kpi metric value list": "KPI メトリック 値 リスト",
|
|
7
12
|
"title.kpi performance board": "KPI パフォーマンス ボード",
|
|
8
13
|
"title.kpi management center": "KPI 管理 センター",
|
|
9
14
|
"title.kpi overview": "KPI 概要",
|
|
10
15
|
"title.kpi value manual entry": "KPI 値 手入力",
|
|
11
|
-
"
|
|
16
|
+
"title.kpi metric value manual entry": "KPI メトリック 値 手入力",
|
|
17
|
+
"button.enter-kpi-value": "KPI 値 入力",
|
|
18
|
+
"button.enter-kpi-metric-value": "KPI メトリック 値 入力"
|
|
12
19
|
}
|
package/translations/ko.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"label.formula": "산식",
|
|
3
|
+
"label.available-variables": "사용 가능한 변수",
|
|
4
|
+
"label.operators": "연산자",
|
|
5
|
+
"label.functions": "함수",
|
|
2
6
|
"title.kpi list": "KPI 목록",
|
|
3
7
|
"title.kpi category list": "KPI 카테고리 목록",
|
|
4
8
|
"title.kpi grade list": "KPI 등급 목록",
|
|
5
|
-
"title.kpi metric list": "KPI
|
|
9
|
+
"title.kpi metric list": "KPI 메트릭 목록",
|
|
6
10
|
"title.kpi value list": "KPI 값 목록",
|
|
11
|
+
"title.kpi metric value list": "KPI 메트릭 값 목록",
|
|
7
12
|
"title.kpi performance board": "KPI 성과 보드",
|
|
8
13
|
"title.kpi management center": "KPI 관리 센터",
|
|
9
14
|
"title.kpi overview": "KPI 개요",
|
|
10
15
|
"title.kpi value manual entry": "KPI 값 수기 입력",
|
|
11
|
-
"
|
|
16
|
+
"title.kpi metric value manual entry": "KPI 메트릭 값 수기 입력",
|
|
17
|
+
"button.enter-kpi-value": "KPI 값 입력",
|
|
18
|
+
"button.enter-kpi-metric-value": "KPI 메트릭 값 입력"
|
|
12
19
|
}
|
package/translations/ms.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"label.formula": "formula",
|
|
3
|
+
"label.available-variables": "available variables",
|
|
4
|
+
"label.operators": "operators",
|
|
5
|
+
"label.functions": "functions",
|
|
2
6
|
"title.kpi list": "Senarai KPI",
|
|
3
7
|
"title.kpi category list": "Senarai Kategori KPI",
|
|
4
8
|
"title.kpi grade list": "Senarai Gred KPI",
|
|
5
9
|
"title.kpi metric list": "Senarai Metrik KPI",
|
|
6
10
|
"title.kpi value list": "Senarai Nilai KPI",
|
|
11
|
+
"title.kpi metric value list": "Senarai Metrik KPI",
|
|
7
12
|
"title.kpi performance board": "Papan KPI",
|
|
8
13
|
"title.kpi management center": "Pusat Pengurusan KPI",
|
|
9
14
|
"title.kpi overview": "Pengenalan KPI",
|
|
10
|
-
"title.kpi value manual entry": "KPI
|
|
11
|
-
"
|
|
15
|
+
"title.kpi value manual entry": "KPI Nilai Manual",
|
|
16
|
+
"title.kpi metric value manual entry": "KPI Metrik Manual",
|
|
17
|
+
"button.enter-kpi-value": "KPI Nilai Manual",
|
|
18
|
+
"button.enter-kpi-metric-value": "KPI Metrik Manual"
|
|
12
19
|
}
|