@things-factory/kpi 9.0.14 → 9.0.16
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-list-page.ts +78 -2
- package/client/pages/kpi/kpi-viz-editor.ts +353 -0
- package/client/pages/kpi-dashboard/kpi-dashboard.ts +128 -1
- package/dist-client/pages/kpi/kpi-list-page.d.ts +6 -1
- package/dist-client/pages/kpi/kpi-list-page.js +72 -2
- package/dist-client/pages/kpi/kpi-list-page.js.map +1 -1
- package/dist-client/pages/kpi/kpi-viz-editor.d.ts +20 -0
- package/dist-client/pages/kpi/kpi-viz-editor.js +364 -0
- package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +2 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +117 -1
- package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/kpi/kpi-type.d.ts +3 -3
- package/dist-server/service/kpi/kpi-type.js +4 -4
- package/dist-server/service/kpi/kpi-type.js.map +1 -1
- package/dist-server/service/kpi/kpi.d.ts +18 -1
- package/dist-server/service/kpi/kpi.js +26 -4
- package/dist-server/service/kpi/kpi.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-type.d.ts +1 -0
- package/dist-server/service/kpi-category/kpi-category-type.js +5 -1
- package/dist-server/service/kpi-category/kpi-category-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/kpi/kpi-type.ts +7 -7
- package/server/service/kpi/kpi.ts +27 -4
- package/server/service/kpi-category/kpi-category-type.ts +4 -1
|
@@ -3,6 +3,7 @@ import '@material/web/button/elevated-button.js'
|
|
|
3
3
|
import '@operato/data-grist/ox-grist.js'
|
|
4
4
|
import '@operato/data-grist/ox-filters-form.js'
|
|
5
5
|
import '@operato/data-grist/ox-record-creator.js'
|
|
6
|
+
import './kpi-viz-editor.js'
|
|
6
7
|
|
|
7
8
|
import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
|
|
8
9
|
import { PageView, store } from '@operato/shell'
|
|
@@ -21,6 +22,7 @@ import gql from 'graphql-tag'
|
|
|
21
22
|
|
|
22
23
|
import { KpiImporter } from './kpi-importer'
|
|
23
24
|
import { KpiGradeEditor } from './kpi-grade-editor'
|
|
25
|
+
import { KpiVizEditor } from './kpi-viz-editor'
|
|
24
26
|
|
|
25
27
|
@customElement('kpi-list-page')
|
|
26
28
|
export class KpiListPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
|
|
@@ -52,7 +54,8 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
|
|
|
52
54
|
static get scopedElements() {
|
|
53
55
|
return {
|
|
54
56
|
'kpi-importer': KpiImporter,
|
|
55
|
-
'kpi-grade-editor': KpiGradeEditor
|
|
57
|
+
'kpi-grade-editor': KpiGradeEditor,
|
|
58
|
+
'kpi-viz-editor': KpiVizEditor
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -237,7 +240,30 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
|
|
|
237
240
|
width: 60
|
|
238
241
|
},
|
|
239
242
|
{ type: 'string', name: 'state', header: '상태', record: { editable: false }, width: 100 },
|
|
240
|
-
{
|
|
243
|
+
{
|
|
244
|
+
type: 'string',
|
|
245
|
+
name: 'vizType',
|
|
246
|
+
header: '시각화 설정',
|
|
247
|
+
record: {
|
|
248
|
+
editable: false,
|
|
249
|
+
renderer: (v, c, r) => {
|
|
250
|
+
const vizType = r.vizType || 'CARD'
|
|
251
|
+
const vizMeta = r.vizMeta || {}
|
|
252
|
+
const color = vizMeta.color || '#2196f3'
|
|
253
|
+
const icon = vizMeta.icon || 'dashboard'
|
|
254
|
+
|
|
255
|
+
return html`
|
|
256
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
257
|
+
<span style="color:${color};cursor:pointer;" @click=${() => this._editViz(r)}> ${vizType} </span>
|
|
258
|
+
<md-icon style="color:${color};font-size:16px;cursor:pointer;" @click=${() => this._editViz(r)}>
|
|
259
|
+
${icon}
|
|
260
|
+
</md-icon>
|
|
261
|
+
</div>
|
|
262
|
+
`
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
width: 150
|
|
266
|
+
},
|
|
241
267
|
{ type: 'string', name: 'schedule', header: '스케줄', record: { editable: true }, width: 120 },
|
|
242
268
|
{ type: 'string', name: 'scheduleId', header: '스케줄ID', record: { editable: false }, width: 120 },
|
|
243
269
|
{ type: 'string', name: 'timezone', header: '타임존', record: { editable: true }, width: 100 },
|
|
@@ -295,6 +321,8 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
|
|
|
295
321
|
description
|
|
296
322
|
active
|
|
297
323
|
grades
|
|
324
|
+
vizType
|
|
325
|
+
vizMeta
|
|
298
326
|
category {
|
|
299
327
|
id
|
|
300
328
|
name
|
|
@@ -518,4 +546,52 @@ export class KpiListPage extends connect(store)(localize(i18next)(ScopedElements
|
|
|
518
546
|
})
|
|
519
547
|
}
|
|
520
548
|
}
|
|
549
|
+
|
|
550
|
+
async _editViz(kpi: any) {
|
|
551
|
+
const popup = await openPopup(
|
|
552
|
+
html`
|
|
553
|
+
<kpi-viz-editor
|
|
554
|
+
.kpi=${kpi}
|
|
555
|
+
.onSave=${(vizType: string, vizMeta: any) => this._onVizUpdated(kpi.id, vizType, vizMeta)}
|
|
556
|
+
.onCancel=${() => popup.close()}
|
|
557
|
+
></kpi-viz-editor>
|
|
558
|
+
`,
|
|
559
|
+
{
|
|
560
|
+
title: `${kpi.name} - 시각화 설정`,
|
|
561
|
+
size: 'large'
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async _onVizUpdated(kpiId: string, vizType: string, vizMeta: any) {
|
|
567
|
+
try {
|
|
568
|
+
const response = await client.mutate({
|
|
569
|
+
mutation: gql`
|
|
570
|
+
mutation ($id: String!, $patch: KpiPatch!) {
|
|
571
|
+
updateKpi(id: $id, patch: $patch) {
|
|
572
|
+
id
|
|
573
|
+
name
|
|
574
|
+
vizType
|
|
575
|
+
vizMeta
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
`,
|
|
579
|
+
variables: {
|
|
580
|
+
id: kpiId,
|
|
581
|
+
patch: { vizType, vizMeta }
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
if (!response.errors) {
|
|
586
|
+
this.grist.fetch()
|
|
587
|
+
notify({
|
|
588
|
+
message: '시각화 설정이 성공적으로 업데이트되었습니다.'
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
} catch (error) {
|
|
592
|
+
notify({
|
|
593
|
+
message: '시각화 설정 업데이트 중 오류가 발생했습니다.'
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
}
|
|
521
597
|
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import '@material/web/button/elevated-button.js'
|
|
2
|
+
import '@material/web/select/outlined-select.js'
|
|
3
|
+
import '@material/web/textfield/outlined-text-field.js'
|
|
4
|
+
import '@material/web/icon/icon.js'
|
|
5
|
+
|
|
6
|
+
import { PageView } from '@operato/shell'
|
|
7
|
+
import { css, html, LitElement } from 'lit'
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
9
|
+
import { client } from '@operato/graphql'
|
|
10
|
+
import { i18next, localize } from '@operato/i18n'
|
|
11
|
+
import { notify } from '@operato/layout'
|
|
12
|
+
import { OxPopup } from '@operato/popup'
|
|
13
|
+
import gql from 'graphql-tag'
|
|
14
|
+
import { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'
|
|
15
|
+
|
|
16
|
+
const VIZ_TYPES = [
|
|
17
|
+
{ value: 'CARD', label: '카드', icon: 'dashboard' },
|
|
18
|
+
{ value: 'GAUGE', label: '게이지', icon: 'speed' },
|
|
19
|
+
{ value: 'PROGRESS', label: '진행률', icon: 'linear_scale' },
|
|
20
|
+
{ value: 'BAR', label: '막대 차트', icon: 'bar_chart' },
|
|
21
|
+
{ value: 'LINE', label: '선 차트', icon: 'show_chart' },
|
|
22
|
+
{ value: 'PIE', label: '파이 차트', icon: 'pie_chart' },
|
|
23
|
+
{ value: 'DONUT', label: '도넛 차트', icon: 'donut_large' },
|
|
24
|
+
{ value: 'RADAR', label: '레이더 차트', icon: 'radar' },
|
|
25
|
+
{ value: 'BULLET', label: '불릿 차트', icon: 'track_changes' },
|
|
26
|
+
{ value: 'THERMOMETER', label: '온도계', icon: 'thermostat' },
|
|
27
|
+
{ value: 'SPEEDOMETER', label: '속도계', icon: 'speed' },
|
|
28
|
+
{ value: 'ICON', label: '아이콘', icon: 'emoji_events' },
|
|
29
|
+
{ value: 'BADGE', label: '배지', icon: 'badge' },
|
|
30
|
+
{ value: 'TEXT', label: '텍스트', icon: 'text_fields' },
|
|
31
|
+
{ value: 'TABLE', label: '테이블', icon: 'table_chart' }
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
@customElement('kpi-viz-editor')
|
|
35
|
+
export class KpiVizEditor extends localize(i18next)(LitElement) {
|
|
36
|
+
static styles = [
|
|
37
|
+
CommonHeaderStyles,
|
|
38
|
+
ScrollbarStyles,
|
|
39
|
+
css`
|
|
40
|
+
:host {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
background-color: var(--md-sys-color-surface, #f4f6fa);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.viz-editor {
|
|
47
|
+
flex: 1;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
overflow-y: auto;
|
|
51
|
+
|
|
52
|
+
background: white;
|
|
53
|
+
padding: 24px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.form-group {
|
|
57
|
+
margin-bottom: 20px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.form-group label {
|
|
61
|
+
display: block;
|
|
62
|
+
margin-bottom: 8px;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
color: #555;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.form-options {
|
|
68
|
+
flex: 1;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: row;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.viz-type-grid {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
76
|
+
gap: 12px;
|
|
77
|
+
margin-bottom: 20px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.viz-type-option {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
align-items: center;
|
|
84
|
+
padding: 16px 12px;
|
|
85
|
+
border: 2px solid #e0e0e0;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
transition: all 0.2s;
|
|
89
|
+
text-align: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.viz-type-option:hover {
|
|
93
|
+
border-color: #2196f3;
|
|
94
|
+
background: #f5f9ff;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.viz-type-option.selected {
|
|
98
|
+
border-color: #2196f3;
|
|
99
|
+
background: #e3f2fd;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.viz-type-option md-icon {
|
|
103
|
+
font-size: 24px;
|
|
104
|
+
margin-bottom: 8px;
|
|
105
|
+
color: #666;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.viz-type-option.selected md-icon {
|
|
109
|
+
color: #2196f3;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.viz-type-option .label {
|
|
113
|
+
font-size: 0.9rem;
|
|
114
|
+
font-weight: 500;
|
|
115
|
+
color: #333;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.viz-meta-section {
|
|
119
|
+
margin-top: 24px;
|
|
120
|
+
padding-top: 20px;
|
|
121
|
+
border-top: 1px solid #eee;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.color-picker {
|
|
125
|
+
display: flex;
|
|
126
|
+
gap: 12px;
|
|
127
|
+
align-items: center;
|
|
128
|
+
margin-bottom: 16px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.color-input {
|
|
132
|
+
width: 60px;
|
|
133
|
+
height: 40px;
|
|
134
|
+
border: none;
|
|
135
|
+
border-radius: 6px;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.buttons {
|
|
140
|
+
display: flex;
|
|
141
|
+
gap: 12px;
|
|
142
|
+
justify-content: flex-end;
|
|
143
|
+
margin-top: 24px;
|
|
144
|
+
padding-top: 20px;
|
|
145
|
+
border-top: 1px solid #eee;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.preview {
|
|
149
|
+
flex: 1;
|
|
150
|
+
margin: 30px 16px 16px 16px;
|
|
151
|
+
padding: 16px;
|
|
152
|
+
background: #f8f9fa;
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
border: 1px solid #e9ecef;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.preview h4 {
|
|
158
|
+
margin: 0 0 12px 0;
|
|
159
|
+
color: #495057;
|
|
160
|
+
font-size: 0.95rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.footer span {
|
|
164
|
+
font-size: 0.8em;
|
|
165
|
+
color: var(--md-sys-color-on-surface);
|
|
166
|
+
line-height: 1.5;
|
|
167
|
+
padding: 10px;
|
|
168
|
+
}
|
|
169
|
+
`
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
@property({ type: Object }) kpi: any = null
|
|
173
|
+
@property({ type: String }) selectedVizType: string = 'CARD'
|
|
174
|
+
@property({ type: Object }) vizMeta: any = {}
|
|
175
|
+
@property({ type: Object }) onSave: Function = () => {}
|
|
176
|
+
@property({ type: Object }) onCancel: Function = () => {}
|
|
177
|
+
|
|
178
|
+
connectedCallback() {
|
|
179
|
+
super.connectedCallback()
|
|
180
|
+
if (this.kpi) {
|
|
181
|
+
this.selectedVizType = this.kpi.vizType || 'CARD'
|
|
182
|
+
this.vizMeta = this.kpi.vizMeta || {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_selectVizType(type: string) {
|
|
187
|
+
this.selectedVizType = type
|
|
188
|
+
this.requestUpdate()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_updateVizMeta(key: string, value: any) {
|
|
192
|
+
this.vizMeta = { ...this.vizMeta, [key]: value }
|
|
193
|
+
this.requestUpdate()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_renderPreview() {
|
|
197
|
+
const kpiValue = this.kpi?.value?.value || 75
|
|
198
|
+
const targetValue = this.kpi?.targetValue || 100
|
|
199
|
+
const unit = this.kpi?.unit || ''
|
|
200
|
+
const color = this.vizMeta.color || '#2196f3'
|
|
201
|
+
const icon = this.vizMeta.icon || 'trending_up'
|
|
202
|
+
|
|
203
|
+
switch (this.selectedVizType) {
|
|
204
|
+
case 'CARD':
|
|
205
|
+
return html`
|
|
206
|
+
<div
|
|
207
|
+
style="display:flex;align-items:center;gap:12px;padding:16px;background:white;border-radius:8px;border:1px solid #e0e0e0;"
|
|
208
|
+
>
|
|
209
|
+
<md-icon style="color:${color};font-size:32px;">${icon}</md-icon>
|
|
210
|
+
<div>
|
|
211
|
+
<div style="font-size:1.5rem;font-weight:bold;color:${color};">${kpiValue}${unit}</div>
|
|
212
|
+
<div style="font-size:0.9rem;color:#666;">목표: ${targetValue}${unit}</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
`
|
|
216
|
+
case 'GAUGE':
|
|
217
|
+
const percentage = Math.min((kpiValue / targetValue) * 100, 100)
|
|
218
|
+
return html`
|
|
219
|
+
<div style="text-align:center;padding:16px;">
|
|
220
|
+
<div
|
|
221
|
+
style="width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} 0deg ${percentage *
|
|
222
|
+
3.6}deg, #e0e0e0 ${percentage * 3.6}deg 360deg);margin:0 auto;position:relative;"
|
|
223
|
+
>
|
|
224
|
+
<div
|
|
225
|
+
style="position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};"
|
|
226
|
+
>
|
|
227
|
+
${kpiValue}${unit}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
`
|
|
232
|
+
case 'PROGRESS':
|
|
233
|
+
const progressPercentage = Math.min((kpiValue / targetValue) * 100, 100)
|
|
234
|
+
return html`
|
|
235
|
+
<div style="padding:16px;">
|
|
236
|
+
<div style="background:#e0e0e0;height:20px;border-radius:10px;overflow:hidden;">
|
|
237
|
+
<div style="background:${color};height:100%;width:${progressPercentage}%;transition:width 0.3s;"></div>
|
|
238
|
+
</div>
|
|
239
|
+
<div style="text-align:center;margin-top:8px;font-weight:bold;color:${color};">
|
|
240
|
+
${kpiValue}${unit} / ${targetValue}${unit}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
`
|
|
244
|
+
case 'ICON':
|
|
245
|
+
return html`
|
|
246
|
+
<div style="text-align:center;padding:16px;">
|
|
247
|
+
<md-icon style="color:${color};font-size:48px;">${icon}</md-icon>
|
|
248
|
+
<div style="font-size:1.2rem;font-weight:bold;color:${color};margin-top:8px;">${kpiValue}${unit}</div>
|
|
249
|
+
</div>
|
|
250
|
+
`
|
|
251
|
+
default:
|
|
252
|
+
return html` <div style="padding:16px;text-align:center;color:#666;">${this.selectedVizType} 미리보기</div> `
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
render() {
|
|
257
|
+
return html`
|
|
258
|
+
<div class="viz-editor">
|
|
259
|
+
<div class="form-group">
|
|
260
|
+
<label>시각화 타입 선택</label>
|
|
261
|
+
<div class="viz-type-grid">
|
|
262
|
+
${VIZ_TYPES.map(
|
|
263
|
+
type => html`
|
|
264
|
+
<div
|
|
265
|
+
class="viz-type-option ${this.selectedVizType === type.value ? 'selected' : ''}"
|
|
266
|
+
@click=${() => this._selectVizType(type.value)}
|
|
267
|
+
>
|
|
268
|
+
<md-icon>${type.icon}</md-icon>
|
|
269
|
+
<div class="label">${type.label}</div>
|
|
270
|
+
</div>
|
|
271
|
+
`
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="form-options">
|
|
277
|
+
<div class="viz-meta-section">
|
|
278
|
+
<label>시각화 옵션</label>
|
|
279
|
+
|
|
280
|
+
<div class="form-group">
|
|
281
|
+
<label>색상</label>
|
|
282
|
+
<div class="color-picker">
|
|
283
|
+
<input
|
|
284
|
+
type="color"
|
|
285
|
+
class="color-input"
|
|
286
|
+
.value=${this.vizMeta.color || '#2196f3'}
|
|
287
|
+
@change=${(e: any) => this._updateVizMeta('color', e.target.value)}
|
|
288
|
+
/>
|
|
289
|
+
<md-outlined-text-field
|
|
290
|
+
label="색상 코드"
|
|
291
|
+
.value=${this.vizMeta.color || '#2196f3'}
|
|
292
|
+
@change=${(e: any) => this._updateVizMeta('color', e.target.value)}
|
|
293
|
+
></md-outlined-text-field>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="form-group">
|
|
298
|
+
<label>아이콘</label>
|
|
299
|
+
<md-outlined-text-field
|
|
300
|
+
label="Material Icons 이름"
|
|
301
|
+
.value=${this.vizMeta.icon || 'trending_up'}
|
|
302
|
+
@change=${(e: any) => this._updateVizMeta('icon', e.target.value)}
|
|
303
|
+
></md-outlined-text-field>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div class="form-group">
|
|
307
|
+
<label>최소값</label>
|
|
308
|
+
<md-outlined-text-field
|
|
309
|
+
type="number"
|
|
310
|
+
label="최소값"
|
|
311
|
+
.value=${this.vizMeta.minValue || 0}
|
|
312
|
+
@change=${(e: any) => this._updateVizMeta('minValue', parseFloat(e.target.value))}
|
|
313
|
+
></md-outlined-text-field>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div class="form-group">
|
|
317
|
+
<label>최대값</label>
|
|
318
|
+
<md-outlined-text-field
|
|
319
|
+
type="number"
|
|
320
|
+
label="최대값"
|
|
321
|
+
.value=${this.vizMeta.maxValue || 100}
|
|
322
|
+
@change=${(e: any) => this._updateVizMeta('maxValue', parseFloat(e.target.value))}
|
|
323
|
+
></md-outlined-text-field>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div class="form-group">
|
|
327
|
+
<label>소수점 자릿수</label>
|
|
328
|
+
<md-outlined-text-field
|
|
329
|
+
type="number"
|
|
330
|
+
label="소수점 자릿수"
|
|
331
|
+
.value=${this.vizMeta.decimalPlaces || 0}
|
|
332
|
+
@change=${(e: any) => this._updateVizMeta('decimalPlaces', parseInt(e.target.value))}
|
|
333
|
+
></md-outlined-text-field>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div class="preview">
|
|
338
|
+
<h4>미리보기</h4>
|
|
339
|
+
${this._renderPreview()}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div class="footer">
|
|
345
|
+
<div filler></div>
|
|
346
|
+
<button type="button" @click=${this.onCancel}><md-icon>cancel</md-icon>취소</button>
|
|
347
|
+
<button type="button" @click=${() => this.onSave(this.selectedVizType, this.vizMeta)} done>
|
|
348
|
+
<md-icon>save</md-icon>저장
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
`
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -5,6 +5,7 @@ import { ScrollbarStyles } from '@operato/styles'
|
|
|
5
5
|
import { client } from '@operato/graphql'
|
|
6
6
|
import gql from 'graphql-tag'
|
|
7
7
|
import { state } from 'lit/decorators.js'
|
|
8
|
+
import '@material/web/icon/icon.js'
|
|
8
9
|
|
|
9
10
|
import './kpi-performance-summary'
|
|
10
11
|
import './kpi-grade-visualization'
|
|
@@ -110,6 +111,8 @@ export class KpiDashboardPage extends PageView {
|
|
|
110
111
|
targetValue
|
|
111
112
|
unit
|
|
112
113
|
grades
|
|
114
|
+
vizType
|
|
115
|
+
vizMeta
|
|
113
116
|
histories(limit: 1) {
|
|
114
117
|
version
|
|
115
118
|
updatedAt
|
|
@@ -174,6 +177,130 @@ export class KpiDashboardPage extends PageView {
|
|
|
174
177
|
this.modalKpiName = ''
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
_renderKpiValue(kpi: any) {
|
|
181
|
+
const kpiValue = kpi.value?.value ?? 0
|
|
182
|
+
const targetValue = kpi.targetValue ?? 100
|
|
183
|
+
const unit = kpi.unit ?? ''
|
|
184
|
+
const vizType = kpi.vizType || 'CARD'
|
|
185
|
+
const vizMeta = kpi.vizMeta || {}
|
|
186
|
+
const color = vizMeta.color || '#3a3ad6'
|
|
187
|
+
const icon = vizMeta.icon || 'trending_up'
|
|
188
|
+
const minValue = vizMeta.minValue || 0
|
|
189
|
+
const maxValue = vizMeta.maxValue || 100
|
|
190
|
+
const decimalPlaces = vizMeta.decimalPlaces || 0
|
|
191
|
+
|
|
192
|
+
const formattedValue = typeof kpiValue === 'number' ? kpiValue.toFixed(decimalPlaces) : kpiValue
|
|
193
|
+
|
|
194
|
+
switch (vizType) {
|
|
195
|
+
case 'GAUGE':
|
|
196
|
+
const gaugePercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)
|
|
197
|
+
return html`
|
|
198
|
+
<div style="text-align:center;margin:16px 0;">
|
|
199
|
+
<div
|
|
200
|
+
style="width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} 0deg ${gaugePercentage *
|
|
201
|
+
3.6}deg, #e0e0e0 ${gaugePercentage * 3.6}deg 360deg);margin:0 auto;position:relative;"
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
style="position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};"
|
|
205
|
+
>
|
|
206
|
+
${formattedValue}${unit}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
`
|
|
211
|
+
|
|
212
|
+
case 'PROGRESS':
|
|
213
|
+
const progressPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)
|
|
214
|
+
return html`
|
|
215
|
+
<div style="margin:16px 0;">
|
|
216
|
+
<div style="background:#e0e0e0;height:20px;border-radius:10px;overflow:hidden;">
|
|
217
|
+
<div style="background:${color};height:100%;width:${progressPercentage}%;transition:width 0.3s;"></div>
|
|
218
|
+
</div>
|
|
219
|
+
<div style="text-align:center;margin-top:8px;font-weight:bold;color:${color};font-size:1.2rem;">
|
|
220
|
+
${formattedValue}${unit}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
`
|
|
224
|
+
|
|
225
|
+
case 'ICON':
|
|
226
|
+
return html`
|
|
227
|
+
<div style="text-align:center;margin:16px 0;">
|
|
228
|
+
<md-icon style="color:${color};font-size:48px;">${icon}</md-icon>
|
|
229
|
+
<div style="font-size:1.5rem;font-weight:bold;color:${color};margin-top:8px;">${formattedValue}${unit}</div>
|
|
230
|
+
</div>
|
|
231
|
+
`
|
|
232
|
+
|
|
233
|
+
case 'THERMOMETER':
|
|
234
|
+
const thermoPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)
|
|
235
|
+
return html`
|
|
236
|
+
<div style="text-align:center;margin:16px 0;">
|
|
237
|
+
<div
|
|
238
|
+
style="width:40px;height:120px;background:#e0e0e0;border-radius:20px;margin:0 auto;position:relative;overflow:hidden;"
|
|
239
|
+
>
|
|
240
|
+
<div
|
|
241
|
+
style="position:absolute;bottom:0;width:100%;height:${thermoPercentage}%;background:${color};border-radius:20px;"
|
|
242
|
+
></div>
|
|
243
|
+
</div>
|
|
244
|
+
<div style="font-size:1.2rem;font-weight:bold;color:${color};margin-top:8px;">${formattedValue}${unit}</div>
|
|
245
|
+
</div>
|
|
246
|
+
`
|
|
247
|
+
|
|
248
|
+
case 'SPEEDOMETER':
|
|
249
|
+
const speedPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)
|
|
250
|
+
const angle = (speedPercentage / 100) * 180 - 90 // -90도에서 90도까지
|
|
251
|
+
return html`
|
|
252
|
+
<div style="text-align:center;margin:16px 0;">
|
|
253
|
+
<div
|
|
254
|
+
style="width:120px;height:60px;border-radius:60px 60px 0 0;background:conic-gradient(${color} -90deg ${angle}deg, #e0e0e0 ${angle}deg 90deg);margin:0 auto;position:relative;"
|
|
255
|
+
>
|
|
256
|
+
<div
|
|
257
|
+
style="position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-size:1.2rem;font-weight:bold;color:${color};"
|
|
258
|
+
>
|
|
259
|
+
${formattedValue}${unit}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
`
|
|
264
|
+
|
|
265
|
+
case 'BULLET':
|
|
266
|
+
const bulletPercentage = Math.min(((kpiValue - minValue) / (maxValue - minValue)) * 100, 100)
|
|
267
|
+
const targetPercentage = Math.min(((targetValue - minValue) / (maxValue - minValue)) * 100, 100)
|
|
268
|
+
return html`
|
|
269
|
+
<div style="margin:16px 0;">
|
|
270
|
+
<div style="background:#e0e0e0;height:30px;border-radius:15px;overflow:hidden;position:relative;">
|
|
271
|
+
<div style="background:${color};height:100%;width:${bulletPercentage}%;transition:width 0.3s;"></div>
|
|
272
|
+
<div
|
|
273
|
+
style="position:absolute;top:0;left:${targetPercentage}%;width:2px;height:100%;background:#333;"
|
|
274
|
+
></div>
|
|
275
|
+
</div>
|
|
276
|
+
<div style="text-align:center;margin-top:8px;font-weight:bold;color:${color};font-size:1.2rem;">
|
|
277
|
+
${formattedValue}${unit}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
`
|
|
281
|
+
|
|
282
|
+
case 'TEXT':
|
|
283
|
+
return html`
|
|
284
|
+
<div style="text-align:center;margin:16px 0;">
|
|
285
|
+
<div style="font-size:1.5rem;font-weight:bold;color:${color};">${formattedValue}${unit}</div>
|
|
286
|
+
</div>
|
|
287
|
+
`
|
|
288
|
+
|
|
289
|
+
case 'BADGE':
|
|
290
|
+
return html`
|
|
291
|
+
<div style="text-align:center;margin:16px 0;">
|
|
292
|
+
<span
|
|
293
|
+
style="display:inline-block;padding:8px 16px;background:${color};color:white;border-radius:20px;font-size:1.2rem;font-weight:bold;"
|
|
294
|
+
>${formattedValue}${unit}</span
|
|
295
|
+
>
|
|
296
|
+
</div>
|
|
297
|
+
`
|
|
298
|
+
|
|
299
|
+
default: // CARD, BAR, LINE, PIE, DONUT, RADAR, TABLE
|
|
300
|
+
return html` <div class="kpi-value">${formattedValue}${unit}</div> `
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
177
304
|
get context() {
|
|
178
305
|
return {
|
|
179
306
|
title: 'KPI 대시보드',
|
|
@@ -223,7 +350,7 @@ export class KpiDashboardPage extends PageView {
|
|
|
223
350
|
kpi => html`
|
|
224
351
|
<div class="kpi-card">
|
|
225
352
|
<div class="kpi-name">${kpi.name}</div>
|
|
226
|
-
|
|
353
|
+
${this._renderKpiValue(kpi)}
|
|
227
354
|
<div class="kpi-target">목표: ${kpi.targetValue ?? '-'}${kpi.unit ?? ''}</div>
|
|
228
355
|
<!-- 등급 시각화 -->
|
|
229
356
|
<div style="margin-top:8px;">
|
|
@@ -3,22 +3,25 @@ import '@material/web/button/elevated-button.js';
|
|
|
3
3
|
import '@operato/data-grist/ox-grist.js';
|
|
4
4
|
import '@operato/data-grist/ox-filters-form.js';
|
|
5
5
|
import '@operato/data-grist/ox-record-creator.js';
|
|
6
|
+
import './kpi-viz-editor.js';
|
|
6
7
|
import { PageView } from '@operato/shell';
|
|
7
8
|
import { FetchOption } from '@operato/data-grist';
|
|
8
9
|
import { KpiImporter } from './kpi-importer';
|
|
9
10
|
import { KpiGradeEditor } from './kpi-grade-editor';
|
|
11
|
+
import { KpiVizEditor } from './kpi-viz-editor';
|
|
10
12
|
declare const KpiListPage_base: (new (...args: any[]) => {
|
|
11
13
|
_storeUnsubscribe: import("redux").Unsubscribe;
|
|
12
14
|
connectedCallback(): void;
|
|
13
15
|
disconnectedCallback(): void;
|
|
14
16
|
stateChanged(_state: unknown): void;
|
|
15
17
|
readonly isConnected: boolean;
|
|
16
|
-
}) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView & import("@open-wc/dedupe-mixin").Constructor<import("@open-wc/scoped-elements/types/src/types").ScopedElementsHost>;
|
|
18
|
+
}) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView & import("@open-wc/dedupe-mixin").Constructor<import("@open-wc/scoped-elements/types/src/types.js").ScopedElementsHost>;
|
|
17
19
|
export declare class KpiListPage extends KpiListPage_base {
|
|
18
20
|
static styles: import("lit").CSSResult[];
|
|
19
21
|
static get scopedElements(): {
|
|
20
22
|
'kpi-importer': typeof KpiImporter;
|
|
21
23
|
'kpi-grade-editor': typeof KpiGradeEditor;
|
|
24
|
+
'kpi-viz-editor': typeof KpiVizEditor;
|
|
22
25
|
};
|
|
23
26
|
gristConfig: any;
|
|
24
27
|
mode: 'CARD' | 'GRID' | 'LIST';
|
|
@@ -66,5 +69,7 @@ export declare class KpiListPage extends KpiListPage_base {
|
|
|
66
69
|
importHandler(records: any): Promise<void>;
|
|
67
70
|
_editGrades(kpi: any): Promise<void>;
|
|
68
71
|
_onGradesUpdated(detail: any): Promise<void>;
|
|
72
|
+
_editViz(kpi: any): Promise<void>;
|
|
73
|
+
_onVizUpdated(kpiId: string, vizType: string, vizMeta: any): Promise<void>;
|
|
69
74
|
}
|
|
70
75
|
export {};
|