@dssp/dkpi 1.0.0-y.0 → 1.0.1

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.
Files changed (213) hide show
  1. package/KPI-STATISTICS-SERVICE.md +233 -0
  2. package/_index.html +4 -9
  3. package/assets/helps/index.md +1 -1
  4. package/assets/images/project-image.png +0 -0
  5. package/assets/manifest.json +2 -2
  6. package/config/config.development.js +2 -1
  7. package/config/config.production.js +2 -1
  8. package/dist-client/bootstrap.d.ts +1 -0
  9. package/dist-client/bootstrap.js +15 -0
  10. package/dist-client/bootstrap.js.map +1 -1
  11. package/dist-client/components/kpi-2d-lookup-chart.d.ts +50 -0
  12. package/dist-client/components/kpi-2d-lookup-chart.js +263 -0
  13. package/dist-client/components/kpi-2d-lookup-chart.js.map +1 -0
  14. package/dist-client/components/kpi-boxplot-chart.d.ts +31 -0
  15. package/dist-client/components/kpi-boxplot-chart.js +306 -0
  16. package/dist-client/components/kpi-boxplot-chart.js.map +1 -0
  17. package/dist-client/components/kpi-lookup-chart.d.ts +58 -0
  18. package/dist-client/components/kpi-lookup-chart.js +464 -0
  19. package/dist-client/components/kpi-lookup-chart.js.map +1 -0
  20. package/dist-client/components/kpi-mini-trend-chart.d.ts +14 -0
  21. package/dist-client/components/kpi-mini-trend-chart.js +180 -0
  22. package/dist-client/components/kpi-mini-trend-chart.js.map +1 -0
  23. package/dist-client/components/kpi-radar-chart.d.ts +17 -0
  24. package/dist-client/components/kpi-radar-chart.js +259 -0
  25. package/dist-client/components/kpi-radar-chart.js.map +1 -0
  26. package/dist-client/components/kpi-single-boxplot-chart.d.ts +14 -2
  27. package/dist-client/components/kpi-single-boxplot-chart.js +237 -137
  28. package/dist-client/components/kpi-single-boxplot-chart.js.map +1 -1
  29. package/dist-client/components/kpi-step-lookup-chart.d.ts +33 -0
  30. package/dist-client/components/kpi-step-lookup-chart.js +181 -0
  31. package/dist-client/components/kpi-step-lookup-chart.js.map +1 -0
  32. package/dist-client/components/kpi-trend-chart.d.ts +25 -0
  33. package/dist-client/components/kpi-trend-chart.js +241 -0
  34. package/dist-client/components/kpi-trend-chart.js.map +1 -0
  35. package/dist-client/entries/auth/checkin.d.ts +38 -0
  36. package/dist-client/entries/auth/checkin.js +551 -0
  37. package/dist-client/entries/auth/checkin.js.map +1 -0
  38. package/dist-client/google-map/common-google-map.d.ts +35 -0
  39. package/dist-client/google-map/common-google-map.js +349 -0
  40. package/dist-client/google-map/common-google-map.js.map +1 -0
  41. package/dist-client/google-map/google-map-loader.d.ts +6 -0
  42. package/dist-client/google-map/google-map-loader.js +23 -0
  43. package/dist-client/google-map/google-map-loader.js.map +1 -0
  44. package/dist-client/pages/component/project-update-header.d.ts +1 -0
  45. package/dist-client/pages/component/project-update-header.js +127 -0
  46. package/dist-client/pages/component/project-update-header.js.map +1 -0
  47. package/dist-client/pages/kpi-admin/dssp-kpi-list-page.d.ts +22 -0
  48. package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js +57 -0
  49. package/dist-client/pages/kpi-admin/dssp-kpi-list-page.js.map +1 -0
  50. package/dist-client/pages/kpi-admin/dssp-kpi-overview.d.ts +47 -0
  51. package/dist-client/pages/kpi-admin/dssp-kpi-overview.js +393 -0
  52. package/dist-client/pages/kpi-admin/dssp-kpi-overview.js.map +1 -0
  53. package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.d.ts +20 -0
  54. package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js +445 -0
  55. package/dist-client/pages/kpi-admin/kpi-grade-2d-editor.js.map +1 -0
  56. package/dist-client/pages/kpi-admin/kpi-system-guide.d.ts +18 -0
  57. package/dist-client/pages/kpi-admin/kpi-system-guide.js +696 -0
  58. package/dist-client/pages/kpi-admin/kpi-system-guide.js.map +1 -0
  59. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +18 -0
  60. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +259 -0
  61. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
  62. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +22 -0
  63. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +346 -0
  64. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
  65. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +26 -0
  66. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +433 -0
  67. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
  68. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
  69. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +78 -0
  70. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
  71. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +46 -0
  72. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +945 -0
  73. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
  74. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +34 -0
  75. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +238 -0
  76. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
  77. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +37 -0
  78. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +689 -0
  79. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
  80. package/dist-client/pages/kpi-dashboard/kpi-alert-panel.d.ts +18 -0
  81. package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js +131 -0
  82. package/dist-client/pages/kpi-dashboard/kpi-alert-panel.js.map +1 -0
  83. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +57 -0
  84. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +866 -0
  85. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
  86. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +63 -0
  87. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +1097 -0
  88. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -0
  89. package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.d.ts +12 -0
  90. package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js +82 -0
  91. package/dist-client/pages/kpi-dashboard/kpi-grade-visualization.js.map +1 -0
  92. package/dist-client/pages/kpi-dashboard/kpi-history-viewer.d.ts +11 -0
  93. package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js +65 -0
  94. package/dist-client/pages/kpi-dashboard/kpi-history-viewer.js.map +1 -0
  95. package/dist-client/pages/kpi-dashboard/kpi-list-summary.d.ts +13 -0
  96. package/dist-client/pages/kpi-dashboard/kpi-list-summary.js +115 -0
  97. package/dist-client/pages/kpi-dashboard/kpi-list-summary.js.map +1 -0
  98. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.d.ts +15 -0
  99. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js +147 -0
  100. package/dist-client/pages/kpi-dashboard/kpi-performance-summary.js.map +1 -0
  101. package/dist-client/pages/kpi-dashboard/kpi-value-entry.d.ts +7 -0
  102. package/dist-client/pages/kpi-dashboard/kpi-value-entry.js +86 -0
  103. package/dist-client/pages/kpi-dashboard/kpi-value-entry.js.map +1 -0
  104. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.d.ts +1 -2
  105. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js +1 -13
  106. package/dist-client/pages/kpi-metric-value/kpi-metric-value-editor-page.js.map +1 -1
  107. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.d.ts +1 -2
  108. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js +20 -11
  109. package/dist-client/pages/kpi-metric-value/kpi-metric-value-list-page.js.map +1 -1
  110. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.d.ts +1 -2
  111. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js +1 -2
  112. package/dist-client/pages/kpi-metric-value/kpi-metric-value-manual-entry-page.js.map +1 -1
  113. package/dist-client/pages/kpi-value/kpi-value-list-page.d.ts +1 -2
  114. package/dist-client/pages/kpi-value/kpi-value-list-page.js +22 -33
  115. package/dist-client/pages/kpi-value/kpi-value-list-page.js.map +1 -1
  116. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.d.ts +56 -0
  117. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js +609 -67
  118. package/dist-client/pages/project-complete-tabs/pc-tab1-plan.js.map +1 -1
  119. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.d.ts +15 -3
  120. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js +180 -65
  121. package/dist-client/pages/project-complete-tabs/pc-tab2-rating.js.map +1 -1
  122. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.d.ts +13 -4
  123. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js +213 -91
  124. package/dist-client/pages/project-complete-tabs/pc-tab3-upload.js.map +1 -1
  125. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.d.ts +51 -0
  126. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js +693 -0
  127. package/dist-client/pages/project-complete-tabs/pc-tab4-monthly.js.map +1 -0
  128. package/dist-client/pages/sv-project-complete.d.ts +12 -3
  129. package/dist-client/pages/sv-project-complete.js +175 -29
  130. package/dist-client/pages/sv-project-complete.js.map +1 -1
  131. package/dist-client/pages/sv-project-completed-list.d.ts +3 -0
  132. package/dist-client/pages/sv-project-completed-list.js +81 -22
  133. package/dist-client/pages/sv-project-completed-list.js.map +1 -1
  134. package/dist-client/pages/sv-project-detail.d.ts +60 -0
  135. package/dist-client/pages/sv-project-detail.js +1092 -85
  136. package/dist-client/pages/sv-project-detail.js.map +1 -1
  137. package/dist-client/pages/sv-project-list.d.ts +20 -2
  138. package/dist-client/pages/sv-project-list.js +180 -30
  139. package/dist-client/pages/sv-project-list.js.map +1 -1
  140. package/dist-client/pages/sv-project-update.d.ts +91 -0
  141. package/dist-client/pages/sv-project-update.js +1150 -0
  142. package/dist-client/pages/sv-project-update.js.map +1 -0
  143. package/dist-client/pages/sv-user-management.d.ts +1 -0
  144. package/dist-client/pages/sv-user-management.js +5 -0
  145. package/dist-client/pages/sv-user-management.js.map +1 -1
  146. package/dist-client/route.d.ts +1 -1
  147. package/dist-client/route.js +29 -2
  148. package/dist-client/route.js.map +1 -1
  149. package/dist-client/shared/complete-api.d.ts +45 -5
  150. package/dist-client/shared/complete-api.js +177 -28
  151. package/dist-client/shared/complete-api.js.map +1 -1
  152. package/dist-client/shared/func.d.ts +1 -1
  153. package/dist-client/shared/func.js.map +1 -1
  154. package/dist-client/shared/geo-group-mapping.d.ts +25 -0
  155. package/dist-client/shared/geo-group-mapping.js +189 -0
  156. package/dist-client/shared/geo-group-mapping.js.map +1 -0
  157. package/dist-client/shared/integration-fetch.d.ts +35 -0
  158. package/dist-client/shared/integration-fetch.js +53 -0
  159. package/dist-client/shared/integration-fetch.js.map +1 -0
  160. package/dist-client/shared/kpi-project-sync.d.ts +16 -0
  161. package/dist-client/shared/kpi-project-sync.js +129 -0
  162. package/dist-client/shared/kpi-project-sync.js.map +1 -0
  163. package/dist-client/tsconfig.tsbuildinfo +1 -1
  164. package/dist-client/viewparts/menu-tools.d.ts +11 -3
  165. package/dist-client/viewparts/menu-tools.js +126 -75
  166. package/dist-client/viewparts/menu-tools.js.map +1 -1
  167. package/dist-server/scripts/calculate-kpi-scores.js +65 -3
  168. package/dist-server/scripts/calculate-kpi-scores.js.map +1 -1
  169. package/dist-server/scripts/load-grade-data-migration.d.ts +4 -0
  170. package/dist-server/scripts/load-grade-data-migration.js +95 -10
  171. package/dist-server/scripts/load-grade-data-migration.js.map +1 -1
  172. package/dist-server/scripts/propagate-parent-kpi-values.d.ts +4 -0
  173. package/dist-server/scripts/propagate-parent-kpi-values.js +423 -77
  174. package/dist-server/scripts/propagate-parent-kpi-values.js.map +1 -1
  175. package/dist-server/scripts/recalculate-by-project-name.d.ts +2 -0
  176. package/dist-server/scripts/recalculate-by-project-name.js +72 -0
  177. package/dist-server/scripts/recalculate-by-project-name.js.map +1 -0
  178. package/dist-server/service/index.d.ts +1 -3
  179. package/dist-server/service/index.js +3 -4
  180. package/dist-server/service/index.js.map +1 -1
  181. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.d.ts +192 -1
  182. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js +993 -38
  183. package/dist-server/service/kpi-metric-value/kpi-metric-value-mutation.js.map +1 -1
  184. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.d.ts +9 -1
  185. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js +33 -8
  186. package/dist-server/service/kpi-metric-value/kpi-metric-value-query.js.map +1 -1
  187. package/dist-server/service/kpi-stat/index.d.ts +4 -0
  188. package/dist-server/service/kpi-stat/index.js +8 -0
  189. package/dist-server/service/kpi-stat/index.js.map +1 -0
  190. package/dist-server/service/kpi-stat/kpi-stat-query.d.ts +13 -0
  191. package/dist-server/service/kpi-stat/kpi-stat-query.js +751 -0
  192. package/dist-server/service/kpi-stat/kpi-stat-query.js.map +1 -0
  193. package/dist-server/service/kpi-stat/kpi-stat-types.d.ts +32 -0
  194. package/dist-server/service/kpi-stat/kpi-stat-types.js +180 -0
  195. package/dist-server/service/kpi-stat/kpi-stat-types.js.map +1 -0
  196. package/dist-server/service/kpi-value/kpi-value-query.d.ts +2 -1
  197. package/dist-server/service/kpi-value/kpi-value-query.js +49 -6
  198. package/dist-server/service/kpi-value/kpi-value-query.js.map +1 -1
  199. package/dist-server/tsconfig.tsbuildinfo +1 -1
  200. package/kpi-module-service-tests.md +1286 -0
  201. package/kpi-module-test-report.md +676 -0
  202. package/kpi-module-unit-test-detailed-report.md +925 -0
  203. package/kpi-module-unit-tests-detailed.md +1452 -0
  204. package/openapi/unstable.yaml +2 -2
  205. package/package.json +58 -54
  206. package/recalculate-batch.sh +64 -0
  207. package/recalculate-projects-range.sh +98 -0
  208. package/schema.graphql +1159 -55
  209. package/things-factory.config.js +6 -1
  210. package/translations/en.json +5 -1
  211. package/translations/ko.json +5 -1
  212. package/views/auth-page.html +4 -5
  213. package/views/public/home.html +5 -6
@@ -0,0 +1,693 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { css, html, LitElement } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { client } from '@operato/graphql';
5
+ import { gql } from '@apollo/client';
6
+ import { notify } from '@operato/layout';
7
+ import { OxPrompt } from '@operato/popup';
8
+ import { hasPrivilege } from '@things-factory/auth-base/dist-client';
9
+ import moment from 'moment-timezone';
10
+ import { tenantHeaders } from '@dssp/project/dist-client/shared/domain-context';
11
+ import { ProjectState } from '../sv-project-list';
12
+ import { syncKpiToProjectInfo } from '../../shared/kpi-project-sync';
13
+ import '@things-factory/kpi/dist-client/viewparts/kpi-metric-input';
14
+ /**
15
+ * 프로젝트 완공 처리 탭 4 — 월별 KPI 데이터 그리드.
16
+ *
17
+ * 데이터 source = `KpiMetric` (periodType=MONTH) + `KpiMetricValue` (그 metric 의
18
+ * 월별 값들). 이전 버전 (DataSet/DataSample) 폐기. KpiMetricValue 의 unique
19
+ * 인덱스 (domain, metric, valueDate, org) 가 (도메인, 프로젝트, 메트릭, 월) 단일 row
20
+ * 정책을 보장.
21
+ *
22
+ * 단방향 흐름:
23
+ * - 탭1/탭2 의 metric 저장 (valueDate=YYYY-MM-01) → 그 월 row 가 여기 자동 노출
24
+ * - 여기서 cell 수정 → 그 metric × 그 월 row 만 update (탭1/탭2 의 "현재값" 영향 없음 —
25
+ * 탭1/탭2 는 KpiMetricValueProvider 가 latest 또는 valueDate-based lookup 으로 동작)
26
+ */
27
+ /** rating 셀렉트로 표시할 metric 인지 판단 — 이름에 '평가' 또는 '이탈' 포함되면 1~5 rating. */
28
+ function isRating(metricName) {
29
+ return /평가|이탈/.test(metricName || '');
30
+ }
31
+ let SvProjectCompleteTab4Monthly = class SvProjectCompleteTab4Monthly extends LitElement {
32
+ constructor() {
33
+ super(...arguments);
34
+ this.project = {};
35
+ /** 월별 metric 목록 (periodType=MONTH). KpiMetric admin 에 등록된 것 기준. */
36
+ this.monthlyMetrics = [];
37
+ /** monthRows: { workDate:'YYYY-MM', values:{[metricId]:value}, originalValues, dirty }[] */
38
+ this.monthRows = [];
39
+ this.addYear = new Date().getFullYear();
40
+ this.addMonth = new Date().getMonth() + 1;
41
+ /** kpi:input — Step4 월별 데이터 저장 권한 (cumulative 와 같은 권한) */
42
+ this.canSave = false;
43
+ }
44
+ async connectedCallback() {
45
+ super.connectedCallback();
46
+ this.canSave = await hasPrivilege({
47
+ category: 'kpi',
48
+ privilege: 'input',
49
+ domainOwnerGranted: true,
50
+ superUserGranted: true
51
+ });
52
+ }
53
+ render() {
54
+ const years = this._getYearRange();
55
+ return html `
56
+ <div class="title">
57
+ 프로젝트 수행 기간 동안의 월별 데이터를 입력합니다.<br />
58
+ 공정률, 감리자 정성 평가(1~5) 등의 항목을 월 단위로 기록하여 성과 추세 분석에 활용합니다.
59
+ </div>
60
+
61
+ ${this.isCompleted
62
+ ? html `<div class="toolbar">
63
+ <span class="locked-note">🔒 종료된 프로젝트 — 월 추가 불가, 기존 월 데이터만 수정할 수 있습니다.</span>
64
+ </div>`
65
+ : html `<div class="toolbar">
66
+ <select
67
+ .value=${String(this.addYear)}
68
+ @change=${(e) => (this.addYear = Number(e.target.value))}
69
+ >
70
+ ${years.map(y => html `<option value=${y} ?selected=${y === this.addYear}>${y}년</option>`)}
71
+ </select>
72
+ <select
73
+ .value=${String(this.addMonth)}
74
+ @change=${(e) => (this.addMonth = Number(e.target.value))}
75
+ >
76
+ ${[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(m => html `<option value=${m} ?selected=${m === this.addMonth}>${m}월</option>`)}
77
+ </select>
78
+ <div class="add-btn" @click=${this._addMonth}>+ 월 추가</div>
79
+ </div>`}
80
+
81
+ ${this.monthlyMetrics.length === 0
82
+ ? html `<div class="empty-msg">
83
+ 월별 metric (periodType=MONTH) 이 등록되어 있지 않습니다. KPI 관리자에서 먼저 등록해주세요.
84
+ </div>`
85
+ : this.monthRows.length === 0
86
+ ? html `<div class="empty-msg">등록된 월별 데이터가 없습니다. 위에서 월을 추가해주세요.</div>`
87
+ : html `
88
+ <div class="grid-wrapper">
89
+ <table>
90
+ <thead>
91
+ <tr>
92
+ <th>월</th>
93
+ ${this.monthlyMetrics.map(m => html `
94
+ <th
95
+ class=${this._isMetricNeverEntered(m.id) ? 'pending' : ''}
96
+ title=${this._isMetricNeverEntered(m.id) ? '한 번도 입력된 적 없는 metric' : ''}
97
+ >
98
+ ${m.name}${m.unit ? ` (${m.unit})` : ''}
99
+ </th>
100
+ `)}
101
+ <th>상태</th>
102
+ <th></th>
103
+ </tr>
104
+ </thead>
105
+ <tbody>
106
+ ${this.monthRows.map((row, rowIdx) => html `
107
+ <tr>
108
+ <td class="month-cell ${this._isCurrentMonth(row.workDate) ? 'current' : ''}">${row.workDate}</td>
109
+ ${this.monthlyMetrics.map(m => {
110
+ var _a;
111
+ const pending = this._isCellPending(row, m.id);
112
+ // 위젯은 metric.meta.input 선언으로 결정(이름추측 isRating 제거).
113
+ return html `
114
+ <td class=${pending ? 'pending' : ''}>
115
+ <kpi-metric-input
116
+ .metric=${this._withInputHint(m)}
117
+ .value=${(_a = row.values[m.id]) !== null && _a !== void 0 ? _a : null}
118
+ ?disabled=${!this.canSave}
119
+ @metric-input-change=${(e) => this._setCell(rowIdx, m.id, e.detail.value)}
120
+ ></kpi-metric-input>
121
+ </td>
122
+ `;
123
+ })}
124
+ <td>
125
+ ${row.dirty
126
+ ? html `<span class="status-unsaved">수정됨</span>`
127
+ : row.isAuto
128
+ ? html `<span class="status-pending">미입력</span>`
129
+ : row.isNew
130
+ ? html `<span class="status-new">신규</span>`
131
+ : html `<span class="status-saved">저장됨</span>`}
132
+ </td>
133
+ <td>
134
+ <span class="delete-btn" title="삭제" @click=${() => this._removeMonth(rowIdx)}>✕</span>
135
+ </td>
136
+ </tr>
137
+ `)}
138
+ </tbody>
139
+ </table>
140
+ </div>
141
+ `}
142
+
143
+ <div class="button-line">
144
+ <div class="ghost-btn" @click=${this._reset}>초기화</div>
145
+ <div
146
+ class="ghost-btn secondary ${this.canSave ? '' : 'disabled'}"
147
+ title=${this.canSave ? '' : 'kpi:input 권한 필요'}
148
+ @click=${() => this.canSave && this._save()}
149
+ >
150
+ 저장
151
+ </div>
152
+ </div>
153
+ `;
154
+ }
155
+ willUpdate(changedProperties) {
156
+ var _a;
157
+ super.willUpdate(changedProperties);
158
+ if (changedProperties.has('project') && ((_a = this.project) === null || _a === void 0 ? void 0 : _a.id)) {
159
+ this._loadData();
160
+ }
161
+ }
162
+ _getYearRange() {
163
+ var _a, _b;
164
+ const startYear = ((_a = this.project) === null || _a === void 0 ? void 0 : _a.startDate) ? new Date(this.project.startDate).getFullYear() : new Date().getFullYear() - 2;
165
+ const endYear = ((_b = this.project) === null || _b === void 0 ? void 0 : _b.endDate) ? new Date(this.project.endDate).getFullYear() + 1 : new Date().getFullYear() + 1;
166
+ const years = [];
167
+ for (let y = startYear; y <= endYear; y++)
168
+ years.push(y);
169
+ return years;
170
+ }
171
+ _isCurrentMonth(workDate) {
172
+ return workDate === moment().tz('Asia/Seoul').format('YYYY-MM');
173
+ }
174
+ /**
175
+ * 월별 metric 정의 + 그 프로젝트의 월별 KpiMetricValue 들을 조회해 그리드 row 구성.
176
+ *
177
+ * 1) KpiMetric where periodType=MONTH → monthlyMetrics
178
+ * 2) KpiMetricValue where org=projectId → 월별로 그룹핑하여 monthRows
179
+ */
180
+ async _loadData() {
181
+ var _a, _b, _c, _d, _e, _f;
182
+ try {
183
+ // 1) 월별 metric 목록
184
+ const metricsResp = await client.query({
185
+ query: gql `
186
+ query KpiMetrics {
187
+ kpiMetrics {
188
+ items {
189
+ id
190
+ name
191
+ unit
192
+ periodType
193
+ rank
194
+ meta
195
+ collectType
196
+ }
197
+ }
198
+ }
199
+ `,
200
+ context: { headers: tenantHeaders((_a = this.project) === null || _a === void 0 ? void 0 : _a.code) }
201
+ });
202
+ const all = ((_c = (_b = metricsResp.data) === null || _b === void 0 ? void 0 : _b.kpiMetrics) === null || _c === void 0 ? void 0 : _c.items) || [];
203
+ // periodType=MONTH 만, rank 오름차순(미지정 null 은 뒤로, 동률은 기존 순서 유지).
204
+ this.monthlyMetrics = all
205
+ .filter((m) => m.periodType === 'MONTH')
206
+ .map((m, i) => ({ m, i }))
207
+ .sort((a, b) => {
208
+ var _a, _b;
209
+ const ra = (_a = a.m.rank) !== null && _a !== void 0 ? _a : Number.POSITIVE_INFINITY;
210
+ const rb = (_b = b.m.rank) !== null && _b !== void 0 ? _b : Number.POSITIVE_INFINITY;
211
+ return ra !== rb ? ra - rb : a.i - b.i;
212
+ })
213
+ .map((x) => x.m);
214
+ if (this.monthlyMetrics.length === 0) {
215
+ this.monthRows = [];
216
+ return;
217
+ }
218
+ // 2) 프로젝트의 KpiMetricValue 들 (org=projectId). 월별 metric 만 client 측에서 필터.
219
+ const monthlyMetricIds = new Set(this.monthlyMetrics.map((m) => m.id));
220
+ const valuesResp = await client.query({
221
+ query: gql `
222
+ query KpiMetricValues($filters: [Filter!]) {
223
+ kpiMetricValues(filters: $filters) {
224
+ items {
225
+ id
226
+ value
227
+ unit
228
+ periodType
229
+ valueDate
230
+ metricId
231
+ }
232
+ }
233
+ }
234
+ `,
235
+ variables: { filters: [{ name: 'org', operator: 'eq', value: this.project.id }] },
236
+ context: { headers: tenantHeaders((_d = this.project) === null || _d === void 0 ? void 0 : _d.code) }
237
+ });
238
+ const allValues = ((_f = (_e = valuesResp.data) === null || _e === void 0 ? void 0 : _e.kpiMetricValues) === null || _f === void 0 ? void 0 : _f.items) || [];
239
+ // periodType='MONTH' 만 그리드에 — 비월별 row 가 valueDate prefix 로 잘못 묶이지 않도록.
240
+ const monthlyValues = allValues.filter((v) => monthlyMetricIds.has(v.metricId) && v.periodType === 'MONTH');
241
+ // 월별 그룹핑 — valueDate YYYY-MM-DD 의 앞 7자리. id 도 보존 (삭제 시 사용).
242
+ // 현재월(=this month) row 는 운영 원칙상 표시 대상 아님 → 아예 bucket 제외.
243
+ const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM');
244
+ const byMonth = new Map();
245
+ for (const v of monthlyValues) {
246
+ const ym = (v.valueDate || '').slice(0, 7);
247
+ if (!ym)
248
+ continue;
249
+ if (ym === currentYm)
250
+ continue; // 현재월 record 무시 (DB 에 우연히 있더라도)
251
+ if (!byMonth.has(ym))
252
+ byMonth.set(ym, { workDate: ym, values: {}, originalValues: {}, valueIds: {}, dirty: false });
253
+ const row = byMonth.get(ym);
254
+ row.values[v.metricId] = v.value;
255
+ row.originalValues[v.metricId] = v.value;
256
+ row.valueIds[v.metricId] = v.id;
257
+ }
258
+ // project.startDate ~ 현재월 사이 누락된 월 placeholder row 자동 추가.
259
+ // 사용자가 값을 입력하지 않으면 dirty 가 안 되므로 저장 시 무시되어 spurious patch 없음.
260
+ for (const ym of this._generateExpectedMonths()) {
261
+ if (!byMonth.has(ym)) {
262
+ byMonth.set(ym, {
263
+ workDate: ym,
264
+ values: {},
265
+ originalValues: {},
266
+ valueIds: {},
267
+ dirty: false,
268
+ isAuto: true
269
+ });
270
+ }
271
+ }
272
+ this.monthRows = Array.from(byMonth.values()).sort((a, b) => b.workDate.localeCompare(a.workDate));
273
+ }
274
+ catch (e) {
275
+ console.error('Failed to load monthly data:', e);
276
+ this.monthRows = [];
277
+ }
278
+ }
279
+ /** project.startDate (YYYY-MM) ~ **전월** (YYYY-MM) 까지 매월 문자열 배열.
280
+ * 현재월은 의도적으로 제외 — 운영 원칙상 "이번달 데이터는 아직 입력 대상이 아님". */
281
+ _generateExpectedMonths() {
282
+ var _a;
283
+ const startStr = (((_a = this.project) === null || _a === void 0 ? void 0 : _a.startDate) || '').slice(0, 7);
284
+ if (!startStr || !/^\d{4}-\d{2}$/.test(startStr))
285
+ return [];
286
+ const tz = 'Asia/Seoul';
287
+ let cur = moment.tz(startStr, 'YYYY-MM', tz);
288
+ const end = moment.tz(moment().tz(tz).subtract(1, 'month').format('YYYY-MM'), 'YYYY-MM', tz);
289
+ if (!cur.isValid() || cur.isAfter(end))
290
+ return [];
291
+ const months = [];
292
+ while (cur.isSameOrBefore(end)) {
293
+ months.push(cur.format('YYYY-MM'));
294
+ cur = cur.add(1, 'month');
295
+ }
296
+ return months;
297
+ }
298
+ /** 셀 미입력 여부 — 값 없음 AND 그 월이 **전월** (직전 한 달) 인 경우만 pending.
299
+ * 과월 전체가 아니라 전월 한 달에 한해서만 강조 — 사용자 입력 흐름(이번 달에 지난달 데이터)과 일치. */
300
+ _isCellPending(row, metricId) {
301
+ var _a;
302
+ const v = (_a = row.values) === null || _a === void 0 ? void 0 : _a[metricId];
303
+ if (v !== undefined && v !== null && v !== '')
304
+ return false;
305
+ const lastMonth = moment().tz('Asia/Seoul').subtract(1, 'month').format('YYYY-MM');
306
+ return row.workDate === lastMonth;
307
+ }
308
+ /** metric 컬럼이 전 row 통틀어 한 번도 값이 없으면 헤더 강조. */
309
+ _isMetricNeverEntered(metricId) {
310
+ return !this.monthRows.some(r => {
311
+ var _a;
312
+ const v = (_a = r.values) === null || _a === void 0 ? void 0 : _a[metricId];
313
+ return v !== undefined && v !== null && v !== '';
314
+ });
315
+ }
316
+ /** 종료(COMPLETED) 프로젝트 — 신규 월 추가 금지, 기존 월만 수정 가능. */
317
+ get isCompleted() {
318
+ var _a;
319
+ return ((_a = this.project) === null || _a === void 0 ? void 0 : _a.state) === ProjectState.COMPLETED;
320
+ }
321
+ _addMonth() {
322
+ // 종료된 프로젝트는 새 월을 추가할 수 없다 (기존 월 데이터 수정만 허용).
323
+ if (this.isCompleted) {
324
+ notify({ message: '종료된 프로젝트는 월을 추가할 수 없습니다. 기존 월 데이터만 수정할 수 있습니다.' });
325
+ return;
326
+ }
327
+ const workDate = `${this.addYear}-${String(this.addMonth).padStart(2, '0')}`;
328
+ // 현재월 row 는 추가 금지 — 운영 원칙상 이번달 데이터는 입력 대상 아님.
329
+ const currentYm = moment().tz('Asia/Seoul').format('YYYY-MM');
330
+ if (workDate === currentYm) {
331
+ notify({ message: '현재월(이번 달) 데이터는 입력 대상이 아닙니다.' });
332
+ return;
333
+ }
334
+ // 미래월도 차단
335
+ if (workDate > currentYm) {
336
+ notify({ message: '미래월은 입력 대상이 아닙니다.' });
337
+ return;
338
+ }
339
+ if (this.monthRows.some(r => r.workDate === workDate)) {
340
+ notify({ message: `${workDate}은 이미 존재합니다.` });
341
+ return;
342
+ }
343
+ const newRow = { workDate, values: {}, originalValues: {}, valueIds: {}, dirty: true, isNew: true };
344
+ const rows = [...this.monthRows, newRow];
345
+ rows.sort((a, b) => b.workDate.localeCompare(a.workDate));
346
+ this.monthRows = rows;
347
+ }
348
+ async _removeMonth(rowIdx) {
349
+ var _a;
350
+ const row = this.monthRows[rowIdx];
351
+ // 신규 row (DB 미반영) — 메모리에서만 제거
352
+ if (row.isNew) {
353
+ this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx);
354
+ return;
355
+ }
356
+ // 저장된 row — 그 월의 모든 KpiMetricValue id 들을 한 번에 삭제.
357
+ // 권한: things-factory 의 deleteKpiMetricValues 는 category="kpi", privilege="mutation"
358
+ // (superUser/도메인 owner 자동 통과, 일반 사용자는 별도 grant 필요).
359
+ const ids = Object.values(row.valueIds || {}).filter(Boolean);
360
+ if (ids.length === 0) {
361
+ this.monthRows = this.monthRows.filter((_, i) => i !== rowIdx);
362
+ return;
363
+ }
364
+ const ok = await OxPrompt.open({
365
+ title: `${row.workDate} 의 월별 데이터를 삭제하시겠습니까?`,
366
+ confirmButton: { text: '삭제' },
367
+ cancelButton: { text: '취소' }
368
+ });
369
+ if (!ok)
370
+ return;
371
+ try {
372
+ const response = await client.mutate({
373
+ mutation: gql `
374
+ mutation DeleteKpiMetricValues($ids: [String!]!) {
375
+ deleteKpiMetricValues(ids: $ids)
376
+ }
377
+ `,
378
+ variables: { ids },
379
+ context: { headers: tenantHeaders((_a = this.project) === null || _a === void 0 ? void 0 : _a.code) }
380
+ });
381
+ if (response.errors) {
382
+ throw new Error(response.errors.map((e) => e.message).join('\n'));
383
+ }
384
+ notify({ message: `${row.workDate} 월별 데이터 ${ids.length}건 삭제되었습니다.` });
385
+ await this._loadData();
386
+ }
387
+ catch (e) {
388
+ console.error('Failed to delete monthly values:', e);
389
+ notify({ message: '삭제 중 오류가 발생했습니다. 권한을 확인해주세요.' });
390
+ }
391
+ }
392
+ /**
393
+ * metric 입력 힌트 결정: DB 선언(meta.input) 우선. 없으면 전환용 파생(DB meta 미도입 동안).
394
+ * 향후 metric.meta.input 이 채워지면 이 파생 분기(및 isRating)는 제거한다.
395
+ */
396
+ _withInputHint(m) {
397
+ var _a, _b;
398
+ // 유효 힌트: DB 선언(meta.input) 우선, 없으면 전환용 파생.
399
+ const input = ((_a = m === null || m === void 0 ? void 0 : m.meta) === null || _a === void 0 ? void 0 : _a.input)
400
+ ? m.meta.input
401
+ : isRating(m === null || m === void 0 ? void 0 : m.name)
402
+ ? { widget: 'rating', max: 5, step: 1 }
403
+ : { widget: 'number', step: 1, min: 0 };
404
+ // 월별 그리드는 조밀해 별표 시인성이 떨어지므로, rating 은 1~N 셀렉터로 "표현"만 오버라이드.
405
+ // (metric 설정=별표 는 그대로. 넓은 완료평가 화면은 별표 유지)
406
+ if (input.widget === 'rating') {
407
+ const max = (_b = input.max) !== null && _b !== void 0 ? _b : 5;
408
+ const options = Array.from({ length: max }, (_, i) => ({ value: i + 1, label: String(i + 1) }));
409
+ return Object.assign(Object.assign({}, m), { meta: Object.assign(Object.assign({}, ((m === null || m === void 0 ? void 0 : m.meta) || {})), { input: { widget: 'select', options } }) });
410
+ }
411
+ return Object.assign(Object.assign({}, m), { meta: Object.assign(Object.assign({}, ((m === null || m === void 0 ? void 0 : m.meta) || {})), { input }) });
412
+ }
413
+ /** <kpi-metric-input> 이 방출하는 (이미 타입된) 값을 그대로 저장. */
414
+ _setCell(rowIdx, metricId, value) {
415
+ this.monthRows = this.monthRows.map((row, i) => i !== rowIdx ? row : Object.assign(Object.assign({}, row), { values: Object.assign(Object.assign({}, row.values), { [metricId]: value }), dirty: true }));
416
+ }
417
+ /**
418
+ * 저장 — dirty row 들 안의 변경된 cell 들을 KpiMetricValuePatch 로 모아
419
+ * updateKpiMetricValuesCumulative 한 번 호출 (backend upsert 가 unique 조합 처리).
420
+ */
421
+ async _save() {
422
+ var _a, _b;
423
+ const patches = [];
424
+ for (const row of this.monthRows) {
425
+ if (!row.dirty)
426
+ continue;
427
+ for (const m of this.monthlyMetrics) {
428
+ const value = row.values[m.id];
429
+ const original = row.originalValues[m.id];
430
+ if (value === undefined || value === null)
431
+ continue; // 빈 값은 skip (기존 유지)
432
+ if (value === original)
433
+ continue; // 변화 없으면 skip
434
+ patches.push({
435
+ metricId: m.id,
436
+ value,
437
+ unit: m.unit || '',
438
+ org: this.project.id,
439
+ periodType: 'MONTH',
440
+ valueDate: `${row.workDate}-01`
441
+ });
442
+ }
443
+ }
444
+ if (patches.length === 0) {
445
+ notify({ message: '변경된 데이터가 없습니다.' });
446
+ return;
447
+ }
448
+ try {
449
+ const response = await client.mutate({
450
+ mutation: gql `
451
+ mutation UpdateKpiMetricValuesCumulative($patches: [KpiMetricValuePatch!]!) {
452
+ updateKpiMetricValuesCumulative(patches: $patches) {
453
+ id
454
+ }
455
+ }
456
+ `,
457
+ variables: { patches },
458
+ context: { headers: tenantHeaders((_a = this.project) === null || _a === void 0 ? void 0 : _a.code) }
459
+ });
460
+ if (response.errors) {
461
+ throw new Error(response.errors.map((e) => e.message).join('\n'));
462
+ }
463
+ notify({ message: `${patches.length}건 저장되었습니다.` });
464
+ // 월별 → 기본정보(BuildingComplex) 역동기 (BC→월별 방향과 대칭).
465
+ // MONTH 양방향 필드(총투입인력·총설계변경 등)를 전월 값 기준으로 BC 에 반영.
466
+ // syncKpiToProjectInfo 의 findExistingRow 가 MONTH 는 전월 row 를 선택하므로
467
+ // monthRows 를 KpiMetricValue 형태로 평탄화해 전달한다.
468
+ try {
469
+ const kmvs = this.monthRows.flatMap((row) => this.monthlyMetrics.map((m) => ({
470
+ metricId: m.id,
471
+ value: row.values[m.id],
472
+ periodType: 'MONTH',
473
+ valueDate: `${row.workDate}-01`,
474
+ unit: m.unit || '',
475
+ org: this.project.id
476
+ })));
477
+ await syncKpiToProjectInfo(this.project, this.monthlyMetrics, kmvs, (_b = this.project) === null || _b === void 0 ? void 0 : _b.code);
478
+ }
479
+ catch (e) {
480
+ console.warn('[월별→프로젝트정보] BuildingComplex 동기 실패', e);
481
+ }
482
+ await this._loadData();
483
+ }
484
+ catch (e) {
485
+ console.error('Failed to save monthly values:', e);
486
+ notify({ message: '저장 중 오류가 발생했습니다.' });
487
+ }
488
+ }
489
+ _reset() {
490
+ this._loadData();
491
+ }
492
+ };
493
+ SvProjectCompleteTab4Monthly.styles = [
494
+ css `
495
+ :host {
496
+ display: block;
497
+ }
498
+ .title {
499
+ color: #212529;
500
+ font-size: 13px;
501
+ font-weight: 400;
502
+ line-height: 24px;
503
+ text-align: center;
504
+ margin-bottom: 8px;
505
+ }
506
+
507
+ .toolbar {
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: center;
511
+ gap: 12px;
512
+ margin-bottom: 12px;
513
+ }
514
+ .toolbar select {
515
+ padding: 5px 8px;
516
+ border: 1px solid rgba(0, 0, 0, 0.15);
517
+ border-radius: 5px;
518
+ font-size: 14px;
519
+ color: #212529;
520
+ }
521
+ .toolbar .add-btn {
522
+ display: inline-flex;
523
+ align-items: center;
524
+ gap: 4px;
525
+ padding: 5px 10px;
526
+ background: #35618e;
527
+ color: #fff;
528
+ border-radius: 5px;
529
+ cursor: pointer;
530
+ font-size: 13px;
531
+ }
532
+ .toolbar .locked-note {
533
+ font-size: 13px;
534
+ color: #6c757d;
535
+ }
536
+
537
+ .grid-wrapper {
538
+ overflow-x: auto;
539
+ padding: 0 6px;
540
+ }
541
+ table {
542
+ width: 100%;
543
+ border-collapse: collapse;
544
+ font-size: 13px;
545
+ }
546
+ thead th {
547
+ background: #f3f3fa;
548
+ border-top: 2px solid #0c4da2;
549
+ color: #212529;
550
+ text-align: center;
551
+ padding: 8px 6px;
552
+ white-space: nowrap;
553
+ font-weight: 500;
554
+ }
555
+ tbody td {
556
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
557
+ text-align: center;
558
+ padding: 6px 4px;
559
+ vertical-align: middle;
560
+ }
561
+ tbody tr:hover {
562
+ background: #f8fafc;
563
+ }
564
+ .month-cell {
565
+ color: #35618e;
566
+ font-weight: 600;
567
+ white-space: nowrap;
568
+ }
569
+ .month-cell.current {
570
+ color: #16a085;
571
+ }
572
+
573
+ input[type='number'] {
574
+ width: 70px;
575
+ padding: 4px 6px;
576
+ border: 1px solid rgba(0, 0, 0, 0.12);
577
+ border-radius: 4px;
578
+ text-align: center;
579
+ font-size: 13px;
580
+ }
581
+ input[type='number']:focus {
582
+ outline: none;
583
+ border-color: #35618e;
584
+ }
585
+
586
+ .rating-select {
587
+ padding: 4px 6px;
588
+ border: 1px solid rgba(0, 0, 0, 0.12);
589
+ border-radius: 4px;
590
+ font-size: 13px;
591
+ text-align: center;
592
+ }
593
+
594
+ .status-saved {
595
+ color: #16a085;
596
+ font-size: 12px;
597
+ }
598
+ .status-unsaved {
599
+ color: #e67e22;
600
+ font-size: 12px;
601
+ }
602
+ .status-new {
603
+ color: #3498db;
604
+ font-size: 12px;
605
+ }
606
+ .status-pending {
607
+ color: #e74c3c;
608
+ font-size: 12px;
609
+ font-weight: 600;
610
+ }
611
+
612
+ /* 미입력 셀 — 과월/현재월 row 에서 값이 비어있을 때 강조 */
613
+ td.pending input[type='number'],
614
+ td.pending .rating-select {
615
+ border-color: #e74c3c;
616
+ background-color: #fff5f5;
617
+ }
618
+ /* 한 번도 입력된 적 없는 metric 컬럼 — 헤더 강조 */
619
+ th.pending {
620
+ background: #fdecec;
621
+ color: #c0392b;
622
+ }
623
+
624
+ .button-line {
625
+ display: flex;
626
+ justify-content: center;
627
+ gap: 10px;
628
+ margin-top: 16px;
629
+ }
630
+ .ghost-btn {
631
+ display: inline-flex;
632
+ align-items: center;
633
+ gap: 6px;
634
+ padding: 6px 10px;
635
+ background: #35618e;
636
+ color: #ffffff;
637
+ border-radius: 5px;
638
+ cursor: pointer;
639
+ }
640
+ .ghost-btn.secondary {
641
+ background: #24be7b;
642
+ }
643
+ .ghost-btn.disabled {
644
+ opacity: 0.45;
645
+ cursor: not-allowed;
646
+ }
647
+
648
+ .delete-btn {
649
+ cursor: pointer;
650
+ color: #999;
651
+ font-size: 18px;
652
+ }
653
+ .delete-btn:hover {
654
+ color: #e74c3c;
655
+ }
656
+
657
+ .empty-msg {
658
+ text-align: center;
659
+ color: #999;
660
+ padding: 40px 0;
661
+ font-size: 14px;
662
+ }
663
+ `
664
+ ];
665
+ __decorate([
666
+ property({ type: Object }),
667
+ __metadata("design:type", Object)
668
+ ], SvProjectCompleteTab4Monthly.prototype, "project", void 0);
669
+ __decorate([
670
+ state(),
671
+ __metadata("design:type", Array)
672
+ ], SvProjectCompleteTab4Monthly.prototype, "monthlyMetrics", void 0);
673
+ __decorate([
674
+ state(),
675
+ __metadata("design:type", Array)
676
+ ], SvProjectCompleteTab4Monthly.prototype, "monthRows", void 0);
677
+ __decorate([
678
+ state(),
679
+ __metadata("design:type", Number)
680
+ ], SvProjectCompleteTab4Monthly.prototype, "addYear", void 0);
681
+ __decorate([
682
+ state(),
683
+ __metadata("design:type", Number)
684
+ ], SvProjectCompleteTab4Monthly.prototype, "addMonth", void 0);
685
+ __decorate([
686
+ state(),
687
+ __metadata("design:type", Object)
688
+ ], SvProjectCompleteTab4Monthly.prototype, "canSave", void 0);
689
+ SvProjectCompleteTab4Monthly = __decorate([
690
+ customElement('sv-pc-tab4-monthly')
691
+ ], SvProjectCompleteTab4Monthly);
692
+ export { SvProjectCompleteTab4Monthly };
693
+ //# sourceMappingURL=pc-tab4-monthly.js.map