@things-factory/kpi 9.0.23 → 9.0.25
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-viz-editor.ts +1 -1
- package/client/pages/kpi-category-value/kpi-category-value-list-page.ts +404 -0
- package/client/pages/kpi-metric-value/kpi-metric-value-editor-page.ts +763 -0
- package/client/pages/kpi-metric-value/kpi-metric-value-list-page.ts +12 -0
- package/client/pages/kpi-value/kpi-value-editor-page.ts +774 -0
- package/client/pages/kpi-value/kpi-value-list-page.ts +13 -0
- package/client/route.ts +16 -0
- package/dist-client/pages/kpi/kpi-viz-editor.d.ts +0 -1
- package/dist-client/pages/kpi/kpi-viz-editor.js +1 -1
- package/dist-client/pages/kpi/kpi-viz-editor.js.map +1 -1
- package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.d.ts +63 -0
- package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js +393 -0
- package/dist-client/pages/kpi-category-value/kpi-category-value-list-page.js.map +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +58 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +736 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +11 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-editor-page.d.ts +55 -0
- package/dist-client/pages/kpi-value/kpi-value-editor-page.js +748 -0
- package/dist-client/pages/kpi-value/kpi-value-editor-page.js.map +1 -0
- package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +9 -2
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +12 -0
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +12 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +5 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-mutation.d.ts +2 -3
- package/dist-server/service/kpi-category/kpi-category-mutation.js +149 -78
- package/dist-server/service/kpi-category/kpi-category-mutation.js.map +1 -1
- package/dist-server/service/kpi-category/kpi-category-query.d.ts +1 -9
- package/dist-server/service/kpi-category/kpi-category-query.js +3 -165
- package/dist-server/service/kpi-category/kpi-category-query.js.map +1 -1
- package/dist-server/service/kpi-category-value/index.d.ts +6 -0
- package/dist-server/service/kpi-category-value/index.js +10 -0
- package/dist-server/service/kpi-category-value/index.js.map +1 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-mutation.d.ts +8 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js +102 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-mutation.js.map +1 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-query.d.ts +13 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-query.js +91 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-query.js.map +1 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-type.d.ts +19 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-type.js +73 -0
- package/dist-server/service/kpi-category-value/kpi-category-value-type.js.map +1 -0
- package/dist-server/service/kpi-category-value/kpi-category-value.d.ts +19 -0
- package/dist-server/service/kpi-category-value/kpi-category-value.js +91 -0
- package/dist-server/service/kpi-category-value/kpi-category-value.js.map +1 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +20 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-mutation.d.ts +1 -0
- package/dist-server/service/kpi-value/kpi-value-mutation.js +60 -0
- package/dist-server/service/kpi-value/kpi-value-mutation.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/index.ts +5 -0
- package/server/service/kpi-category/kpi-category-mutation.ts +154 -81
- package/server/service/kpi-category/kpi-category-query.ts +1 -155
- package/server/service/kpi-category-value/index.ts +7 -0
- package/server/service/kpi-category-value/kpi-category-value-mutation.ts +88 -0
- package/server/service/kpi-category-value/kpi-category-value-query.ts +62 -0
- package/server/service/kpi-category-value/kpi-category-value-type.ts +48 -0
- package/server/service/kpi-category-value/kpi-category-value.ts +79 -0
- package/server/service/kpi-metric-value/kpi-metric-value-mutation.ts +28 -0
- package/server/service/kpi-value/kpi-value-mutation.ts +66 -0
- package/things-factory.config.js +3 -0
- package/translations/en.json +3 -0
- package/translations/ja.json +3 -0
- package/translations/ko.json +3 -0
- package/translations/ms.json +3 -0
- package/translations/zh.json +3 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
import '@material/web/button/elevated-button.js'
|
|
3
|
+
import '@material/web/textfield/outlined-text-field.js'
|
|
4
|
+
|
|
5
|
+
import { CommonButtonStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'
|
|
6
|
+
import { PageView, store } from '@operato/shell'
|
|
7
|
+
import { css, html } from 'lit'
|
|
8
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
9
|
+
import { ScopedElementsMixin } from '@open-wc/scoped-elements'
|
|
10
|
+
import { client } from '@operato/graphql'
|
|
11
|
+
import { i18next, localize } from '@operato/i18n'
|
|
12
|
+
import { notify } from '@operato/layout'
|
|
13
|
+
import { connect } from 'pwa-helpers/connect-mixin'
|
|
14
|
+
import gql from 'graphql-tag'
|
|
15
|
+
|
|
16
|
+
interface KpiMetricValueData {
|
|
17
|
+
metricId: string
|
|
18
|
+
metricName: string
|
|
19
|
+
periodType: string
|
|
20
|
+
values: { [date: string]: { value: number | null; isDirty?: boolean } }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EditorCell {
|
|
24
|
+
date: string
|
|
25
|
+
value: number | null
|
|
26
|
+
isEditable: boolean
|
|
27
|
+
isHighlighted: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@customElement('kpi-metric-value-editor-page')
|
|
31
|
+
export class KpiMetricValueEditorPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
|
|
32
|
+
static styles = [
|
|
33
|
+
CommonHeaderStyles,
|
|
34
|
+
ScrollbarStyles,
|
|
35
|
+
css`
|
|
36
|
+
:host {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
padding: 20px;
|
|
40
|
+
overflow-x: auto;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.header {
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: 16px;
|
|
46
|
+
align-items: center;
|
|
47
|
+
margin-bottom: 20px;
|
|
48
|
+
padding: 16px;
|
|
49
|
+
background: var(--md-sys-color-surface-container);
|
|
50
|
+
border-radius: 8px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.controls {
|
|
54
|
+
display: flex;
|
|
55
|
+
gap: 12px;
|
|
56
|
+
align-items: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.table-container {
|
|
60
|
+
flex: 1;
|
|
61
|
+
overflow: auto;
|
|
62
|
+
border: 1px solid var(--md-sys-color-outline);
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
table {
|
|
67
|
+
width: 100%;
|
|
68
|
+
border-collapse: collapse;
|
|
69
|
+
min-width: max-content;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
th {
|
|
73
|
+
background: var(--md-sys-color-surface-container-low);
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
padding: 8px 12px;
|
|
76
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
77
|
+
min-width: 80px;
|
|
78
|
+
height: 120px;
|
|
79
|
+
vertical-align: middle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
td {
|
|
83
|
+
padding: 8px 12px;
|
|
84
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
85
|
+
min-width: 80px;
|
|
86
|
+
height: 60px;
|
|
87
|
+
text-align: right;
|
|
88
|
+
vertical-align: middle;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.metric-header {
|
|
92
|
+
position: sticky;
|
|
93
|
+
left: 0;
|
|
94
|
+
top: 0;
|
|
95
|
+
z-index: 3;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.metric-name {
|
|
99
|
+
position: sticky;
|
|
100
|
+
left: 0;
|
|
101
|
+
background: var(--md-sys-color-surface);
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
min-width: 200px;
|
|
104
|
+
text-align: left;
|
|
105
|
+
z-index: 2;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
tr:hover {
|
|
109
|
+
background: var(--md-sys-color-surface-container-high);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
th.date-header {
|
|
113
|
+
position: sticky;
|
|
114
|
+
top: 0;
|
|
115
|
+
z-index: 2;
|
|
116
|
+
text-align: center;
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
119
|
+
writing-mode: vertical-rl;
|
|
120
|
+
text-orientation: mixed;
|
|
121
|
+
min-height: 120px;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
td.editable-cell {
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
background: var(--md-sys-color-primary-container);
|
|
130
|
+
color: var(--md-sys-color-on-primary-container);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
td.editable-cell:hover {
|
|
134
|
+
background: var(--md-sys-color-primary-container-high);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
td.highlighted-cell {
|
|
138
|
+
background: var(--md-sys-color-secondary-container);
|
|
139
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
140
|
+
font-weight: 500;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
td.highlighted-cell:hover {
|
|
144
|
+
background: var(--md-sys-color-secondary-container-high);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
td.disabled-cell {
|
|
148
|
+
background: var(--md-sys-color-surface-container);
|
|
149
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
150
|
+
cursor: not-allowed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.value-input {
|
|
154
|
+
width: 100%;
|
|
155
|
+
text-align: center;
|
|
156
|
+
border: none;
|
|
157
|
+
background: transparent;
|
|
158
|
+
color: inherit;
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
padding: 2px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.value-input:focus {
|
|
164
|
+
outline: 2px solid var(--md-sys-color-primary);
|
|
165
|
+
border-radius: 4px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.period-type-badge {
|
|
169
|
+
font-size: 10px;
|
|
170
|
+
padding: 2px 6px;
|
|
171
|
+
border-radius: 4px;
|
|
172
|
+
background: var(--md-sys-color-tertiary-container);
|
|
173
|
+
color: var(--md-sys-color-on-tertiary-container);
|
|
174
|
+
margin-left: 8px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.loading {
|
|
178
|
+
display: flex;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
align-items: center;
|
|
181
|
+
height: 200px;
|
|
182
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.error {
|
|
186
|
+
color: var(--md-sys-color-error);
|
|
187
|
+
padding: 16px;
|
|
188
|
+
text-align: center;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.legend {
|
|
192
|
+
display: flex;
|
|
193
|
+
gap: 16px;
|
|
194
|
+
margin-top: 16px;
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.legend-item {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 4px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.legend-color {
|
|
205
|
+
width: 16px;
|
|
206
|
+
height: 16px;
|
|
207
|
+
border-radius: 2px;
|
|
208
|
+
}
|
|
209
|
+
`
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
@property({ type: String }) group: string = ''
|
|
213
|
+
@property({ type: String }) startDate: string = ''
|
|
214
|
+
@property({ type: String }) endDate: string = ''
|
|
215
|
+
|
|
216
|
+
@state() private metrics: KpiMetricValueData[] = []
|
|
217
|
+
@state() private dates: string[] = []
|
|
218
|
+
@state() private loading: boolean = false
|
|
219
|
+
@state() private error: string = ''
|
|
220
|
+
@state() private editingCell: { metricId: string; date: string } | null = null
|
|
221
|
+
@state() private _existingValues: any[] = []
|
|
222
|
+
|
|
223
|
+
get context() {
|
|
224
|
+
return {
|
|
225
|
+
title: i18next.t('title.kpi metric value editor'),
|
|
226
|
+
actions: [
|
|
227
|
+
{
|
|
228
|
+
title: i18next.t('button.save'),
|
|
229
|
+
action: this._saveValues.bind(this),
|
|
230
|
+
...CommonButtonStyles.save
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
title: i18next.t('button.cancel'),
|
|
234
|
+
action: this._cancel.bind(this),
|
|
235
|
+
...CommonButtonStyles.cancel
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
render() {
|
|
242
|
+
if (this.loading) {
|
|
243
|
+
return html`
|
|
244
|
+
<div class="loading">
|
|
245
|
+
<md-icon>hourglass_empty</md-icon>
|
|
246
|
+
<span>데이터를 불러오는 중...</span>
|
|
247
|
+
</div>
|
|
248
|
+
`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.error) {
|
|
252
|
+
return html`
|
|
253
|
+
<div class="error">
|
|
254
|
+
<md-icon>error</md-icon>
|
|
255
|
+
<span>${this.error}</span>
|
|
256
|
+
</div>
|
|
257
|
+
`
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return html`
|
|
261
|
+
<div class="header">
|
|
262
|
+
<div class="controls">
|
|
263
|
+
<md-outlined-text-field
|
|
264
|
+
label="그룹"
|
|
265
|
+
.value=${this.group}
|
|
266
|
+
@input=${(e: any) => (this.group = e.target.value)}
|
|
267
|
+
style="width: 150px;"
|
|
268
|
+
></md-outlined-text-field>
|
|
269
|
+
|
|
270
|
+
<md-outlined-text-field
|
|
271
|
+
label="시작일"
|
|
272
|
+
type="date"
|
|
273
|
+
.value=${this.startDate}
|
|
274
|
+
@input=${(e: any) => (this.startDate = e.target.value)}
|
|
275
|
+
style="width: 150px;"
|
|
276
|
+
></md-outlined-text-field>
|
|
277
|
+
|
|
278
|
+
<md-outlined-text-field
|
|
279
|
+
label="종료일"
|
|
280
|
+
type="date"
|
|
281
|
+
.value=${this.endDate}
|
|
282
|
+
@input=${(e: any) => (this.endDate = e.target.value)}
|
|
283
|
+
style="width: 150px;"
|
|
284
|
+
></md-outlined-text-field>
|
|
285
|
+
|
|
286
|
+
<md-elevated-button @click=${this._loadData}>
|
|
287
|
+
<md-icon slot="icon">refresh</md-icon>
|
|
288
|
+
새로고침
|
|
289
|
+
</md-elevated-button>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div class="table-container">
|
|
294
|
+
<table style="border-collapse: collapse; width: 100%;">
|
|
295
|
+
<thead>
|
|
296
|
+
<tr>
|
|
297
|
+
<th
|
|
298
|
+
style="border: 1px solid #ccc; padding: 8px; background: #f5f5f5; min-width: 200px; left: 0; top: 0;"
|
|
299
|
+
class="metric-header"
|
|
300
|
+
>
|
|
301
|
+
Metric명
|
|
302
|
+
</th>
|
|
303
|
+
${this.dates.map(
|
|
304
|
+
date => html`
|
|
305
|
+
<th
|
|
306
|
+
style="border: 1px solid #ccc; padding: 8px; background: #f5f5f5; min-width: 80px; position: sticky; top: 0;"
|
|
307
|
+
>
|
|
308
|
+
${this._formatDate(date)}
|
|
309
|
+
</th>
|
|
310
|
+
`
|
|
311
|
+
)}
|
|
312
|
+
</tr>
|
|
313
|
+
</thead>
|
|
314
|
+
<tbody>
|
|
315
|
+
${this.metrics.map(
|
|
316
|
+
metric => html`
|
|
317
|
+
<tr>
|
|
318
|
+
<td
|
|
319
|
+
style="border: 1px solid #ccc; padding: 8px; background: #fff; min-width: 200px;"
|
|
320
|
+
class="metric-name"
|
|
321
|
+
>
|
|
322
|
+
${metric.metricName}
|
|
323
|
+
<span
|
|
324
|
+
style="font-size: 10px; padding: 2px 6px; background: #e0e0e0; border-radius: 4px; margin-left: 8px;"
|
|
325
|
+
>${metric.periodType}</span
|
|
326
|
+
>
|
|
327
|
+
</td>
|
|
328
|
+
${this.dates.map(
|
|
329
|
+
date => html`
|
|
330
|
+
<td
|
|
331
|
+
style="border: 1px solid #ccc; padding: 8px; background: #e3f2fd; text-align: center; min-width: 80px; cursor: pointer;"
|
|
332
|
+
@click=${() => this._startEdit(metric.metricId, date)}
|
|
333
|
+
>
|
|
334
|
+
${this._renderCellContent(metric, date)}
|
|
335
|
+
</td>
|
|
336
|
+
`
|
|
337
|
+
)}
|
|
338
|
+
</tr>
|
|
339
|
+
`
|
|
340
|
+
)}
|
|
341
|
+
</tbody>
|
|
342
|
+
</table>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<div class="legend">
|
|
346
|
+
<div class="legend-item">
|
|
347
|
+
<div class="legend-color" style="background: var(--md-sys-color-primary-container);"></div>
|
|
348
|
+
<span>편집 가능</span>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="legend-item">
|
|
351
|
+
<div class="legend-color" style="background: var(--md-sys-color-secondary-container);"></div>
|
|
352
|
+
<span>하이라이트 (PeriodType에 따라)</span>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="legend-item">
|
|
355
|
+
<div class="legend-color" style="background: var(--md-sys-color-surface-container);"></div>
|
|
356
|
+
<span>편집 불가</span>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
`
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private _renderCellContent(metric: KpiMetricValueData, date: string) {
|
|
363
|
+
const isEditing = this.editingCell?.metricId === metric.metricId && this.editingCell?.date === date
|
|
364
|
+
|
|
365
|
+
// periodType에 따라 값을 가져오는 방식 결정
|
|
366
|
+
let value: number | null = null
|
|
367
|
+
|
|
368
|
+
if (metric.periodType === 'DAY') {
|
|
369
|
+
// DAY인 경우 해당 날짜의 값을 사용
|
|
370
|
+
value = metric.values[date]?.value
|
|
371
|
+
} else if (metric.periodType === 'WEEK') {
|
|
372
|
+
// WEEK인 경우 해당 주의 월요일의 값을 사용
|
|
373
|
+
const weekKey = this._getWeekKey(date)
|
|
374
|
+
value = metric.values[weekKey]?.value
|
|
375
|
+
} else if (metric.periodType === 'MONTH') {
|
|
376
|
+
// MONTH인 경우 해당 월의 값을 사용
|
|
377
|
+
const monthKey = this._getMonthKey(date)
|
|
378
|
+
value = metric.values[monthKey]?.value
|
|
379
|
+
} else if (metric.periodType === 'QUARTER') {
|
|
380
|
+
// QUARTER인 경우 해당 분기의 값을 사용
|
|
381
|
+
const quarterKey = this._getQuarterKey(date)
|
|
382
|
+
value = metric.values[quarterKey]?.value
|
|
383
|
+
} else if (metric.periodType === 'YEAR') {
|
|
384
|
+
// YEAR인 경우 해당 연도의 값을 사용
|
|
385
|
+
const yearKey = this._getYearKey(date)
|
|
386
|
+
value = metric.values[yearKey]?.value
|
|
387
|
+
} else {
|
|
388
|
+
// 기본값: DAY와 동일
|
|
389
|
+
value = metric.values[date]?.value
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (isEditing) {
|
|
393
|
+
console.log('Rendering input for editing, value:', value)
|
|
394
|
+
return html`
|
|
395
|
+
<input
|
|
396
|
+
type="number"
|
|
397
|
+
step="0.01"
|
|
398
|
+
.value=${(value || '').toString()}
|
|
399
|
+
@blur=${(e: any) => this._finishEdit(metric.metricId, date, parseFloat(e.target.value) || 0)}
|
|
400
|
+
@keydown=${(e: any) => e.key === 'Enter' && e.target.blur()}
|
|
401
|
+
style="width: 100%; text-align: center; border: none; background: transparent;"
|
|
402
|
+
autofocus
|
|
403
|
+
/>
|
|
404
|
+
`
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return html`
|
|
408
|
+
<div style="display: flex; flex-direction: column; gap: 2px;">
|
|
409
|
+
<span style="font-weight: 500; color: ${value !== null && value !== undefined ? 'inherit' : '#999'};">
|
|
410
|
+
${value !== null && value !== undefined ? value.toLocaleString() : '클릭하여 입력'}
|
|
411
|
+
</span>
|
|
412
|
+
</div>
|
|
413
|
+
`
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private _formatDate(date: string): string {
|
|
417
|
+
const d = new Date(date)
|
|
418
|
+
return d.toLocaleDateString('ko-KR', { month: 'numeric', day: 'numeric' })
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private _startEdit(metricId: string, date: string) {
|
|
422
|
+
this.editingCell = { metricId, date }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private _finishEdit(metricId: string, date: string, value: number) {
|
|
426
|
+
const metric = this.metrics.find(m => m.metricId === metricId)
|
|
427
|
+
if (metric) {
|
|
428
|
+
// periodType에 따라 저장할 키 결정
|
|
429
|
+
let storageKey = date
|
|
430
|
+
|
|
431
|
+
if (metric.periodType === 'WEEK') {
|
|
432
|
+
storageKey = this._getWeekKey(date)
|
|
433
|
+
} else if (metric.periodType === 'MONTH') {
|
|
434
|
+
storageKey = this._getMonthKey(date)
|
|
435
|
+
} else if (metric.periodType === 'QUARTER') {
|
|
436
|
+
storageKey = this._getQuarterKey(date)
|
|
437
|
+
} else if (metric.periodType === 'YEAR') {
|
|
438
|
+
storageKey = this._getYearKey(date)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (!metric.values[storageKey]) {
|
|
442
|
+
metric.values[storageKey] = { value: 0, isDirty: false }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 값이 변경되었는지 확인
|
|
446
|
+
const originalValue = this._findExistingValue(metricId, storageKey)?.value
|
|
447
|
+
const isChanged = originalValue !== value
|
|
448
|
+
|
|
449
|
+
metric.values[storageKey].value = value
|
|
450
|
+
metric.values[storageKey].isDirty = isChanged
|
|
451
|
+
}
|
|
452
|
+
this.editingCell = null
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private async _loadData() {
|
|
456
|
+
if (!this.startDate || !this.endDate) {
|
|
457
|
+
this.error = '시작일, 종료일을 모두 입력해주세요.'
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.loading = true
|
|
462
|
+
this.error = ''
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
// KPI Metric 목록 조회
|
|
466
|
+
const metricsResponse = await client.query({
|
|
467
|
+
query: gql`
|
|
468
|
+
query ($filters: [Filter!]) {
|
|
469
|
+
kpiMetrics(filters: $filters) {
|
|
470
|
+
items {
|
|
471
|
+
id
|
|
472
|
+
name
|
|
473
|
+
periodType
|
|
474
|
+
active
|
|
475
|
+
}
|
|
476
|
+
total
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
`,
|
|
480
|
+
variables: {
|
|
481
|
+
filters: [{ name: 'active', operator: 'eq', value: true }]
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// KPI Metric Value 데이터 조회
|
|
486
|
+
const valuesResponse = await client.query({
|
|
487
|
+
query: gql`
|
|
488
|
+
query ($filters: [Filter!]) {
|
|
489
|
+
kpiMetricValues(filters: $filters) {
|
|
490
|
+
items {
|
|
491
|
+
id
|
|
492
|
+
metricId
|
|
493
|
+
valueDate
|
|
494
|
+
value
|
|
495
|
+
group
|
|
496
|
+
}
|
|
497
|
+
total
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
`,
|
|
501
|
+
variables: {
|
|
502
|
+
filters: [
|
|
503
|
+
...(this.group ? [{ name: 'group', operator: 'eq', value: this.group }] : []),
|
|
504
|
+
{ name: 'valueDate', operator: 'between', value: [this.startDate, this.endDate] }
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// 날짜 배열 생성
|
|
510
|
+
this.dates = this._generateDateArray(this.startDate, this.endDate)
|
|
511
|
+
|
|
512
|
+
// KPI Metric 목록을 기준으로 데이터 구성
|
|
513
|
+
this.metrics = metricsResponse.data.kpiMetrics.items.map((metric: any) => ({
|
|
514
|
+
metricId: metric.id,
|
|
515
|
+
metricName: metric.name,
|
|
516
|
+
periodType: metric.periodType,
|
|
517
|
+
values: {}
|
|
518
|
+
}))
|
|
519
|
+
|
|
520
|
+
// 기존 KPI Metric Value 데이터 저장
|
|
521
|
+
this._existingValues = valuesResponse.data.kpiMetricValues.items
|
|
522
|
+
|
|
523
|
+
// KPI Metric Value 데이터를 해당 Metric에 매핑
|
|
524
|
+
valuesResponse.data.kpiMetricValues.items.forEach((value: any) => {
|
|
525
|
+
const metric = this.metrics.find(m => m.metricId === value.metricId)
|
|
526
|
+
if (metric) {
|
|
527
|
+
metric.values[value.valueDate] = {
|
|
528
|
+
value: value.value,
|
|
529
|
+
isDirty: false
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// KPI Metric Value가 없는 Metric도 빈 값으로 초기화
|
|
535
|
+
this.metrics.forEach(metric => {
|
|
536
|
+
this.dates.forEach(date => {
|
|
537
|
+
if (!metric.values[date]) {
|
|
538
|
+
metric.values[date] = { value: null, isDirty: false }
|
|
539
|
+
}
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
this.requestUpdate()
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error('데이터 로드 중 오류:', error)
|
|
546
|
+
this.error = '데이터를 불러오는 중 오류가 발생했습니다.'
|
|
547
|
+
} finally {
|
|
548
|
+
this.loading = false
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private _generateDateArray(startDate: string, endDate: string): string[] {
|
|
553
|
+
const dates: string[] = []
|
|
554
|
+
const start = new Date(startDate)
|
|
555
|
+
const end = new Date(endDate)
|
|
556
|
+
|
|
557
|
+
// 일별로 생성 (기본)
|
|
558
|
+
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
559
|
+
dates.push(d.toLocaleDateString('sv-SE'))
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return dates
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private async _saveValues() {
|
|
566
|
+
try {
|
|
567
|
+
const patches: any[] = []
|
|
568
|
+
|
|
569
|
+
this.metrics.forEach(metric => {
|
|
570
|
+
Object.entries(metric.values).forEach(([date, data]) => {
|
|
571
|
+
// dirty 상태인 데이터만 처리
|
|
572
|
+
if (data.isDirty && data.value !== null && data.value !== undefined) {
|
|
573
|
+
const existingValue = this._findExistingValue(metric.metricId, date)
|
|
574
|
+
|
|
575
|
+
if (existingValue) {
|
|
576
|
+
patches.push({
|
|
577
|
+
id: existingValue.id,
|
|
578
|
+
value: data.value,
|
|
579
|
+
group: this.group,
|
|
580
|
+
cuFlag: 'M'
|
|
581
|
+
})
|
|
582
|
+
} else {
|
|
583
|
+
console.log(
|
|
584
|
+
'Creating new value for metric:',
|
|
585
|
+
metric.metricName,
|
|
586
|
+
'periodType:',
|
|
587
|
+
metric.periodType,
|
|
588
|
+
'date:',
|
|
589
|
+
date
|
|
590
|
+
)
|
|
591
|
+
const normalizedDate = this._normalizeDateByPeriodType(date, metric.periodType)
|
|
592
|
+
console.log('Normalized date:', normalizedDate)
|
|
593
|
+
patches.push({
|
|
594
|
+
metricId: metric.metricId,
|
|
595
|
+
valueDate: normalizedDate,
|
|
596
|
+
value: data.value,
|
|
597
|
+
group: this.group,
|
|
598
|
+
cuFlag: '+'
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
if (patches.length === 0) {
|
|
606
|
+
notify({ message: '저장할 데이터가 없습니다.' })
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// 디버깅: patches 내용 확인
|
|
611
|
+
console.log('Sending patches:', patches)
|
|
612
|
+
|
|
613
|
+
// 단일 mutation으로 생성과 업데이트 처리
|
|
614
|
+
const response = await client.mutate({
|
|
615
|
+
mutation: gql`
|
|
616
|
+
mutation ($patches: [KpiMetricValuePatch!]!) {
|
|
617
|
+
updateMultipleKpiMetricValue(patches: $patches) {
|
|
618
|
+
id
|
|
619
|
+
value
|
|
620
|
+
valueDate
|
|
621
|
+
metricId
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
`,
|
|
625
|
+
variables: { patches }
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
if (!response.errors) {
|
|
629
|
+
// 저장 후 dirty 상태 해제
|
|
630
|
+
response.data.updateMultipleKpiMetricValue.forEach((savedValue: any) => {
|
|
631
|
+
const metric = this.metrics.find(m => m.metricId === savedValue.metricId)
|
|
632
|
+
if (metric && metric.values[savedValue.valueDate]) {
|
|
633
|
+
metric.values[savedValue.valueDate].isDirty = false
|
|
634
|
+
}
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
notify({ message: 'KPI Metric 값이 성공적으로 저장되었습니다.' })
|
|
638
|
+
this.requestUpdate()
|
|
639
|
+
}
|
|
640
|
+
} catch (error) {
|
|
641
|
+
console.error('저장 중 오류:', error)
|
|
642
|
+
notify({ message: '저장 중 오류가 발생했습니다.' })
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private _findExistingValue(metricId: string, date: string) {
|
|
647
|
+
// 기존 로드된 KPI Metric Value 데이터에서 찾기
|
|
648
|
+
return this._existingValues?.find((v: any) => v.metricId === metricId && v.valueDate === date)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private _getMonthKey(date: string): string {
|
|
652
|
+
const dateObj = new Date(date)
|
|
653
|
+
return `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}`
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private _getQuarterKey(date: string): string {
|
|
657
|
+
const dateObj = new Date(date)
|
|
658
|
+
const quarter = Math.floor(dateObj.getMonth() / 3) + 1
|
|
659
|
+
return `${dateObj.getFullYear()}-Q${quarter}`
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private _getYearKey(date: string): string {
|
|
663
|
+
const dateObj = new Date(date)
|
|
664
|
+
return `${dateObj.getFullYear()}`
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private _getWeekKey(date: string): string {
|
|
668
|
+
const dateObj = new Date(date)
|
|
669
|
+
const dayOfWeek = dateObj.getDay()
|
|
670
|
+
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1
|
|
671
|
+
const monday = new Date(dateObj)
|
|
672
|
+
monday.setDate(dateObj.getDate() - daysToMonday)
|
|
673
|
+
return monday.toLocaleDateString('sv-SE')
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private _getMondayOfWeek(date: string): string {
|
|
677
|
+
const dateObj = new Date(date)
|
|
678
|
+
const dayOfWeek = dateObj.getDay()
|
|
679
|
+
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1
|
|
680
|
+
const monday = new Date(dateObj)
|
|
681
|
+
monday.setDate(dateObj.getDate() - daysToMonday)
|
|
682
|
+
return monday.toLocaleDateString('sv-SE')
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private _getFirstDayOfMonth(date: string): string {
|
|
686
|
+
const dateObj = new Date(date)
|
|
687
|
+
return new Date(dateObj.getFullYear(), dateObj.getMonth(), 1).toLocaleDateString('sv-SE')
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private _getFirstDayOfQuarter(date: string): string {
|
|
691
|
+
const dateObj = new Date(date)
|
|
692
|
+
const quarter = Math.floor(dateObj.getMonth() / 3)
|
|
693
|
+
const firstMonthOfQuarter = quarter * 3
|
|
694
|
+
return new Date(dateObj.getFullYear(), firstMonthOfQuarter, 1).toLocaleDateString('sv-SE')
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private _getFirstDayOfYear(date: string): string {
|
|
698
|
+
const dateObj = new Date(date)
|
|
699
|
+
return new Date(dateObj.getFullYear(), 0, 1).toLocaleDateString('sv-SE')
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private _normalizeDateByPeriodType(date: string, periodType: string): string {
|
|
703
|
+
console.log('Normalizing date:', date, 'for periodType:', periodType)
|
|
704
|
+
|
|
705
|
+
const dateObj = new Date(date)
|
|
706
|
+
|
|
707
|
+
switch (periodType) {
|
|
708
|
+
case 'DAY':
|
|
709
|
+
console.log('DAY - returning original date:', date)
|
|
710
|
+
return date // 그대로 사용
|
|
711
|
+
case 'WEEK':
|
|
712
|
+
// 해당 주의 월요일로 설정
|
|
713
|
+
const dayOfWeek = dateObj.getDay()
|
|
714
|
+
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1 // 일요일이면 6일 전, 아니면 dayOfWeek - 1
|
|
715
|
+
const monday = new Date(dateObj)
|
|
716
|
+
monday.setDate(dateObj.getDate() - daysToMonday)
|
|
717
|
+
const weeklyDate = monday.toLocaleDateString('sv-SE')
|
|
718
|
+
console.log('WEEK - returning monday:', weeklyDate)
|
|
719
|
+
return weeklyDate
|
|
720
|
+
case 'MONTH':
|
|
721
|
+
// 년-월 형식으로 설정 (예: "2025-01")
|
|
722
|
+
const monthlyDate = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, '0')}`
|
|
723
|
+
console.log('MONTH - returning year-month format:', monthlyDate)
|
|
724
|
+
return monthlyDate
|
|
725
|
+
case 'QUARTER':
|
|
726
|
+
// 분기 형식으로 설정 (예: "2025-Q1")
|
|
727
|
+
const quarter = Math.floor(dateObj.getMonth() / 3) + 1
|
|
728
|
+
const quarterlyDate = `${dateObj.getFullYear()}-Q${quarter}`
|
|
729
|
+
console.log('QUARTER - returning quarter format:', quarterlyDate)
|
|
730
|
+
return quarterlyDate
|
|
731
|
+
case 'YEAR':
|
|
732
|
+
// 년도 형식으로 설정 (예: "2025")
|
|
733
|
+
const yearlyDate = `${dateObj.getFullYear()}`
|
|
734
|
+
console.log('YEAR - returning year format:', yearlyDate)
|
|
735
|
+
return yearlyDate
|
|
736
|
+
default:
|
|
737
|
+
console.log('DEFAULT - returning original date:', date)
|
|
738
|
+
return date // 기본값
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private _cancel() {
|
|
743
|
+
// 편집 취소 로직
|
|
744
|
+
this.editingCell = null
|
|
745
|
+
this._loadData() // 원본 데이터로 복원
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async pageInitialized(lifecycle: any) {
|
|
749
|
+
// 기본값 설정 - 지난 1개월
|
|
750
|
+
if (!this.startDate) {
|
|
751
|
+
const today = new Date()
|
|
752
|
+
const oneMonthAgo = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate())
|
|
753
|
+
this.startDate = oneMonthAgo.toLocaleDateString('sv-SE')
|
|
754
|
+
}
|
|
755
|
+
if (!this.endDate) {
|
|
756
|
+
const today = new Date()
|
|
757
|
+
this.endDate = today.toLocaleDateString('sv-SE')
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// 페이지 초기화 시 자동으로 데이터 로드
|
|
761
|
+
await this._loadData()
|
|
762
|
+
}
|
|
763
|
+
}
|