@things-factory/kpi 9.0.16 → 9.0.18
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 +12 -38
- package/client/pages/kpi/kpi-viz-editor.ts +214 -14
- 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 +10 -34
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi/kpi-viz-editor.js +208 -14
- package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -194,11 +194,13 @@ export class KpiVizEditor extends localize(i18next)(LitElement) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
_renderPreview() {
|
|
197
|
-
const kpiValue = this.kpi?.value?.value
|
|
198
|
-
const targetValue = this.kpi?.targetValue
|
|
199
|
-
const unit = this.kpi?.unit
|
|
197
|
+
const kpiValue = this.kpi?.value?.value ?? 75
|
|
198
|
+
const targetValue = this.kpi?.targetValue ?? 100
|
|
199
|
+
const unit = this.kpi?.unit ?? ''
|
|
200
200
|
const color = this.vizMeta.color || '#2196f3'
|
|
201
201
|
const icon = this.vizMeta.icon || 'trending_up'
|
|
202
|
+
const min = this.vizMeta.minValue ?? 0
|
|
203
|
+
const max = this.vizMeta.maxValue ?? 100
|
|
202
204
|
|
|
203
205
|
switch (this.selectedVizType) {
|
|
204
206
|
case 'CARD':
|
|
@@ -213,22 +215,161 @@ export class KpiVizEditor extends localize(i18next)(LitElement) {
|
|
|
213
215
|
</div>
|
|
214
216
|
</div>
|
|
215
217
|
`
|
|
216
|
-
case 'GAUGE':
|
|
217
|
-
const
|
|
218
|
+
case 'GAUGE': {
|
|
219
|
+
const value = Math.max(min, Math.min(kpiValue, max))
|
|
220
|
+
const percent = max - min > 0 ? (value - min) / (max - min) : 0
|
|
221
|
+
const r = 60
|
|
222
|
+
const cx = 90
|
|
223
|
+
const cy = 90
|
|
224
|
+
const startX = cx - r
|
|
225
|
+
const startY = cy
|
|
226
|
+
const endX = cx + r * Math.cos(Math.PI * (1 - percent))
|
|
227
|
+
const endY = cy - r * Math.sin(Math.PI * (1 - percent))
|
|
228
|
+
const needleAngle = Math.PI - Math.PI * percent
|
|
229
|
+
const needleX = cx + r * Math.cos(needleAngle)
|
|
230
|
+
const needleY = cy - r * Math.sin(needleAngle)
|
|
218
231
|
return html`
|
|
219
232
|
<div style="text-align:center;padding:16px;">
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
<svg width="180" height="110" viewBox="0 0 180 110">
|
|
234
|
+
<!-- 배경 arc -->
|
|
235
|
+
<path
|
|
236
|
+
d="M${startX},${startY} A${r},${r} 0 0,1 ${cx + r},${cy}"
|
|
237
|
+
fill="none"
|
|
238
|
+
stroke="#e0e0e0"
|
|
239
|
+
stroke-width="16"
|
|
240
|
+
/>
|
|
241
|
+
<!-- 값 arc -->
|
|
242
|
+
<path
|
|
243
|
+
d="M${startX},${startY} A${r},${r} 0 0,1 ${endX},${endY}"
|
|
244
|
+
fill="none"
|
|
245
|
+
stroke="${color}"
|
|
246
|
+
stroke-width="16"
|
|
247
|
+
/>
|
|
248
|
+
<!-- 바늘 -->
|
|
249
|
+
<line x1="${cx}" y1="${cy}" x2="${needleX}" y2="${needleY}" stroke="#333" stroke-width="4" />
|
|
250
|
+
<!-- 중심 원 -->
|
|
251
|
+
<circle cx="${cx}" cy="${cy}" r="7" fill="#333" />
|
|
252
|
+
<!-- 중앙값 -->
|
|
253
|
+
<text x="${cx}" y="${cy - 25}" text-anchor="middle" font-size="22" fill="${color}" font-weight="bold">
|
|
254
|
+
${value}${unit}
|
|
255
|
+
</text>
|
|
256
|
+
<!-- min/max -->
|
|
257
|
+
<text x="${cx - r}" y="${cy + 20}" text-anchor="middle" font-size="12" fill="#888">${min}</text>
|
|
258
|
+
<text x="${cx + r}" y="${cy + 20}" text-anchor="middle" font-size="12" fill="#888">${max}</text>
|
|
259
|
+
</svg>
|
|
260
|
+
</div>
|
|
261
|
+
`
|
|
262
|
+
}
|
|
263
|
+
case 'SPEEDOMETER': {
|
|
264
|
+
const value = Math.max(min, Math.min(kpiValue, max))
|
|
265
|
+
const percent = max - min > 0 ? (value - min) / (max - min) : 0
|
|
266
|
+
const r = 60
|
|
267
|
+
const cx = 90
|
|
268
|
+
const cy = 90
|
|
269
|
+
const startX = cx - r
|
|
270
|
+
const startY = cy
|
|
271
|
+
const endX = cx + r * Math.cos(Math.PI * (1 - percent))
|
|
272
|
+
const endY = cy - r * Math.sin(Math.PI * (1 - percent))
|
|
273
|
+
const needleAngle = Math.PI - Math.PI * percent
|
|
274
|
+
const needleX = cx + r * Math.cos(needleAngle)
|
|
275
|
+
const needleY = cy - r * Math.sin(needleAngle)
|
|
276
|
+
// 중간 눈금 (5개)
|
|
277
|
+
const ticks = Array.from({ length: 6 }, (_, i) => {
|
|
278
|
+
const tickAngle = Math.PI - (Math.PI * i) / 5
|
|
279
|
+
const tx1 = cx + (r - 8) * Math.cos(tickAngle)
|
|
280
|
+
const ty1 = cy - (r - 8) * Math.sin(tickAngle)
|
|
281
|
+
const tx2 = cx + (r + 8) * Math.cos(tickAngle)
|
|
282
|
+
const ty2 = cy - (r + 8) * Math.sin(tickAngle)
|
|
283
|
+
const label = Math.round(min + (max - min) * (i / 5))
|
|
284
|
+
const lx = cx + (r + 22) * Math.cos(tickAngle)
|
|
285
|
+
const ly = cy - (r + 22) * Math.sin(tickAngle) + 6
|
|
286
|
+
return { tx1, ty1, tx2, ty2, label, lx, ly }
|
|
287
|
+
})
|
|
288
|
+
return html`
|
|
289
|
+
<div style="text-align:center;padding:16px;">
|
|
290
|
+
<svg width="200" height="120" viewBox="0 0 200 120">
|
|
291
|
+
<!-- 배경 arc (더 두껍게) -->
|
|
292
|
+
<path
|
|
293
|
+
d="M${startX + 10},${startY} A${r},${r} 0 0,1 ${cx + r + 10},${cy}"
|
|
294
|
+
fill="none"
|
|
295
|
+
stroke="#e0e0e0"
|
|
296
|
+
stroke-width="28"
|
|
297
|
+
/>
|
|
298
|
+
<!-- 값 arc -->
|
|
299
|
+
<path
|
|
300
|
+
d="M${startX + 10},${startY} A${r},${r} 0 0,1 ${endX + 10},${endY}"
|
|
301
|
+
fill="none"
|
|
302
|
+
stroke="${color}"
|
|
303
|
+
stroke-width="28"
|
|
304
|
+
/>
|
|
305
|
+
<!-- 눈금 -->
|
|
306
|
+
${ticks.map(
|
|
307
|
+
t =>
|
|
308
|
+
html`<line
|
|
309
|
+
x1="${t.tx1 + 10}"
|
|
310
|
+
y1="${t.ty1}"
|
|
311
|
+
x2="${t.tx2 + 10}"
|
|
312
|
+
y2="${t.ty2}"
|
|
313
|
+
stroke="#888"
|
|
314
|
+
stroke-width="2"
|
|
315
|
+
/>`
|
|
316
|
+
)}
|
|
317
|
+
<!-- 눈금 숫자 -->
|
|
318
|
+
${ticks.map(
|
|
319
|
+
t =>
|
|
320
|
+
html`<text
|
|
321
|
+
x="${t.lx + 10}"
|
|
322
|
+
y="${t.ly}"
|
|
323
|
+
text-anchor="middle"
|
|
324
|
+
font-size="14"
|
|
325
|
+
fill="#333"
|
|
326
|
+
font-weight="bold"
|
|
327
|
+
>${t.label}</text
|
|
328
|
+
>`
|
|
329
|
+
)}
|
|
330
|
+
<!-- 바늘 (빨간색) -->
|
|
331
|
+
<line x1="${cx + 10}" y1="${cy}" x2="${needleX + 10}" y2="${needleY}" stroke="#d32f2f" stroke-width="6" />
|
|
332
|
+
<!-- 중심 원 -->
|
|
333
|
+
<circle cx="${cx + 10}" cy="${cy}" r="13" fill="#333" />
|
|
334
|
+
<!-- 중앙값 -->
|
|
335
|
+
<text
|
|
336
|
+
x="${cx + 10}"
|
|
337
|
+
y="${cy - 32}"
|
|
338
|
+
text-anchor="middle"
|
|
339
|
+
font-size="26"
|
|
340
|
+
fill="${color}"
|
|
341
|
+
font-weight="bold"
|
|
226
342
|
>
|
|
227
|
-
${
|
|
228
|
-
</
|
|
229
|
-
|
|
343
|
+
${value}${unit}
|
|
344
|
+
</text>
|
|
345
|
+
<!-- min/max 포인트 -->
|
|
346
|
+
<circle cx="${startX + 10}" cy="${startY}" r="7" fill="#fff" stroke="#888" stroke-width="2" />
|
|
347
|
+
<circle cx="${cx + r + 10}" cy="${cy}" r="7" fill="#fff" stroke="#888" stroke-width="2" />
|
|
348
|
+
<!-- min/max 숫자 크게 -->
|
|
349
|
+
<text
|
|
350
|
+
x="${startX + 10}"
|
|
351
|
+
y="${startY + 32}"
|
|
352
|
+
text-anchor="middle"
|
|
353
|
+
font-size="16"
|
|
354
|
+
fill="#333"
|
|
355
|
+
font-weight="bold"
|
|
356
|
+
>
|
|
357
|
+
${min}
|
|
358
|
+
</text>
|
|
359
|
+
<text
|
|
360
|
+
x="${cx + r + 10}"
|
|
361
|
+
y="${cy + 32}"
|
|
362
|
+
text-anchor="middle"
|
|
363
|
+
font-size="16"
|
|
364
|
+
fill="#333"
|
|
365
|
+
font-weight="bold"
|
|
366
|
+
>
|
|
367
|
+
${max}
|
|
368
|
+
</text>
|
|
369
|
+
</svg>
|
|
230
370
|
</div>
|
|
231
371
|
`
|
|
372
|
+
}
|
|
232
373
|
case 'PROGRESS':
|
|
233
374
|
const progressPercentage = Math.min((kpiValue / targetValue) * 100, 100)
|
|
234
375
|
return html`
|
|
@@ -241,6 +382,65 @@ export class KpiVizEditor extends localize(i18next)(LitElement) {
|
|
|
241
382
|
</div>
|
|
242
383
|
</div>
|
|
243
384
|
`
|
|
385
|
+
case 'THERMOMETER': {
|
|
386
|
+
const value = Math.max(min, Math.min(kpiValue, max))
|
|
387
|
+
const percent = max - min > 0 ? (value - min) / (max - min) : 0
|
|
388
|
+
const barHeight = 120
|
|
389
|
+
const barWidth = 24
|
|
390
|
+
const x = 100
|
|
391
|
+
const yTop = 30
|
|
392
|
+
const yBottom = yTop + barHeight
|
|
393
|
+
const fillY = yBottom - percent * barHeight
|
|
394
|
+
return html`
|
|
395
|
+
<div style="text-align:center;padding:16px;">
|
|
396
|
+
<svg width="200" height="180" viewBox="0 0 200 180">
|
|
397
|
+
<!-- 바깥 테두리 -->
|
|
398
|
+
<rect
|
|
399
|
+
x="${x - barWidth / 2 - 4}"
|
|
400
|
+
y="${yTop - 4}"
|
|
401
|
+
width="${barWidth + 8}"
|
|
402
|
+
height="${barHeight + 8}"
|
|
403
|
+
rx="16"
|
|
404
|
+
fill="#f5f5f5"
|
|
405
|
+
stroke="#bbb"
|
|
406
|
+
stroke-width="2"
|
|
407
|
+
/>
|
|
408
|
+
<!-- 빈 막대 -->
|
|
409
|
+
<rect
|
|
410
|
+
x="${x - barWidth / 2}"
|
|
411
|
+
y="${yTop}"
|
|
412
|
+
width="${barWidth}"
|
|
413
|
+
height="${barHeight}"
|
|
414
|
+
rx="12"
|
|
415
|
+
fill="#e0e0e0"
|
|
416
|
+
/>
|
|
417
|
+
<!-- 채워진 부분 -->
|
|
418
|
+
<rect
|
|
419
|
+
x="${x - barWidth / 2}"
|
|
420
|
+
y="${fillY}"
|
|
421
|
+
width="${barWidth}"
|
|
422
|
+
height="${yBottom - fillY}"
|
|
423
|
+
rx="12"
|
|
424
|
+
fill="${color}"
|
|
425
|
+
/>
|
|
426
|
+
<!-- 하단 구슬 -->
|
|
427
|
+
<circle cx="${x}" cy="${yBottom + 18}" r="22" fill="#e0e0e0" stroke="#bbb" stroke-width="2" />
|
|
428
|
+
<circle cx="${x}" cy="${yBottom + 18}" r="18" fill="${color}" />
|
|
429
|
+
<!-- 현재값 -->
|
|
430
|
+
<text x="${x}" y="${fillY - 12}" text-anchor="middle" font-size="22" fill="${color}" font-weight="bold">
|
|
431
|
+
${value}${unit}
|
|
432
|
+
</text>
|
|
433
|
+
<!-- min/max -->
|
|
434
|
+
<text x="${x}" y="${yBottom + 52}" text-anchor="middle" font-size="16" fill="#333" font-weight="bold">
|
|
435
|
+
${min}
|
|
436
|
+
</text>
|
|
437
|
+
<text x="${x}" y="${yTop - 12}" text-anchor="middle" font-size="16" fill="#333" font-weight="bold">
|
|
438
|
+
${max}
|
|
439
|
+
</text>
|
|
440
|
+
</svg>
|
|
441
|
+
</div>
|
|
442
|
+
`
|
|
443
|
+
}
|
|
244
444
|
case 'ICON':
|
|
245
445
|
return html`
|
|
246
446
|
<div style="text-align:center;padding:16px;">
|
|
@@ -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 {};
|
|
@@ -2,261 +2,144 @@ import { __decorate, __metadata } from "tslib";
|
|
|
2
2
|
import '@material/web/button/elevated-button.js';
|
|
3
3
|
import '@material/web/button/filled-button.js';
|
|
4
4
|
import '@material/web/button/text-button.js';
|
|
5
|
-
import '@material/web/textfield/outlined-text-field.js';
|
|
6
|
-
import '@material/web/select/outlined-select.js';
|
|
7
|
-
import '@material/web/select/select-option.js';
|
|
8
5
|
import '@material/web/icon/icon.js';
|
|
6
|
+
import '@operato/data-grist/ox-grist.js';
|
|
7
|
+
import deepEquals from 'lodash-es/isEqual';
|
|
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';
|
|
13
|
+
import { client } from '@operato/graphql';
|
|
12
14
|
import { notify } from '@operato/layout';
|
|
13
|
-
import { CommonHeaderStyles
|
|
14
|
-
let KpiGradeEditor = class KpiGradeEditor extends
|
|
15
|
+
import { CommonHeaderStyles } from '@operato/styles';
|
|
16
|
+
let KpiGradeEditor = class KpiGradeEditor extends localize(i18next)(LitElement) {
|
|
15
17
|
constructor() {
|
|
16
18
|
super(...arguments);
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
+
this.kpi = { grades: [] };
|
|
20
|
+
this.grades = this.kpi?.grades || [];
|
|
21
|
+
this.gristConfig = null;
|
|
19
22
|
}
|
|
20
23
|
static { this.styles = [
|
|
21
24
|
CommonHeaderStyles,
|
|
22
|
-
ScrollbarStyles,
|
|
23
25
|
css `
|
|
24
26
|
:host {
|
|
25
27
|
display: flex;
|
|
26
28
|
flex-direction: column;
|
|
27
|
-
background-color: var(--md-sys-color-surface, #f4f6fa);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.grade-list {
|
|
31
|
-
flex: 1;
|
|
32
|
-
margin-bottom: 20px;
|
|
33
|
-
overflow-y: auto;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.grade-item {
|
|
37
|
-
display: flex;
|
|
38
|
-
align-items: center;
|
|
39
|
-
gap: 10px;
|
|
40
|
-
padding: 10px;
|
|
41
|
-
border: 1px solid #ddd;
|
|
42
|
-
border-radius: 4px;
|
|
43
|
-
margin-bottom: 10px;
|
|
44
|
-
background: #f9f9f9;
|
|
45
|
-
}
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
background: #f0f0f0;
|
|
30
|
+
background-color: var(--md-sys-color-surface);
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
|
|
52
|
-
display: flex;
|
|
53
|
-
gap: 10px;
|
|
33
|
+
ox-grist {
|
|
54
34
|
flex: 1;
|
|
55
35
|
}
|
|
56
|
-
|
|
57
|
-
.grade-inputs md-outlined-text-field {
|
|
58
|
-
flex: 1;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.grade-actions {
|
|
62
|
-
display: flex;
|
|
63
|
-
gap: 5px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.footer span {
|
|
67
|
-
font-size: 0.8em;
|
|
68
|
-
color: var(--md-sys-color-on-surface);
|
|
69
|
-
line-height: 1.5;
|
|
70
|
-
padding: 10px;
|
|
71
|
-
}
|
|
72
36
|
`
|
|
73
37
|
]; }
|
|
74
|
-
|
|
75
|
-
super.connectedCallback();
|
|
38
|
+
async firstUpdated() {
|
|
76
39
|
if (this.kpi?.grades) {
|
|
77
40
|
this.grades = [...this.kpi.grades];
|
|
78
41
|
}
|
|
42
|
+
this.gristConfig = {
|
|
43
|
+
list: { fields: ['name', 'minValue', 'maxValue', 'score', 'color', 'description'] },
|
|
44
|
+
columns: [
|
|
45
|
+
{ type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true },
|
|
46
|
+
{ type: 'gutter', gutterName: 'sequence', fixed: true },
|
|
47
|
+
{ type: 'gutter', gutterName: 'button', fixed: true, icon: 'add', handlers: { click: 'record-copy' } },
|
|
48
|
+
{ type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_upward', handlers: { click: 'move-up' } },
|
|
49
|
+
{ type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_downward', handlers: { click: 'move-down' } },
|
|
50
|
+
{ type: 'string', name: 'name', header: '등급명', record: { editable: true }, width: 100 },
|
|
51
|
+
{ type: 'number', name: 'minValue', header: '최소값', record: { editable: true }, width: 100 },
|
|
52
|
+
{ type: 'number', name: 'maxValue', header: '최대값', record: { editable: true }, width: 100 },
|
|
53
|
+
{ type: 'number', name: 'score', header: '점수', record: { editable: true }, width: 80 },
|
|
54
|
+
{ type: 'color', name: 'color', header: '색상', record: { editable: true }, width: 100 },
|
|
55
|
+
{ type: 'string', name: 'description', header: '설명', record: { editable: true }, width: 200 }
|
|
56
|
+
],
|
|
57
|
+
rows: { selectable: { multiple: true } },
|
|
58
|
+
pagination: { infinite: true }
|
|
59
|
+
};
|
|
79
60
|
}
|
|
80
61
|
render() {
|
|
81
62
|
return html `
|
|
82
|
-
<
|
|
83
|
-
|
|
63
|
+
<ox-grist .mode=${'GRID'} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}></ox-grist>
|
|
84
64
|
<div class="footer">
|
|
85
|
-
<button type="button" @click=${this._addGrade}><md-icon>add</md-icon>등급 추가</button>
|
|
86
65
|
<div filler></div>
|
|
87
|
-
<button
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
_renderGradeItem(grade, index) {
|
|
93
|
-
return html `
|
|
94
|
-
<div class="grade-item">
|
|
95
|
-
<div class="grade-inputs">
|
|
96
|
-
<md-outlined-text-field
|
|
97
|
-
label="등급명"
|
|
98
|
-
value=${grade.name}
|
|
99
|
-
@input=${(e) => this._updateGrade(index, 'name', e.target.value)}
|
|
100
|
-
></md-outlined-text-field>
|
|
101
|
-
|
|
102
|
-
<md-outlined-text-field
|
|
103
|
-
label="최소값"
|
|
104
|
-
type="number"
|
|
105
|
-
value=${grade.minValue}
|
|
106
|
-
@input=${(e) => this._updateGrade(index, 'minValue', parseFloat(e.target.value))}
|
|
107
|
-
></md-outlined-text-field>
|
|
108
|
-
|
|
109
|
-
<md-outlined-text-field
|
|
110
|
-
label="최대값"
|
|
111
|
-
type="number"
|
|
112
|
-
value=${grade.maxValue}
|
|
113
|
-
@input=${(e) => this._updateGrade(index, 'maxValue', parseFloat(e.target.value))}
|
|
114
|
-
></md-outlined-text-field>
|
|
115
|
-
|
|
116
|
-
<md-outlined-text-field
|
|
117
|
-
label="점수"
|
|
118
|
-
type="number"
|
|
119
|
-
value=${grade.score || ''}
|
|
120
|
-
@input=${(e) => this._updateGrade(index, 'score', parseFloat(e.target.value))}
|
|
121
|
-
></md-outlined-text-field>
|
|
122
|
-
|
|
123
|
-
<md-outlined-text-field
|
|
124
|
-
label="색상"
|
|
125
|
-
value=${grade.color || ''}
|
|
126
|
-
@input=${(e) => this._updateGrade(index, 'color', e.target.value)}
|
|
127
|
-
></md-outlined-text-field>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<div class="grade-actions">
|
|
131
|
-
<md-icon-button @click=${() => this._removeGrade(index)}>
|
|
132
|
-
<md-icon>delete</md-icon>
|
|
133
|
-
</md-icon-button>
|
|
134
|
-
</div>
|
|
66
|
+
<button danger @click=${this._deleteGrades.bind(this)}>
|
|
67
|
+
<md-icon>delete</md-icon>${i18next.t('button.delete')}
|
|
68
|
+
</button>
|
|
69
|
+
<button done type="button" @click=${this._updateGrades}><md-icon>save</md-icon>저장</button>
|
|
135
70
|
</div>
|
|
136
71
|
`;
|
|
137
72
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
_addGrade() {
|
|
144
|
-
const newGrade = {
|
|
145
|
-
name: '',
|
|
146
|
-
minValue: 0,
|
|
147
|
-
maxValue: 0,
|
|
148
|
-
score: 0,
|
|
149
|
-
color: '#4caf50',
|
|
150
|
-
description: ''
|
|
73
|
+
async fetchHandler({ page, limit, sorters = [] }) {
|
|
74
|
+
return {
|
|
75
|
+
total: this.grades.length,
|
|
76
|
+
records: this.grades
|
|
151
77
|
};
|
|
152
|
-
this.grades.push(newGrade);
|
|
153
|
-
this.isDirty = true;
|
|
154
|
-
this.requestUpdate();
|
|
155
|
-
}
|
|
156
|
-
_removeGrade(index) {
|
|
157
|
-
this.grades.splice(index, 1);
|
|
158
|
-
this.isDirty = true;
|
|
159
|
-
this.requestUpdate();
|
|
160
78
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
];
|
|
171
|
-
break;
|
|
172
|
-
case '3grade':
|
|
173
|
-
this.grades = [
|
|
174
|
-
{ name: '우수', minValue: 0, maxValue: 1.5, score: 95, color: '#4caf50', description: '목표 달성' },
|
|
175
|
-
{ name: '양호', minValue: 1.5, maxValue: 2.5, score: 85, color: '#ff9800', description: '기준 달성' },
|
|
176
|
-
{ name: '미흡', minValue: 2.5, maxValue: 999, score: 75, color: '#f44336', description: '개선 필요' }
|
|
177
|
-
];
|
|
178
|
-
break;
|
|
179
|
-
case 'continuous':
|
|
180
|
-
this.grades = [
|
|
181
|
-
{ name: '0.999999', minValue: 0, maxValue: 0.025, score: 0.999999, color: '#4caf50' },
|
|
182
|
-
{ name: '0.944189368', minValue: 0.025, maxValue: 0.05, score: 0.944189368, color: '#4caf50' },
|
|
183
|
-
{ name: '0.888379735', minValue: 0.05, maxValue: 0.075, score: 0.888379735, color: '#4caf50' }
|
|
184
|
-
];
|
|
185
|
-
break;
|
|
186
|
-
case 'clear':
|
|
187
|
-
this.grades = [];
|
|
188
|
-
break;
|
|
79
|
+
async _updateGrades() {
|
|
80
|
+
this.grades = this.grist.dirtyData.records
|
|
81
|
+
.map(patch => {
|
|
82
|
+
const { name, minValue, maxValue, score, color, description } = patch;
|
|
83
|
+
return { name, minValue, maxValue, score, color, description };
|
|
84
|
+
})
|
|
85
|
+
.sort((a, b) => a.minValue - b.minValue);
|
|
86
|
+
if (!this._validateGrades()) {
|
|
87
|
+
return;
|
|
189
88
|
}
|
|
190
|
-
this.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
89
|
+
if (!deepEquals(this.kpi?.grades, this.grades)) {
|
|
90
|
+
try {
|
|
91
|
+
const response = await client.mutate({
|
|
92
|
+
mutation: gql `
|
|
93
|
+
mutation ($id: String!, $patch: KpiPatch!) {
|
|
94
|
+
updateKpi(id: $id, patch: $patch) {
|
|
95
|
+
id
|
|
96
|
+
name
|
|
97
|
+
grades
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
`,
|
|
101
|
+
variables: {
|
|
102
|
+
id: this.kpi.id,
|
|
103
|
+
patch: { grades: this.grades }
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.grades = response.data.updateKpi.grades;
|
|
107
|
+
this.grist.fetch();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
notify({ message: '등급 저장 중 오류가 발생했습니다.' });
|
|
198
111
|
}
|
|
199
|
-
// 정렬 (minValue 기준)
|
|
200
|
-
this.grades.sort((a, b) => a.minValue - b.minValue);
|
|
201
|
-
this.dispatchEvent(new CustomEvent('grades-updated', {
|
|
202
|
-
detail: {
|
|
203
|
-
kpiId: this.kpi.id,
|
|
204
|
-
grades: this.grades
|
|
205
|
-
}
|
|
206
|
-
}));
|
|
207
|
-
this.isDirty = false;
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
notify({
|
|
211
|
-
message: '등급 저장 중 오류가 발생했습니다.'
|
|
212
|
-
});
|
|
213
112
|
}
|
|
214
113
|
}
|
|
215
114
|
_validateGrades() {
|
|
216
|
-
// 최소 1개 등급 필요
|
|
217
115
|
if (this.grades.length === 0) {
|
|
218
|
-
notify({
|
|
219
|
-
message: '최소 1개 이상의 등급을 설정해야 합니다.'
|
|
220
|
-
});
|
|
116
|
+
notify({ message: '최소 1개 이상의 등급을 설정해야 합니다.' });
|
|
221
117
|
return false;
|
|
222
118
|
}
|
|
223
|
-
// 등급명 중복 체크
|
|
224
119
|
const names = this.grades.map(g => g.name);
|
|
225
120
|
const uniqueNames = new Set(names);
|
|
226
121
|
if (names.length !== uniqueNames.size) {
|
|
227
|
-
notify({
|
|
228
|
-
message: '등급명이 중복되었습니다.'
|
|
229
|
-
});
|
|
122
|
+
notify({ message: '등급명이 중복되었습니다.' });
|
|
230
123
|
return false;
|
|
231
124
|
}
|
|
232
|
-
// 값 범위 체크
|
|
233
125
|
for (let i = 0; i < this.grades.length; i++) {
|
|
234
126
|
const grade = this.grades[i];
|
|
235
127
|
if (grade.minValue >= grade.maxValue) {
|
|
236
|
-
notify({
|
|
237
|
-
message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.`
|
|
238
|
-
});
|
|
128
|
+
notify({ message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.` });
|
|
239
129
|
return false;
|
|
240
130
|
}
|
|
241
|
-
// 연속성 체크
|
|
242
131
|
if (i > 0) {
|
|
243
132
|
const prevGrade = this.grades[i - 1];
|
|
244
133
|
if (prevGrade.maxValue !== grade.minValue) {
|
|
245
|
-
notify({
|
|
246
|
-
message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.`
|
|
247
|
-
});
|
|
134
|
+
notify({ message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.` });
|
|
248
135
|
return false;
|
|
249
136
|
}
|
|
250
137
|
}
|
|
251
138
|
}
|
|
252
139
|
return true;
|
|
253
140
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const popup = this.closest('ox-popup');
|
|
257
|
-
if (popup && popup.close) {
|
|
258
|
-
popup.close();
|
|
259
|
-
}
|
|
141
|
+
async _deleteGrades() {
|
|
142
|
+
this.grist.deleteSelectedRecords(true);
|
|
260
143
|
}
|
|
261
144
|
};
|
|
262
145
|
__decorate([
|
|
@@ -270,7 +153,11 @@ __decorate([
|
|
|
270
153
|
__decorate([
|
|
271
154
|
state(),
|
|
272
155
|
__metadata("design:type", Object)
|
|
273
|
-
], KpiGradeEditor.prototype, "
|
|
156
|
+
], KpiGradeEditor.prototype, "gristConfig", void 0);
|
|
157
|
+
__decorate([
|
|
158
|
+
query('ox-grist'),
|
|
159
|
+
__metadata("design:type", DataGrist)
|
|
160
|
+
], KpiGradeEditor.prototype, "grist", void 0);
|
|
274
161
|
KpiGradeEditor = __decorate([
|
|
275
162
|
customElement('kpi-grade-editor')
|
|
276
163
|
], KpiGradeEditor);
|