@things-factory/kpi 9.0.16 → 9.0.17
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/pages/kpi/kpi-grade-editor.ts +80 -207
- package/client/pages/kpi/kpi-list-page.ts +6 -40
- package/dist-client/pages/kpi/kpi-grade-editor.d.ts +13 -13
- package/dist-client/pages/kpi/kpi-grade-editor.js +84 -197
- package/dist-client/pages/kpi/kpi-grade-editor.js.map +1 -1
- package/dist-client/pages/kpi/kpi-list-page.d.ts +0 -1
- package/dist-client/pages/kpi/kpi-list-page.js +4 -34
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import '@material/web/button/elevated-button.js'
|
|
2
2
|
import '@material/web/button/filled-button.js'
|
|
3
3
|
import '@material/web/button/text-button.js'
|
|
4
|
-
import '@material/web/textfield/outlined-text-field.js'
|
|
5
|
-
import '@material/web/select/outlined-select.js'
|
|
6
|
-
import '@material/web/select/select-option.js'
|
|
7
4
|
import '@material/web/icon/icon.js'
|
|
5
|
+
import '@operato/data-grist/ox-grist.js'
|
|
6
|
+
import deepEquals from 'lodash-es/isEqual'
|
|
8
7
|
|
|
8
|
+
import gql from 'graphql-tag'
|
|
9
9
|
import { LitElement, css, html } from 'lit'
|
|
10
|
-
import { customElement, property, state } from 'lit/decorators.js'
|
|
11
|
-
import {
|
|
10
|
+
import { customElement, property, state, query } from 'lit/decorators.js'
|
|
11
|
+
import { DataGrist } from '@operato/data-grist/ox-grist.js'
|
|
12
|
+
import { i18next, localize } from '@operato/i18n'
|
|
12
13
|
import { client } from '@operato/graphql'
|
|
13
14
|
import { notify } from '@operato/layout'
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
15
|
+
import { FetchOption } from '@operato/data-grist'
|
|
16
|
+
import { CommonHeaderStyles } from '@operato/styles'
|
|
16
17
|
|
|
17
|
-
// KPI 등급 타입 정의 (서버와 동일한 구조)
|
|
18
18
|
interface KpiGrade {
|
|
19
19
|
name: string
|
|
20
20
|
minValue: number
|
|
@@ -27,268 +27,141 @@ interface KpiGrade {
|
|
|
27
27
|
type KpiGrades = KpiGrade[]
|
|
28
28
|
|
|
29
29
|
@customElement('kpi-grade-editor')
|
|
30
|
-
export class KpiGradeEditor extends
|
|
30
|
+
export class KpiGradeEditor extends localize(i18next)(LitElement) {
|
|
31
31
|
static styles = [
|
|
32
32
|
CommonHeaderStyles,
|
|
33
|
-
ScrollbarStyles,
|
|
34
33
|
css`
|
|
35
34
|
:host {
|
|
36
35
|
display: flex;
|
|
37
36
|
flex-direction: column;
|
|
38
|
-
background-color: var(--md-sys-color-surface, #f4f6fa);
|
|
39
|
-
}
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
flex: 1;
|
|
43
|
-
margin-bottom: 20px;
|
|
44
|
-
overflow-y: auto;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.grade-item {
|
|
48
|
-
display: flex;
|
|
49
|
-
align-items: center;
|
|
50
|
-
gap: 10px;
|
|
51
|
-
padding: 10px;
|
|
52
|
-
border: 1px solid #ddd;
|
|
53
|
-
border-radius: 4px;
|
|
54
|
-
margin-bottom: 10px;
|
|
55
|
-
background: #f9f9f9;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.grade-item:hover {
|
|
59
|
-
background: #f0f0f0;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.grade-inputs {
|
|
63
|
-
display: flex;
|
|
64
|
-
gap: 10px;
|
|
65
|
-
flex: 1;
|
|
38
|
+
background-color: var(--md-sys-color-surface);
|
|
66
39
|
}
|
|
67
40
|
|
|
68
|
-
|
|
41
|
+
ox-grist {
|
|
69
42
|
flex: 1;
|
|
70
43
|
}
|
|
71
|
-
|
|
72
|
-
.grade-actions {
|
|
73
|
-
display: flex;
|
|
74
|
-
gap: 5px;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.footer span {
|
|
78
|
-
font-size: 0.8em;
|
|
79
|
-
color: var(--md-sys-color-on-surface);
|
|
80
|
-
line-height: 1.5;
|
|
81
|
-
padding: 10px;
|
|
82
|
-
}
|
|
83
44
|
`
|
|
84
45
|
]
|
|
85
46
|
|
|
86
|
-
@property({ type: Object }) kpi: any
|
|
87
|
-
|
|
88
|
-
@state()
|
|
47
|
+
@property({ type: Object }) kpi: any = { grades: [] }
|
|
48
|
+
|
|
49
|
+
@state() grades: KpiGrades = this.kpi?.grades || []
|
|
50
|
+
@state() gristConfig: any = null
|
|
51
|
+
@query('ox-grist') grist!: DataGrist
|
|
89
52
|
|
|
90
|
-
|
|
91
|
-
super.connectedCallback()
|
|
53
|
+
async firstUpdated() {
|
|
92
54
|
if (this.kpi?.grades) {
|
|
93
55
|
this.grades = [...this.kpi.grades]
|
|
94
56
|
}
|
|
57
|
+
|
|
58
|
+
this.gristConfig = {
|
|
59
|
+
list: { fields: ['name', 'minValue', 'maxValue', 'score', 'color', 'description'] },
|
|
60
|
+
columns: [
|
|
61
|
+
{ type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true },
|
|
62
|
+
{ type: 'gutter', gutterName: 'sequence', fixed: true },
|
|
63
|
+
{ type: 'gutter', gutterName: 'button', fixed: true, icon: 'add', handlers: { click: 'record-copy' } },
|
|
64
|
+
{ type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_upward', handlers: { click: 'move-up' } },
|
|
65
|
+
{ type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_downward', handlers: { click: 'move-down' } },
|
|
66
|
+
{ type: 'string', name: 'name', header: '등급명', record: { editable: true }, width: 100 },
|
|
67
|
+
{ type: 'number', name: 'minValue', header: '최소값', record: { editable: true }, width: 100 },
|
|
68
|
+
{ type: 'number', name: 'maxValue', header: '최대값', record: { editable: true }, width: 100 },
|
|
69
|
+
{ type: 'number', name: 'score', header: '점수', record: { editable: true }, width: 80 },
|
|
70
|
+
{ type: 'color', name: 'color', header: '색상', record: { editable: true }, width: 100 },
|
|
71
|
+
{ type: 'string', name: 'description', header: '설명', record: { editable: true }, width: 200 }
|
|
72
|
+
],
|
|
73
|
+
rows: { selectable: { multiple: true } },
|
|
74
|
+
pagination: { infinite: true }
|
|
75
|
+
}
|
|
95
76
|
}
|
|
96
77
|
|
|
97
78
|
render() {
|
|
98
79
|
return html`
|
|
99
|
-
<
|
|
100
|
-
|
|
80
|
+
<ox-grist .mode=${'GRID'} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}></ox-grist>
|
|
101
81
|
<div class="footer">
|
|
102
|
-
<button type="button" @click=${this._addGrade}><md-icon>add</md-icon>등급 추가</button>
|
|
103
82
|
<div filler></div>
|
|
104
|
-
<button
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
_renderGradeItem(grade: KpiGrade, index: number) {
|
|
111
|
-
return html`
|
|
112
|
-
<div class="grade-item">
|
|
113
|
-
<div class="grade-inputs">
|
|
114
|
-
<md-outlined-text-field
|
|
115
|
-
label="등급명"
|
|
116
|
-
value=${grade.name}
|
|
117
|
-
@input=${(e: any) => this._updateGrade(index, 'name', e.target.value)}
|
|
118
|
-
></md-outlined-text-field>
|
|
119
|
-
|
|
120
|
-
<md-outlined-text-field
|
|
121
|
-
label="최소값"
|
|
122
|
-
type="number"
|
|
123
|
-
value=${grade.minValue}
|
|
124
|
-
@input=${(e: any) => this._updateGrade(index, 'minValue', parseFloat(e.target.value))}
|
|
125
|
-
></md-outlined-text-field>
|
|
126
|
-
|
|
127
|
-
<md-outlined-text-field
|
|
128
|
-
label="최대값"
|
|
129
|
-
type="number"
|
|
130
|
-
value=${grade.maxValue}
|
|
131
|
-
@input=${(e: any) => this._updateGrade(index, 'maxValue', parseFloat(e.target.value))}
|
|
132
|
-
></md-outlined-text-field>
|
|
133
|
-
|
|
134
|
-
<md-outlined-text-field
|
|
135
|
-
label="점수"
|
|
136
|
-
type="number"
|
|
137
|
-
value=${grade.score || ''}
|
|
138
|
-
@input=${(e: any) => this._updateGrade(index, 'score', parseFloat(e.target.value))}
|
|
139
|
-
></md-outlined-text-field>
|
|
140
|
-
|
|
141
|
-
<md-outlined-text-field
|
|
142
|
-
label="색상"
|
|
143
|
-
value=${grade.color || ''}
|
|
144
|
-
@input=${(e: any) => this._updateGrade(index, 'color', e.target.value)}
|
|
145
|
-
></md-outlined-text-field>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<div class="grade-actions">
|
|
149
|
-
<md-icon-button @click=${() => this._removeGrade(index)}>
|
|
150
|
-
<md-icon>delete</md-icon>
|
|
151
|
-
</md-icon-button>
|
|
152
|
-
</div>
|
|
83
|
+
<button danger @click=${this._deleteGrades.bind(this)}>
|
|
84
|
+
<md-icon>delete</md-icon>${i18next.t('button.delete')}
|
|
85
|
+
</button>
|
|
86
|
+
<button done type="button" @click=${this._updateGrades}><md-icon>save</md-icon>저장</button>
|
|
153
87
|
</div>
|
|
154
88
|
`
|
|
155
89
|
}
|
|
156
90
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
_addGrade() {
|
|
164
|
-
const newGrade: KpiGrade = {
|
|
165
|
-
name: '',
|
|
166
|
-
minValue: 0,
|
|
167
|
-
maxValue: 0,
|
|
168
|
-
score: 0,
|
|
169
|
-
color: '#4caf50',
|
|
170
|
-
description: ''
|
|
91
|
+
async fetchHandler({ page, limit, sorters = [] }: FetchOption) {
|
|
92
|
+
return {
|
|
93
|
+
total: this.grades.length,
|
|
94
|
+
records: this.grades
|
|
171
95
|
}
|
|
172
|
-
this.grades.push(newGrade)
|
|
173
|
-
this.isDirty = true
|
|
174
|
-
this.requestUpdate()
|
|
175
96
|
}
|
|
176
97
|
|
|
177
|
-
|
|
178
|
-
this.grades.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
98
|
+
async _updateGrades() {
|
|
99
|
+
this.grades = this.grist.dirtyData.records
|
|
100
|
+
.map(patch => {
|
|
101
|
+
const { name, minValue, maxValue, score, color, description } = patch
|
|
102
|
+
return { name, minValue, maxValue, score, color, description }
|
|
103
|
+
})
|
|
104
|
+
.sort((a, b) => a.minValue - b.minValue) as any
|
|
182
105
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
case '5grade':
|
|
186
|
-
this.grades = [
|
|
187
|
-
{ name: 'A', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '우수' },
|
|
188
|
-
{ name: 'B', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '양호' },
|
|
189
|
-
{ name: 'C', minValue: 2.5, maxValue: 3.5, score: 75, color: '#ffc107', description: '보통' },
|
|
190
|
-
{ name: 'D', minValue: 3.5, maxValue: 4.5, score: 65, color: '#ff9800', description: '미흡' },
|
|
191
|
-
{ name: 'E', minValue: 4.5, maxValue: 999, score: 55, color: '#f44336', description: '불량' }
|
|
192
|
-
]
|
|
193
|
-
break
|
|
194
|
-
case '3grade':
|
|
195
|
-
this.grades = [
|
|
196
|
-
{ name: '우수', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '목표 달성' },
|
|
197
|
-
{ name: '양호', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '기준 달성' },
|
|
198
|
-
{ name: '미흡', minValue: 2.5, maxValue: 999, score: 75, color: '#f44336', description: '개선 필요' }
|
|
199
|
-
]
|
|
200
|
-
break
|
|
201
|
-
case 'continuous':
|
|
202
|
-
this.grades = [
|
|
203
|
-
{ name: '0.999999', minValue: 0, maxValue: 0.025, score: 0.999999, color: '#4caf50' },
|
|
204
|
-
{ name: '0.944189368', minValue: 0.025, maxValue: 0.05, score: 0.944189368, color: '#4caf50' },
|
|
205
|
-
{ name: '0.888379735', minValue: 0.05, maxValue: 0.075, score: 0.888379735, color: '#4caf50' }
|
|
206
|
-
]
|
|
207
|
-
break
|
|
208
|
-
case 'clear':
|
|
209
|
-
this.grades = []
|
|
210
|
-
break
|
|
106
|
+
if (!this._validateGrades()) {
|
|
107
|
+
return
|
|
211
108
|
}
|
|
212
|
-
this.isDirty = true
|
|
213
|
-
this.requestUpdate()
|
|
214
|
-
}
|
|
215
109
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
grades: this.grades
|
|
110
|
+
if (!deepEquals(this.kpi?.grades, this.grades)) {
|
|
111
|
+
try {
|
|
112
|
+
const response = await client.mutate({
|
|
113
|
+
mutation: gql`
|
|
114
|
+
mutation ($id: String!, $patch: KpiPatch!) {
|
|
115
|
+
updateKpi(id: $id, patch: $patch) {
|
|
116
|
+
id
|
|
117
|
+
name
|
|
118
|
+
grades
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
variables: {
|
|
123
|
+
id: this.kpi.id,
|
|
124
|
+
patch: { grades: this.grades }
|
|
231
125
|
}
|
|
232
126
|
})
|
|
233
|
-
)
|
|
234
127
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
message: '등급 저장 중 오류가 발생했습니다.'
|
|
239
|
-
}
|
|
128
|
+
this.grades = response.data.updateKpi.grades
|
|
129
|
+
this.grist.fetch()
|
|
130
|
+
} catch (error) {
|
|
131
|
+
notify({ message: '등급 저장 중 오류가 발생했습니다.' })
|
|
132
|
+
}
|
|
240
133
|
}
|
|
241
134
|
}
|
|
242
135
|
|
|
243
136
|
_validateGrades(): boolean {
|
|
244
|
-
// 최소 1개 등급 필요
|
|
245
137
|
if (this.grades.length === 0) {
|
|
246
|
-
notify({
|
|
247
|
-
message: '최소 1개 이상의 등급을 설정해야 합니다.'
|
|
248
|
-
})
|
|
138
|
+
notify({ message: '최소 1개 이상의 등급을 설정해야 합니다.' })
|
|
249
139
|
return false
|
|
250
140
|
}
|
|
251
|
-
|
|
252
|
-
// 등급명 중복 체크
|
|
253
141
|
const names = this.grades.map(g => g.name)
|
|
254
142
|
const uniqueNames = new Set(names)
|
|
255
143
|
if (names.length !== uniqueNames.size) {
|
|
256
|
-
notify({
|
|
257
|
-
message: '등급명이 중복되었습니다.'
|
|
258
|
-
})
|
|
144
|
+
notify({ message: '등급명이 중복되었습니다.' })
|
|
259
145
|
return false
|
|
260
146
|
}
|
|
261
|
-
|
|
262
|
-
// 값 범위 체크
|
|
263
147
|
for (let i = 0; i < this.grades.length; i++) {
|
|
264
148
|
const grade = this.grades[i]
|
|
265
149
|
if (grade.minValue >= grade.maxValue) {
|
|
266
|
-
notify({
|
|
267
|
-
message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.`
|
|
268
|
-
})
|
|
150
|
+
notify({ message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.` })
|
|
269
151
|
return false
|
|
270
152
|
}
|
|
271
|
-
|
|
272
|
-
// 연속성 체크
|
|
273
153
|
if (i > 0) {
|
|
274
154
|
const prevGrade = this.grades[i - 1]
|
|
275
155
|
if (prevGrade.maxValue !== grade.minValue) {
|
|
276
|
-
notify({
|
|
277
|
-
message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.`
|
|
278
|
-
})
|
|
156
|
+
notify({ message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.` })
|
|
279
157
|
return false
|
|
280
158
|
}
|
|
281
159
|
}
|
|
282
160
|
}
|
|
283
|
-
|
|
284
161
|
return true
|
|
285
162
|
}
|
|
286
163
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const popup = this.closest('ox-popup') as any
|
|
290
|
-
if (popup && popup.close) {
|
|
291
|
-
popup.close()
|
|
292
|
-
}
|
|
164
|
+
async _deleteGrades() {
|
|
165
|
+
this.grist.deleteSelectedRecords(true)
|
|
293
166
|
}
|
|
294
167
|
}
|
|
@@ -503,47 +503,13 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
async _editGrades(kpi: any) {
|
|
506
|
-
const popup = await openPopup(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
{
|
|
511
|
-
title: `${kpi.name} - 등급 설정`,
|
|
512
|
-
size: 'large'
|
|
513
|
-
}
|
|
514
|
-
)
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
async _onGradesUpdated(detail: any) {
|
|
518
|
-
const { kpiId, grades } = detail
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
const response = await client.mutate({
|
|
522
|
-
mutation: gql`
|
|
523
|
-
mutation ($id: String!, $patch: KpiPatch!) {
|
|
524
|
-
updateKpi(id: $id, patch: $patch) {
|
|
525
|
-
id
|
|
526
|
-
name
|
|
527
|
-
grades
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
`,
|
|
531
|
-
variables: {
|
|
532
|
-
id: kpiId,
|
|
533
|
-
patch: { grades }
|
|
534
|
-
}
|
|
535
|
-
})
|
|
506
|
+
const popup = await openPopup(html` <kpi-grade-editor .kpi=${kpi}></kpi-grade-editor> `, {
|
|
507
|
+
title: `${kpi.name} - 등급 설정`,
|
|
508
|
+
size: 'large'
|
|
509
|
+
})
|
|
536
510
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
notify({
|
|
540
|
-
message: '등급 설정이 성공적으로 업데이트되었습니다.'
|
|
541
|
-
})
|
|
542
|
-
}
|
|
543
|
-
} catch (error) {
|
|
544
|
-
notify({
|
|
545
|
-
message: '등급 설정 업데이트 중 오류가 발생했습니다.'
|
|
546
|
-
})
|
|
511
|
+
popup.onclosed = () => {
|
|
512
|
+
this.grist.fetch()
|
|
547
513
|
}
|
|
548
514
|
}
|
|
549
515
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import '@material/web/button/elevated-button.js';
|
|
2
2
|
import '@material/web/button/filled-button.js';
|
|
3
3
|
import '@material/web/button/text-button.js';
|
|
4
|
-
import '@material/web/textfield/outlined-text-field.js';
|
|
5
|
-
import '@material/web/select/outlined-select.js';
|
|
6
|
-
import '@material/web/select/select-option.js';
|
|
7
4
|
import '@material/web/icon/icon.js';
|
|
5
|
+
import '@operato/data-grist/ox-grist.js';
|
|
8
6
|
import { LitElement } from 'lit';
|
|
7
|
+
import { DataGrist } from '@operato/data-grist/ox-grist.js';
|
|
8
|
+
import { FetchOption } from '@operato/data-grist';
|
|
9
9
|
interface KpiGrade {
|
|
10
10
|
name: string;
|
|
11
11
|
minValue: number;
|
|
@@ -15,21 +15,21 @@ interface KpiGrade {
|
|
|
15
15
|
description?: string;
|
|
16
16
|
}
|
|
17
17
|
type KpiGrades = KpiGrade[];
|
|
18
|
-
declare const KpiGradeEditor_base:
|
|
18
|
+
declare const KpiGradeEditor_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
19
19
|
export declare class KpiGradeEditor extends KpiGradeEditor_base {
|
|
20
20
|
static styles: import("lit").CSSResult[];
|
|
21
21
|
kpi: any;
|
|
22
22
|
grades: KpiGrades;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
gristConfig: any;
|
|
24
|
+
grist: DataGrist;
|
|
25
|
+
firstUpdated(): Promise<void>;
|
|
25
26
|
render(): import("lit-html").TemplateResult<1>;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
_save(): Promise<void>;
|
|
27
|
+
fetchHandler({ page, limit, sorters }: FetchOption): Promise<{
|
|
28
|
+
total: number;
|
|
29
|
+
records: KpiGrades;
|
|
30
|
+
}>;
|
|
31
|
+
_updateGrades(): Promise<void>;
|
|
32
32
|
_validateGrades(): boolean;
|
|
33
|
-
|
|
33
|
+
_deleteGrades(): Promise<void>;
|
|
34
34
|
}
|
|
35
35
|
export {};
|