@dssp/dkpi 1.0.0-alpha.80 → 1.0.0-alpha.81
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/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +11 -0
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/kpi-single-boxplot-chart.d.ts +3 -2
- package/dist-client/components/kpi-single-boxplot-chart.js +30 -23
- package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -1
- package/dist-client/pages/component/project-update-header.d.ts +1 -0
- package/dist-client/pages/component/project-update-header.js +127 -0
- package/dist-client/pages/component/project-update-header.js.map +1 -0
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +1 -1
- package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.js +1 -1
- package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +16 -2
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +138 -128
- package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +4 -2
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +109 -44
- package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +3 -0
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +32 -4
- package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +24 -0
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +365 -157
- package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -1
- package/dist-client/pages/sv-project-complete.d.ts +4 -1
- package/dist-client/pages/sv-project-complete.js +43 -12
- package/dist-client/pages/sv-project-complete.js.map +1 -1
- package/dist-client/pages/sv-project-detail.d.ts +11 -0
- package/dist-client/pages/sv-project-detail.js +184 -48
- package/dist-client/pages/sv-project-detail.js.map +1 -1
- package/dist-client/pages/sv-project-list.d.ts +9 -0
- package/dist-client/pages/sv-project-list.js +93 -3
- package/dist-client/pages/sv-project-list.js.map +1 -1
- package/dist-client/pages/sv-project-update.d.ts +86 -0
- package/dist-client/pages/sv-project-update.js +1331 -0
- package/dist-client/pages/sv-project-update.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/shared/complete-api.d.ts +10 -9
- package/dist-client/shared/complete-api.js +44 -18
- package/dist-client/shared/complete-api.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/menu-tools.js +9 -18
- package/dist-client/viewparts/menu-tools.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +23 -0
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +72 -28
- package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +9 -2
- package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
- package/dist-server/service/kpi-stat/kpi-stat-query.js +19 -18
- package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -1
- package/dist-server/service/kpi-value/kpi-value-query.js +2 -2
- package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/schema.graphql +13 -1
- package/things-factory.config.js +1 -0
- package/dist-client/shared/domain-context.d.ts +0 -7
- package/dist-client/shared/domain-context.js +0 -13
- package/dist-client/shared/domain-context.js.map +0 -1
|
@@ -4,28 +4,48 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|
|
4
4
|
import { client } from '@operato/graphql';
|
|
5
5
|
import { gql } from '@apollo/client';
|
|
6
6
|
import { notify } from '@operato/layout';
|
|
7
|
+
import { OxPrompt } from '@operato/popup';
|
|
8
|
+
import { hasPrivilege } from '@things-factory/auth-base/dist-client';
|
|
7
9
|
import moment from 'moment-timezone';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
import { tenantHeaders } from '@dssp/project/dist-client/shared/domain-context';
|
|
11
|
+
/**
|
|
12
|
+
* 프로젝트 완공 처리 탭 4 — 월별 KPI 데이터 그리드.
|
|
13
|
+
*
|
|
14
|
+
* 데이터 source = `KpiMetric` (periodType=MONTH) + `KpiMetricValue` (그 metric 의
|
|
15
|
+
* 월별 값들). 이전 버전 (DataSet/DataSample) 폐기. KpiMetricValue 의 unique
|
|
16
|
+
* 인덱스 (domain, metric, valueDate, org) 가 (도메인, 프로젝트, 메트릭, 월) 단일 row
|
|
17
|
+
* 정책을 보장.
|
|
18
|
+
*
|
|
19
|
+
* 단방향 흐름:
|
|
20
|
+
* - 탭1/탭2 의 metric 저장 (valueDate=YYYY-MM-01) → 그 월 row 가 여기 자동 노출
|
|
21
|
+
* - 여기서 cell 수정 → 그 metric × 그 월 row 만 update (탭1/탭2 의 "현재값" 영향 없음 —
|
|
22
|
+
* 탭1/탭2 는 KpiMetricValueProvider 가 latest 또는 valueDate-based lookup 으로 동작)
|
|
23
|
+
*/
|
|
24
|
+
/** rating 셀렉트로 표시할 metric 인지 판단 — 이름에 '평가' 또는 '이탈' 포함되면 1~5 rating. */
|
|
25
|
+
function isRating(metricName) {
|
|
26
|
+
return /평가|이탈/.test(metricName || '');
|
|
27
|
+
}
|
|
21
28
|
let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends LitElement {
|
|
22
29
|
constructor() {
|
|
23
30
|
super(...arguments);
|
|
24
31
|
this.project = {};
|
|
25
|
-
|
|
32
|
+
/** 월별 metric 목록 (periodType=MONTH). KpiMetric admin 에 등록된 것 기준. */
|
|
33
|
+
this.monthlyMetrics = [];
|
|
34
|
+
/** monthRows: { workDate:'YYYY-MM', values:{[metricId]:value}, originalValues, dirty }[] */
|
|
26
35
|
this.monthRows = [];
|
|
27
36
|
this.addYear = new Date().getFullYear();
|
|
28
37
|
this.addMonth = new Date().getMonth() + 1;
|
|
38
|
+
/** kpi:input — Step4 월별 데이터 저장 권한 (cumulative 와 같은 권한) */
|
|
39
|
+
this.canSave = false;
|
|
40
|
+
}
|
|
41
|
+
async connectedCallback() {
|
|
42
|
+
super.connectedCallback();
|
|
43
|
+
this.canSave = await hasPrivilege({
|
|
44
|
+
category: 'kpi',
|
|
45
|
+
privilege: 'input',
|
|
46
|
+
domainOwnerGranted: true,
|
|
47
|
+
superUserGranted: true
|
|
48
|
+
});
|
|
29
49
|
}
|
|
30
50
|
render() {
|
|
31
51
|
const years = this._getYearRange();
|
|
@@ -36,7 +56,10 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
|
|
|
36
56
|
</div>
|
|
37
57
|
|
|
38
58
|
<div class="toolbar">
|
|
39
|
-
<select
|
|
59
|
+
<select
|
|
60
|
+
.value=${String(this.addYear)}
|
|
61
|
+
@change=${(e) => (this.addYear = Number(e.target.value))}
|
|
62
|
+
>
|
|
40
63
|
${years.map(y => html `<option value=${y} ?selected=${y === this.addYear}>${y}년</option>`)}
|
|
41
64
|
</select>
|
|
42
65
|
<select
|
|
@@ -48,68 +71,90 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
|
|
|
48
71
|
<div class="add-btn" @click=${this._addMonth}>+ 월 추가</div>
|
|
49
72
|
</div>
|
|
50
73
|
|
|
51
|
-
${this.
|
|
52
|
-
? html `<div class="empty-msg"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
${this.monthlyMetrics.length === 0
|
|
75
|
+
? html `<div class="empty-msg">
|
|
76
|
+
월별 metric (periodType=MONTH) 이 등록되어 있지 않습니다. KPI 관리자에서 먼저 등록해주세요.
|
|
77
|
+
</div>`
|
|
78
|
+
: this.monthRows.length === 0
|
|
79
|
+
? html `<div class="empty-msg">등록된 월별 데이터가 없습니다. 위에서 월을 추가해주세요.</div>`
|
|
80
|
+
: html `
|
|
81
|
+
<div class="grid-wrapper">
|
|
82
|
+
<table>
|
|
83
|
+
<thead>
|
|
84
|
+
<tr>
|
|
85
|
+
<th>월</th>
|
|
86
|
+
${this.monthlyMetrics.map(m => html `
|
|
87
|
+
<th class=${this._isMetricNeverEntered(m.id) ? 'pending' : ''}
|
|
88
|
+
title=${this._isMetricNeverEntered(m.id) ? '한 번도 입력된 적 없는 metric' : ''}>
|
|
89
|
+
${m.name}${m.unit ? ` (${m.unit})` : ''}
|
|
90
|
+
</th>
|
|
91
|
+
`)}
|
|
92
|
+
<th>상태</th>
|
|
93
|
+
<th></th>
|
|
94
|
+
</tr>
|
|
95
|
+
</thead>
|
|
96
|
+
<tbody>
|
|
97
|
+
${this.monthRows.map((row, rowIdx) => html `
|
|
98
|
+
<tr>
|
|
99
|
+
<td class="month-cell ${this._isCurrentMonth(row.workDate) ? 'current' : ''}">
|
|
100
|
+
${row.workDate}
|
|
101
|
+
</td>
|
|
102
|
+
${this.monthlyMetrics.map(m => {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
const pending = this._isCellPending(row, m.id);
|
|
105
|
+
return isRating(m.name)
|
|
106
|
+
? html `
|
|
107
|
+
<td class=${pending ? 'pending' : ''}>
|
|
108
|
+
<select
|
|
109
|
+
class="rating-select"
|
|
110
|
+
.value=${String((_a = row.values[m.id]) !== null && _a !== void 0 ? _a : '')}
|
|
111
|
+
@change=${(e) => this._onCellChange(rowIdx, m.id, e.target.value)}
|
|
112
|
+
>
|
|
113
|
+
<option value="">-</option>
|
|
114
|
+
${[1, 2, 3, 4, 5].map(v => html `<option value=${v} ?selected=${Number(row.values[m.id]) === v}>
|
|
115
|
+
${v}
|
|
116
|
+
</option>`)}
|
|
117
|
+
</select>
|
|
118
|
+
</td>
|
|
119
|
+
`
|
|
120
|
+
: html `
|
|
121
|
+
<td class=${pending ? 'pending' : ''}>
|
|
122
|
+
<input
|
|
123
|
+
type="number"
|
|
124
|
+
.value=${(_b = row.values[m.id]) !== null && _b !== void 0 ? _b : ''}
|
|
125
|
+
@input=${(e) => this._onCellChange(rowIdx, m.id, e.target.value)}
|
|
126
|
+
/>
|
|
127
|
+
</td>
|
|
128
|
+
`;
|
|
129
|
+
})}
|
|
130
|
+
<td>
|
|
131
|
+
${row.dirty
|
|
96
132
|
? html `<span class="status-unsaved">수정됨</span>`
|
|
97
|
-
:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
: row.isAuto
|
|
134
|
+
? html `<span class="status-pending">미입력</span>`
|
|
135
|
+
: row.isNew
|
|
136
|
+
? html `<span class="status-new">신규</span>`
|
|
137
|
+
: html `<span class="status-saved">저장됨</span>`}
|
|
138
|
+
</td>
|
|
139
|
+
<td>
|
|
140
|
+
<span class="delete-btn" title="삭제" @click=${() => this._removeMonth(rowIdx)}>✕</span>
|
|
141
|
+
</td>
|
|
142
|
+
</tr>
|
|
143
|
+
`)}
|
|
144
|
+
</tbody>
|
|
145
|
+
</table>
|
|
146
|
+
</div>
|
|
147
|
+
`}
|
|
109
148
|
|
|
110
149
|
<div class="button-line">
|
|
111
150
|
<div class="ghost-btn" @click=${this._reset}>초기화</div>
|
|
112
|
-
<div
|
|
151
|
+
<div
|
|
152
|
+
class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
|
|
153
|
+
title=${this.canSave ? '' : 'kpi:input 권한 필요'}
|
|
154
|
+
@click=${() => this.canSave && this._save()}
|
|
155
|
+
>
|
|
156
|
+
저장
|
|
157
|
+
</div>
|
|
113
158
|
</div>
|
|
114
159
|
`;
|
|
115
160
|
}
|
|
@@ -122,7 +167,9 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
|
|
|
122
167
|
}
|
|
123
168
|
_getYearRange() {
|
|
124
169
|
var _a, _b;
|
|
125
|
-
const startYear = ((_a = this.project) === null || _a === void 0 ? void 0 : _a.startDate)
|
|
170
|
+
const startYear = ((_a = this.project) === null || _a === void 0 ? void 0 : _a.startDate)
|
|
171
|
+
? new Date(this.project.startDate).getFullYear()
|
|
172
|
+
: new Date().getFullYear() - 2;
|
|
126
173
|
const endYear = ((_b = this.project) === null || _b === void 0 ? void 0 : _b.endDate)
|
|
127
174
|
? new Date(this.project.endDate).getFullYear() + 1
|
|
128
175
|
: new Date().getFullYear() + 1;
|
|
@@ -134,130 +181,262 @@ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends Li
|
|
|
134
181
|
_isCurrentMonth(workDate) {
|
|
135
182
|
return workDate === moment().tz('Asia/Seoul').format('YYYY-MM');
|
|
136
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* 월별 metric 정의 + 그 프로젝트의 월별 KpiMetricValue 들을 조회해 그리드 row 구성.
|
|
186
|
+
*
|
|
187
|
+
* 1) KpiMetric where periodType=MONTH → monthlyMetrics
|
|
188
|
+
* 2) KpiMetricValue where org=projectId → 월별로 그룹핑하여 monthRows
|
|
189
|
+
*/
|
|
137
190
|
async _loadData() {
|
|
138
|
-
var _a, _b;
|
|
191
|
+
var _a, _b, _c, _d, _e, _f;
|
|
139
192
|
try {
|
|
140
|
-
|
|
193
|
+
// 1) 월별 metric 목록
|
|
194
|
+
const metricsResp = await client.query({
|
|
141
195
|
query: gql `
|
|
142
|
-
query
|
|
143
|
-
|
|
196
|
+
query KpiMetrics {
|
|
197
|
+
kpiMetrics {
|
|
144
198
|
items {
|
|
145
199
|
id
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
200
|
+
name
|
|
201
|
+
unit
|
|
202
|
+
periodType
|
|
149
203
|
}
|
|
150
|
-
total
|
|
151
204
|
}
|
|
152
205
|
}
|
|
153
206
|
`,
|
|
154
|
-
|
|
155
|
-
dataSetId: DATASET_ID,
|
|
156
|
-
filters: [{ name: 'key01', operator: 'eq', value: this.project.id }],
|
|
157
|
-
sortings: [{ name: 'workDate', desc: false }],
|
|
158
|
-
pagination: { page: 1, limit: 120 }
|
|
159
|
-
}
|
|
207
|
+
context: { headers: tenantHeaders((_a = this.project) === null || _a === void 0 ? void 0 : _a.code) }
|
|
160
208
|
});
|
|
161
|
-
const
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
209
|
+
const all = ((_c = (_b = metricsResp.data) === null || _b === void 0 ? void 0 : _b.kpiMetrics) === null || _c === void 0 ? void 0 : _c.items) || [];
|
|
210
|
+
this.monthlyMetrics = all.filter((m) => m.periodType === 'MONTH');
|
|
211
|
+
if (this.monthlyMetrics.length === 0) {
|
|
212
|
+
this.monthRows = [];
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// 2) 프로젝트의 KpiMetricValue 들 (org=projectId). 월별 metric 만 client 측에서 필터.
|
|
216
|
+
const monthlyMetricIds = new Set(this.monthlyMetrics.map((m) => m.id));
|
|
217
|
+
const valuesResp = await client.query({
|
|
218
|
+
query: gql `
|
|
219
|
+
query KpiMetricValues($filters: [Filter!]) {
|
|
220
|
+
kpiMetricValues(filters: $filters) {
|
|
221
|
+
items {
|
|
222
|
+
id
|
|
223
|
+
value
|
|
224
|
+
unit
|
|
225
|
+
periodType
|
|
226
|
+
valueDate
|
|
227
|
+
metricId
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
`,
|
|
232
|
+
variables: { filters: [{ name: 'org', operator: 'eq', value: this.project.id }] },
|
|
233
|
+
context: { headers: tenantHeaders((_d = this.project) === null || _d === void 0 ? void 0 : _d.code) }
|
|
170
234
|
});
|
|
235
|
+
const allValues = ((_f = (_e = valuesResp.data) === null || _e === void 0 ? void 0 : _e.kpiMetricValues) === null || _f === void 0 ? void 0 : _f.items) || [];
|
|
236
|
+
// periodType='MONTH' 만 그리드에 — 비월별 row 가 valueDate prefix 로 잘못 묶이지 않도록.
|
|
237
|
+
const monthlyValues = allValues.filter((v) => monthlyMetricIds.has(v.metricId) && v.periodType === 'MONTH');
|
|
238
|
+
// 월별 그룹핑 — valueDate YYYY-MM-DD 의 앞 7자리. id 도 보존 (삭제 시 사용).
|
|
239
|
+
// 현재월(=this month) row 는 운영 원칙상 표시 대상 아님 → 아예 bucket 제외.
|
|
240
|
+
const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM');
|
|
241
|
+
const byMonth = new Map();
|
|
242
|
+
for (const v of monthlyValues) {
|
|
243
|
+
const ym = (v.valueDate || '').slice(0, 7);
|
|
244
|
+
if (!ym)
|
|
245
|
+
continue;
|
|
246
|
+
if (ym === currentYm)
|
|
247
|
+
continue; // 현재월 record 무시 (DB 에 우연히 있더라도)
|
|
248
|
+
if (!byMonth.has(ym))
|
|
249
|
+
byMonth.set(ym, { workDate: ym, values: {}, originalValues: {}, valueIds: {}, dirty: false });
|
|
250
|
+
const row = byMonth.get(ym);
|
|
251
|
+
row.values[v.metricId] = v.value;
|
|
252
|
+
row.originalValues[v.metricId] = v.value;
|
|
253
|
+
row.valueIds[v.metricId] = v.id;
|
|
254
|
+
}
|
|
255
|
+
// project.startDate ~ 현재월 사이 누락된 월 placeholder row 자동 추가.
|
|
256
|
+
// 사용자가 값을 입력하지 않으면 dirty 가 안 되므로 저장 시 무시되어 spurious patch 없음.
|
|
257
|
+
for (const ym of this._generateExpectedMonths()) {
|
|
258
|
+
if (!byMonth.has(ym)) {
|
|
259
|
+
byMonth.set(ym, {
|
|
260
|
+
workDate: ym,
|
|
261
|
+
values: {},
|
|
262
|
+
originalValues: {},
|
|
263
|
+
valueIds: {},
|
|
264
|
+
dirty: false,
|
|
265
|
+
isAuto: true
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this.monthRows = Array.from(byMonth.values()).sort((a, b) => b.workDate.localeCompare(a.workDate));
|
|
171
270
|
}
|
|
172
271
|
catch (e) {
|
|
173
272
|
console.error('Failed to load monthly data:', e);
|
|
174
273
|
this.monthRows = [];
|
|
175
274
|
}
|
|
176
275
|
}
|
|
276
|
+
/** project.startDate (YYYY-MM) ~ **전월** (YYYY-MM) 까지 매월 문자열 배열.
|
|
277
|
+
* 현재월은 의도적으로 제외 — 운영 원칙상 "이번달 데이터는 아직 입력 대상이 아님". */
|
|
278
|
+
_generateExpectedMonths() {
|
|
279
|
+
var _a;
|
|
280
|
+
const startStr = (((_a = this.project) === null || _a === void 0 ? void 0 : _a.startDate) || '').slice(0, 7);
|
|
281
|
+
if (!startStr || !/^\d{4}-\d{2}$/.test(startStr))
|
|
282
|
+
return [];
|
|
283
|
+
const tz = 'Asia/Seoul';
|
|
284
|
+
let cur = moment.tz(startStr, 'YYYY-MM', tz);
|
|
285
|
+
const end = moment.tz(moment().tz(tz).subtract(1, 'month').format('YYYY-MM'), 'YYYY-MM', tz);
|
|
286
|
+
if (!cur.isValid() || cur.isAfter(end))
|
|
287
|
+
return [];
|
|
288
|
+
const months = [];
|
|
289
|
+
while (cur.isSameOrBefore(end)) {
|
|
290
|
+
months.push(cur.format('YYYY-MM'));
|
|
291
|
+
cur = cur.add(1, 'month');
|
|
292
|
+
}
|
|
293
|
+
return months;
|
|
294
|
+
}
|
|
295
|
+
/** 셀 미입력 여부 — 값 없음 AND 그 월이 **전월** (직전 한 달) 인 경우만 pending.
|
|
296
|
+
* 과월 전체가 아니라 전월 한 달에 한해서만 강조 — 사용자 입력 흐름(이번 달에 지난달 데이터)과 일치. */
|
|
297
|
+
_isCellPending(row, metricId) {
|
|
298
|
+
var _a;
|
|
299
|
+
const v = (_a = row.values) === null || _a === void 0 ? void 0 : _a[metricId];
|
|
300
|
+
if (v !== undefined && v !== null && v !== '')
|
|
301
|
+
return false;
|
|
302
|
+
const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
|
|
303
|
+
return row.workDate === lastMonth;
|
|
304
|
+
}
|
|
305
|
+
/** metric 컬럼이 전 row 통틀어 한 번도 값이 없으면 헤더 강조. */
|
|
306
|
+
_isMetricNeverEntered(metricId) {
|
|
307
|
+
return !this.monthRows.some(r => {
|
|
308
|
+
var _a;
|
|
309
|
+
const v = (_a = r.values) === null || _a === void 0 ? void 0 : _a[metricId];
|
|
310
|
+
return v !== undefined && v !== null && v !== '';
|
|
311
|
+
});
|
|
312
|
+
}
|
|
177
313
|
_addMonth() {
|
|
178
314
|
const workDate = `${this.addYear}-${String(this.addMonth).padStart(2, '0')}`;
|
|
179
|
-
//
|
|
315
|
+
// 현재월 row 는 추가 금지 — 운영 원칙상 이번달 데이터는 입력 대상 아님.
|
|
316
|
+
const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM');
|
|
317
|
+
if (workDate === currentYm) {
|
|
318
|
+
notify({ message: '현재월(이번 달) 데이터는 입력 대상이 아닙니다.' });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// 미래월도 차단
|
|
322
|
+
if (workDate > currentYm) {
|
|
323
|
+
notify({ message: '미래월은 입력 대상이 아닙니다.' });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
180
326
|
if (this.monthRows.some(r => r.workDate === workDate)) {
|
|
181
327
|
notify({ message: `${workDate}은 이미 존재합니다.` });
|
|
182
328
|
return;
|
|
183
329
|
}
|
|
184
|
-
const newRow = {
|
|
185
|
-
workDate,
|
|
186
|
-
data: {},
|
|
187
|
-
sampleId: null,
|
|
188
|
-
dirty: true
|
|
189
|
-
};
|
|
190
|
-
// 정렬된 위치에 삽입
|
|
330
|
+
const newRow = { workDate, values: {}, originalValues: {}, valueIds: {}, dirty: true, isNew: true };
|
|
191
331
|
const rows = [...this.monthRows, newRow];
|
|
192
|
-
rows.sort((a, b) =>
|
|
332
|
+
rows.sort((a, b) => b.workDate.localeCompare(a.workDate));
|
|
193
333
|
this.monthRows = rows;
|
|
194
334
|
}
|
|
195
|
-
_removeMonth(rowIdx) {
|
|
196
|
-
|
|
335
|
+
async _removeMonth(rowIdx) {
|
|
336
|
+
var _a;
|
|
337
|
+
const row = this.monthRows[rowIdx];
|
|
338
|
+
// 신규 row (DB 미반영) — 메모리에서만 제거
|
|
339
|
+
if (row.isNew) {
|
|
340
|
+
this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// 저장된 row — 그 월의 모든 KpiMetricValue id 들을 한 번에 삭제.
|
|
344
|
+
// 권한: things-factory 의 deleteKpiMetricValues 는 category="kpi", privilege="mutation"
|
|
345
|
+
// (superUser/도메인 owner 자동 통과, 일반 사용자는 별도 grant 필요).
|
|
346
|
+
const ids = Object.values(row.valueIds || {}).filter(Boolean);
|
|
347
|
+
if (ids.length === 0) {
|
|
348
|
+
this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const ok = await OxPrompt.open({
|
|
352
|
+
title: `${row.workDate} 의 월별 데이터를 삭제하시겠습니까?`,
|
|
353
|
+
confirmButton: { text: '삭제' },
|
|
354
|
+
cancelButton: { text: '취소' }
|
|
355
|
+
});
|
|
356
|
+
if (!ok)
|
|
357
|
+
return;
|
|
358
|
+
try {
|
|
359
|
+
const response = await client.mutate({
|
|
360
|
+
mutation: gql `
|
|
361
|
+
mutation DeleteKpiMetricValues($ids: [String!]!) {
|
|
362
|
+
deleteKpiMetricValues(ids: $ids)
|
|
363
|
+
}
|
|
364
|
+
`,
|
|
365
|
+
variables: { ids },
|
|
366
|
+
context: { headers: tenantHeaders((_a = this.project) === null || _a === void 0 ? void 0 : _a.code) }
|
|
367
|
+
});
|
|
368
|
+
if (response.errors) {
|
|
369
|
+
throw new Error(response.errors.map((e) => e.message).join('\n'));
|
|
370
|
+
}
|
|
371
|
+
notify({ message: `${row.workDate} 월별 데이터 ${ids.length}건 삭제되었습니다.` });
|
|
372
|
+
await this._loadData();
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
console.error('Failed to delete monthly values:', e);
|
|
376
|
+
notify({ message: '삭제 중 오류가 발생했습니다. 권한을 확인해주세요.' });
|
|
377
|
+
}
|
|
197
378
|
}
|
|
198
|
-
_onCellChange(rowIdx,
|
|
379
|
+
_onCellChange(rowIdx, metricId, rawValue) {
|
|
199
380
|
const value = rawValue === '' ? null : Number(rawValue);
|
|
200
381
|
this.monthRows = this.monthRows.map((row, i) => {
|
|
201
382
|
if (i !== rowIdx)
|
|
202
383
|
return row;
|
|
203
|
-
return Object.assign(Object.assign({}, row), {
|
|
384
|
+
return Object.assign(Object.assign({}, row), { values: Object.assign(Object.assign({}, row.values), { [metricId]: value }), dirty: true });
|
|
204
385
|
});
|
|
205
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* 저장 — dirty row 들 안의 변경된 cell 들을 KpiMetricValuePatch 로 모아
|
|
389
|
+
* updateKpiMetricValuesCumulative 한 번 호출 (backend upsert 가 unique 조합 처리).
|
|
390
|
+
*/
|
|
206
391
|
async _save() {
|
|
207
|
-
|
|
208
|
-
|
|
392
|
+
var _a;
|
|
393
|
+
const patches = [];
|
|
394
|
+
for (const row of this.monthRows) {
|
|
395
|
+
if (!row.dirty)
|
|
396
|
+
continue;
|
|
397
|
+
for (const m of this.monthlyMetrics) {
|
|
398
|
+
const value = row.values[m.id];
|
|
399
|
+
const original = row.originalValues[m.id];
|
|
400
|
+
if (value === undefined || value === null)
|
|
401
|
+
continue; // 빈 값은 skip (기존 유지)
|
|
402
|
+
if (value === original)
|
|
403
|
+
continue; // 변화 없으면 skip
|
|
404
|
+
patches.push({
|
|
405
|
+
metricId: m.id,
|
|
406
|
+
value,
|
|
407
|
+
unit: m.unit || '',
|
|
408
|
+
org: this.project.id,
|
|
409
|
+
periodType: 'MONTH',
|
|
410
|
+
valueDate: `${row.workDate}-01`
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (patches.length === 0) {
|
|
209
415
|
notify({ message: '변경된 데이터가 없습니다.' });
|
|
210
416
|
return;
|
|
211
417
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// null 값 제거
|
|
219
|
-
for (const key of Object.keys(data)) {
|
|
220
|
-
if (data[key] === null || data[key] === undefined)
|
|
221
|
-
delete data[key];
|
|
222
|
-
}
|
|
223
|
-
// collectedAt을 월 시작일 고정으로 설정 → 동일 key01+collectedAt이면 기존 레코드 업데이트
|
|
224
|
-
const collectedAt = new Date(`${row.workDate}-01T00:00:00Z`);
|
|
225
|
-
await client.mutate({
|
|
226
|
-
mutation: gql `
|
|
227
|
-
mutation CreateDataSample($dataSample: NewDataSample!) {
|
|
228
|
-
createDataSample(dataSample: $dataSample) {
|
|
229
|
-
id
|
|
230
|
-
data
|
|
231
|
-
workDate
|
|
232
|
-
key01
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
`,
|
|
236
|
-
variables: {
|
|
237
|
-
dataSample: {
|
|
238
|
-
dataSet: { id: DATASET_ID },
|
|
239
|
-
data,
|
|
240
|
-
workDate: `${row.workDate}-01`,
|
|
241
|
-
collectedAt: collectedAt.toISOString(),
|
|
242
|
-
source: 'project-complete'
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
savedCount++;
|
|
418
|
+
try {
|
|
419
|
+
const response = await client.mutate({
|
|
420
|
+
mutation: gql `
|
|
421
|
+
mutation UpdateKpiMetricValuesCumulative($patches: [KpiMetricValuePatch!]!) {
|
|
422
|
+
updateKpiMetricValuesCumulative(patches: $patches) {
|
|
423
|
+
id
|
|
247
424
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
425
|
+
}
|
|
426
|
+
`,
|
|
427
|
+
variables: { patches },
|
|
428
|
+
context: { headers: tenantHeaders((_a = this.project) === null || _a === void 0 ? void 0 : _a.code) }
|
|
429
|
+
});
|
|
430
|
+
if (response.errors) {
|
|
431
|
+
throw new Error(response.errors.map((e) => e.message).join('\n'));
|
|
251
432
|
}
|
|
433
|
+
notify({ message: `${patches.length}건 저장되었습니다.` });
|
|
434
|
+
await this._loadData();
|
|
252
435
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
else {
|
|
257
|
-
notify({ message: `${savedCount}건 저장되었습니다.` });
|
|
436
|
+
catch (e) {
|
|
437
|
+
console.error('Failed to save monthly values:', e);
|
|
438
|
+
notify({ message: '저장 중 오류가 발생했습니다.' });
|
|
258
439
|
}
|
|
259
|
-
// 저장 후 다시 로드하여 sampleId 갱신
|
|
260
|
-
await this._loadData();
|
|
261
440
|
}
|
|
262
441
|
_reset() {
|
|
263
442
|
this._loadData();
|
|
@@ -372,6 +551,23 @@ SvProjectCompleteTab4Monthly.styles = [
|
|
|
372
551
|
color: #3498db;
|
|
373
552
|
font-size: 12px;
|
|
374
553
|
}
|
|
554
|
+
.status-pending {
|
|
555
|
+
color: #e74c3c;
|
|
556
|
+
font-size: 12px;
|
|
557
|
+
font-weight: 600;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* 미입력 셀 — 과월/현재월 row 에서 값이 비어있을 때 강조 */
|
|
561
|
+
td.pending input[type='number'],
|
|
562
|
+
td.pending .rating-select {
|
|
563
|
+
border-color: #e74c3c;
|
|
564
|
+
background-color: #fff5f5;
|
|
565
|
+
}
|
|
566
|
+
/* 한 번도 입력된 적 없는 metric 컬럼 — 헤더 강조 */
|
|
567
|
+
th.pending {
|
|
568
|
+
background: #fdecec;
|
|
569
|
+
color: #c0392b;
|
|
570
|
+
}
|
|
375
571
|
|
|
376
572
|
.button-line {
|
|
377
573
|
display: flex;
|
|
@@ -392,6 +588,10 @@ SvProjectCompleteTab4Monthly.styles = [
|
|
|
392
588
|
.ghost-btn.secondary {
|
|
393
589
|
background: #24be7b;
|
|
394
590
|
}
|
|
591
|
+
.ghost-btn.disabled {
|
|
592
|
+
opacity: 0.45;
|
|
593
|
+
cursor: not-allowed;
|
|
594
|
+
}
|
|
395
595
|
|
|
396
596
|
.delete-btn {
|
|
397
597
|
cursor: pointer;
|
|
@@ -414,6 +614,10 @@ __decorate([
|
|
|
414
614
|
property({ type: Object }),
|
|
415
615
|
__metadata("design:type", Object)
|
|
416
616
|
], SvProjectCompleteTab4Monthly.prototype, "project", void 0);
|
|
617
|
+
__decorate([
|
|
618
|
+
state(),
|
|
619
|
+
__metadata("design:type", Array)
|
|
620
|
+
], SvProjectCompleteTab4Monthly.prototype, "monthlyMetrics", void 0);
|
|
417
621
|
__decorate([
|
|
418
622
|
state(),
|
|
419
623
|
__metadata("design:type", Array)
|
|
@@ -426,6 +630,10 @@ __decorate([
|
|
|
426
630
|
state(),
|
|
427
631
|
__metadata("design:type", Number)
|
|
428
632
|
], SvProjectCompleteTab4Monthly.prototype, "addMonth", void 0);
|
|
633
|
+
__decorate([
|
|
634
|
+
state(),
|
|
635
|
+
__metadata("design:type", Object)
|
|
636
|
+
], SvProjectCompleteTab4Monthly.prototype, "canSave", void 0);
|
|
429
637
|
SvProjectCompleteTab4Monthly = __decorate([
|
|
430
638
|
customElement('sv-pc-tab4-monthly')
|
|
431
639
|
], SvProjectCompleteTab4Monthly);
|