@dssp/dkpi 1.0.0-alpha.84 → 1.0.0-alpha.85

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.
@@ -25,6 +25,8 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
25
25
  this.selectedKpi = null;
26
26
  /** 이 프로젝트에 아직 한 번도 입력되지 않은 metric (active 한 manual/import 한정) */
27
27
  this.pendingMetrics = [];
28
+ /** 현재 프로젝트의 모든 KpiMetricValue (metric.name 포함) — 2D lookup chart 의 progressRate / value 도출용. */
29
+ this.allKpiMetricValues = [];
28
30
  }
29
31
  get context() {
30
32
  var _a;
@@ -35,7 +37,7 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
35
37
  };
36
38
  }
37
39
  render() {
38
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
40
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
39
41
  return html `
40
42
  <div content>
41
43
  <div left>
@@ -271,6 +273,14 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
271
273
  <div class="popup-content" @click=${(e) => e.stopPropagation()}>
272
274
  <div class="popup-header">
273
275
  <div class="popup-title">${this.selectedKpi.kpiName} — 성과 분포</div>
276
+ ${this.selectedKpi.score !== undefined && this.selectedKpi.score !== null
277
+ ? html `
278
+ <div class="popup-score">
279
+ <span class="score-value">${Math.round(Number(this.selectedKpi.score))}</span>
280
+ <span class="score-label">점</span>
281
+ </div>
282
+ `
283
+ : ''}
274
284
  <button class="popup-close" @click=${this._closePopup}>×</button>
275
285
  </div>
276
286
  <div class="popup-chart-container">
@@ -284,32 +294,24 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
284
294
  * LOOKUP: 성과 분포 커브 (kpi-lookup-chart)
285
295
  * ASSESSMENT: 5단계 게이지 (kpi-lookup-chart 자동 감지)
286
296
  */''}
287
- ${(this.selectedKpi.scoreType === 'CUSTOM' ||
288
- ((_l = this.selectedKpi.grades) === null || _l === void 0 ? void 0 : _l.type) === 'PROGRESS_DEVIATION_LOOKUP') &&
289
- this.selectedKpi.value !== null &&
290
- !(this.selectedKpi.value === Math.floor(this.selectedKpi.value) &&
291
- this.selectedKpi.value >= 1 &&
292
- this.selectedKpi.value <= 5)
293
- ? html `
294
- <kpi-2d-lookup-chart
295
- .grades=${this.selectedKpi.grades}
296
- .value=${this.selectedKpi.value}
297
- .progressRate=${(_o = (_m = this.project) === null || _m === void 0 ? void 0 : _m.totalProgress) !== null && _o !== void 0 ? _o : 50}
298
- unit="%"
299
- kpiName=${this.selectedKpi.kpiName || ''}
300
- ></kpi-2d-lookup-chart>
301
- `
297
+ ${
298
+ // 2D lookup KPI (X13 등) 항상 등급 기준 + 히트맵 함께 표시 (value
299
+ // 정수든 소수든 일관). _renderAssessmentGrades 안의 is2D 분기가 히트맵
300
+ // 같이 그림. 그 외 ASSESSMENT 도 등급 기준, LOOKUP 은 분포 커브.
301
+ (this.selectedKpi.scoreType === 'CUSTOM' ||
302
+ ((_l = this.selectedKpi.grades) === null || _l === void 0 ? void 0 : _l.type) === 'PROGRESS_DEVIATION_LOOKUP')
303
+ ? this._renderAssessmentGrades()
302
304
  : this._isAssessmentOrIntegerScore()
303
305
  ? this._renderAssessmentGrades()
304
306
  : html `
305
- <kpi-lookup-chart
306
- .grades=${this.selectedKpi.grades || []}
307
- .value=${this.selectedKpi.value}
308
- .scoreType=${this.selectedKpi.scoreType || ''}
309
- .valueType=${this.selectedKpi.valueType || ''}
310
- unit=${this.selectedKpi.unit || ''}
311
- ></kpi-lookup-chart>
312
- `}
307
+ <kpi-lookup-chart
308
+ .grades=${this.selectedKpi.grades || []}
309
+ .value=${this.selectedKpi.value}
310
+ .scoreType=${this.selectedKpi.scoreType || ''}
311
+ .valueType=${this.selectedKpi.valueType || ''}
312
+ unit=${this.selectedKpi.unit || ''}
313
+ ></kpi-lookup-chart>
314
+ `}
313
315
  </div>
314
316
  </div>
315
317
  </div>
@@ -489,7 +491,7 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
489
491
  this.pendingMetrics = [];
490
492
  return;
491
493
  }
492
- // 2) 이 프로젝트의 모든 KpiMetricValue (value 가져와서 null/undefined 체크)
494
+ // 2) 이 프로젝트의 모든 KpiMetricValue (metric.name 포함 2D 차트의 latest lookup 에도 사용)
493
495
  const valuesResp = await client.query({
494
496
  query: gql `
495
497
  query KpiMetricValues($filters: [Filter!]) {
@@ -499,6 +501,10 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
499
501
  periodType
500
502
  valueDate
501
503
  value
504
+ metric {
505
+ id
506
+ name
507
+ }
502
508
  }
503
509
  }
504
510
  }
@@ -507,6 +513,7 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
507
513
  context: { headers: tenantHeaders((_e = this.project) === null || _e === void 0 ? void 0 : _e.code) }
508
514
  });
509
515
  const values = ((_g = (_f = valuesResp.data) === null || _f === void 0 ? void 0 : _f.kpiMetricValues) === null || _g === void 0 ? void 0 : _g.items) || [];
516
+ this.allKpiMetricValues = values;
510
517
  const lastMonthYm = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
511
518
  // 3) Step 1/2 와 동일 기준으로 미입력 판정 — row 존재 AND value 존재 (null/undefined 아님).
512
519
  const hasValid = (v) => v.value !== null && v.value !== undefined;
@@ -585,6 +592,7 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
585
592
  projectXKpiValues(projectId: $projectId) {
586
593
  id
587
594
  value
595
+ score
588
596
  valueDate
589
597
  kpi {
590
598
  id
@@ -616,7 +624,7 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
616
624
  this._fetchKpiWithGradesByPattern(kpiNamePattern, metric.label);
617
625
  }
618
626
  async _fetchKpiWithGradesByPattern(kpiNamePattern, displayName) {
619
- var _a, _b, _c, _d;
627
+ var _a, _b, _c, _d, _e;
620
628
  try {
621
629
  // X-KPI 이름 패턴으로 전체 이름 찾기 (projectXKpiValues에서)
622
630
  const kpiValue = this.projectXKpiValues.find(kv => { var _a; return ((_a = kv.kpi) === null || _a === void 0 ? void 0 : _a.name) && kv.kpi.name.includes(kpiNamePattern); });
@@ -657,7 +665,8 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
657
665
  scoreType: kpi.scoreType || '',
658
666
  valueType: kpi.valueType || '',
659
667
  value: (_c = kpiValue.value) !== null && _c !== void 0 ? _c : null,
660
- unit: ((_d = kpi.vizMeta) === null || _d === void 0 ? void 0 : _d.unit) || ''
668
+ score: (_d = kpiValue.score) !== null && _d !== void 0 ? _d : null,
669
+ unit: ((_e = kpi.vizMeta) === null || _e === void 0 ? void 0 : _e.unit) || ''
661
670
  };
662
671
  this.showScoreDistribution = true;
663
672
  }
@@ -693,9 +702,10 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
693
702
  }
694
703
  /** 등급 기준 표시 (ASSESSED / 1~5 정수 score) — 현재 값 강조 + CUSTOM은 2D 히트맵도 표시 */
695
704
  _renderAssessmentGrades() {
696
- var _a, _b, _c;
705
+ var _a;
697
706
  const kpi = this.selectedKpi;
698
- const currentScore = (kpi === null || kpi === void 0 ? void 0 : kpi.value) != null ? Math.round(kpi.value) : 0;
707
+ // score 진짜 등급. kpi.value raw 측정값 (편차율 ) 이라 score 쓰면 됨.
708
+ const currentScore = (kpi === null || kpi === void 0 ? void 0 : kpi.score) != null ? Math.round(kpi.score) : 0;
699
709
  const grades = Array.isArray(kpi === null || kpi === void 0 ? void 0 : kpi.grades) ? kpi.grades : [];
700
710
  const colors = ['#e53935', '#ff9800', '#ffca28', '#66bb6a', '#2e7d32'];
701
711
  const is2D = (kpi === null || kpi === void 0 ? void 0 : kpi.scoreType) === 'CUSTOM' || (kpi === null || kpi === void 0 ? void 0 : kpi.valueType) === 'COMPOSITE' ||
@@ -714,11 +724,10 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
714
724
  // ASSESSED(감리자 평가)이고 grades에 description이 있는 경우만 한 줄씩 상세 표시
715
725
  // X13 등 COMPOSITE/CUSTOM은 인라인 칩으로 간결하게
716
726
  const hasDescriptions = !is2D && grades.length > 0 && grades.some((g) => g.description);
727
+ // ASSESSMENT (감리자 평가) 의 등급 description 만 노출. 2D lookup 은 score 가 이미
728
+ // popup-header 큰 점수로 표시되므로 등급 기준 행/동그라미 중복 제거.
717
729
  return html `
718
- <div style="padding: 20px;">
719
- <div style="font-size: 1rem; font-weight: 700; margin-bottom: 16px; color: #333;">
720
- 등급 기준 ${currentScore >= 1 ? html `<span style="color: ${colors[currentScore - 1]}; margin-left: 8px;">현재: ${currentScore}점</span>` : ''}
721
- </div>
730
+ <div style="padding: 20px; display: flex; flex-direction: column; height: 100%;">
722
731
  ${hasDescriptions
723
732
  ? items.map(item => html `
724
733
  <div
@@ -744,30 +753,19 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
744
753
  ${item.desc}
745
754
  </span>
746
755
  </div>`)
747
- : html `
748
- <div style="display: flex; gap: 6px; flex-wrap: wrap;">
749
- ${items.map(item => html `
750
- <span style="
751
- display: inline-flex; align-items: center; justify-content: center;
752
- width: 36px; height: 36px; border-radius: 50%;
753
- background: ${item.active ? item.color : '#e0e0e0'};
754
- color: ${item.active ? '#fff' : '#999'};
755
- font-weight: 700; font-size: 14px;
756
- border: 2px solid ${item.active ? item.color : 'transparent'};
757
- ">${item.score}</span>`)}
758
- </div>
759
- `}
756
+ : ''}
760
757
 
761
758
  ${is2D && ((_a = kpi === null || kpi === void 0 ? void 0 : kpi.grades) === null || _a === void 0 ? void 0 : _a.rows)
762
759
  ? html `
763
- <div style="margin-top: 20px; border-top: 1px solid #e0e0e0; padding-top: 16px;">
760
+ <div style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
764
761
  <div style="font-size: 0.9rem; font-weight: 700; color: #333; margin-bottom: 8px;">
765
762
  공정률 × 공기편차 성과 분포
766
763
  </div>
767
- <div style="height: 250px;">
764
+ <div style="flex: 1; min-height: 0;">
768
765
  <kpi-2d-lookup-chart
769
766
  .grades=${kpi.grades}
770
- .progressRate=${(_c = (_b = this.project) === null || _b === void 0 ? void 0 : _b.totalProgress) !== null && _c !== void 0 ? _c : 50}
767
+ .value=${this._getScheduleDeviation()}
768
+ .progressRate=${this._getProgressRate()}
771
769
  ></kpi-2d-lookup-chart>
772
770
  </div>
773
771
  </div>
@@ -882,6 +880,32 @@ let SvProjectDetailPage = class SvProjectDetailPage extends ScopedElementsMixin(
882
880
  });
883
881
  return data.sort((a, b) => a.org.localeCompare(b.org));
884
882
  }
883
+ /**
884
+ * KpiMetric.name 이 주어진 이름인 KpiMetricValue 중 가장 최근 valueDate 의 value.
885
+ * 2D lookup chart 의 progressRate / value 도출용.
886
+ */
887
+ _getLatestMetricValue(metricName) {
888
+ const candidates = this.allKpiMetricValues
889
+ .filter(v => { var _a; return ((_a = v.metric) === null || _a === void 0 ? void 0 : _a.name) === metricName && v.value !== null && v.value !== undefined; })
890
+ .sort((a, b) => (b.valueDate || '').localeCompare(a.valueDate || ''));
891
+ return candidates.length > 0 ? candidates[0].value : null;
892
+ }
893
+ /** 2D lookup chart 의 progressRate (X축, 실적공정율 %) — 실적공정율 metric latest 우선, 없으면 fallback. */
894
+ _getProgressRate() {
895
+ var _a, _b;
896
+ const latest = this._getLatestMetricValue('실적공정율');
897
+ if (latest !== null)
898
+ return latest;
899
+ return (_b = (_a = this.project) === null || _a === void 0 ? void 0 : _a.totalProgress) !== null && _b !== void 0 ? _b : 50;
900
+ }
901
+ /** 2D lookup chart 의 value (Y축, 공기편차 비율) — '일정 이탈 수준' metric latest 우선, 없으면 selectedKpi.value. */
902
+ _getScheduleDeviation() {
903
+ var _a, _b;
904
+ const latest = this._getLatestMetricValue('일정 이탈 수준');
905
+ if (latest !== null)
906
+ return latest;
907
+ return (_b = (_a = this.selectedKpi) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : null;
908
+ }
885
909
  getProjectYKpiValue(kpiName) {
886
910
  var _a, _b;
887
911
  // 현재 프로젝트의 kpiValues 배열에서 해당 KPI 값을 찾습니다
@@ -1365,6 +1389,23 @@ SvProjectDetailPage.styles = [
1365
1389
  font-weight: 700;
1366
1390
  color: #35618e;
1367
1391
  }
1392
+ .popup-score {
1393
+ display: flex;
1394
+ align-items: baseline;
1395
+ gap: 6px;
1396
+ margin-left: auto;
1397
+ margin-right: 16px;
1398
+ }
1399
+ .popup-score .score-value {
1400
+ font-size: 44px;
1401
+ font-weight: 800;
1402
+ color: #e53935;
1403
+ line-height: 1;
1404
+ }
1405
+ .popup-score .score-label {
1406
+ font-size: 14px;
1407
+ color: #888;
1408
+ }
1368
1409
  .popup-close {
1369
1410
  cursor: pointer;
1370
1411
  font-size: 24px;
@@ -1417,6 +1458,10 @@ __decorate([
1417
1458
  state(),
1418
1459
  __metadata("design:type", Array)
1419
1460
  ], SvProjectDetailPage.prototype, "pendingMetrics", void 0);
1461
+ __decorate([
1462
+ state(),
1463
+ __metadata("design:type", Array)
1464
+ ], SvProjectDetailPage.prototype, "allKpiMetricValues", void 0);
1420
1465
  SvProjectDetailPage = __decorate([
1421
1466
  customElement('sv-project-detail')
1422
1467
  ], SvProjectDetailPage);