@things-factory/kpi 9.0.15 → 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 +53 -11
- 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-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 +6 -2
- package/dist-client/pages/kpi/kpi-list-page.js +50 -10
- 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/index.d.ts +2 -2
- 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/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/kpi/kpi-type.ts +7 -7
- package/server/service/kpi/kpi.ts +27 -4
|
@@ -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;">
|
|
@@ -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 {};
|