@things-factory/worklist 10.1.35 → 10.1.37

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 (37) hide show
  1. package/dist-client/index.d.ts +1 -0
  2. package/dist-client/index.js +1 -0
  3. package/dist-client/index.js.map +1 -1
  4. package/dist-client/pages/activity-catalog/activity-catalog-page.js +50 -40
  5. package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
  6. package/dist-client/pages/activity-catalog/activity-detail-actions.d.ts +34 -0
  7. package/dist-client/pages/activity-catalog/activity-detail-actions.js +18 -0
  8. package/dist-client/pages/activity-catalog/activity-detail-actions.js.map +1 -0
  9. package/dist-client/pages/activity-catalog/activity-detail-page.d.ts +47 -0
  10. package/dist-client/pages/activity-catalog/activity-detail-page.js +430 -0
  11. package/dist-client/pages/activity-catalog/activity-detail-page.js.map +1 -0
  12. package/dist-client/pages/activity-catalog/activity-guide.d.ts +16 -0
  13. package/dist-client/pages/activity-catalog/activity-guide.js +5 -0
  14. package/dist-client/pages/activity-catalog/activity-guide.js.map +1 -0
  15. package/dist-client/pages/activity-catalog/activity-panels.d.ts +52 -0
  16. package/dist-client/pages/activity-catalog/activity-panels.js +34 -0
  17. package/dist-client/pages/activity-catalog/activity-panels.js.map +1 -0
  18. package/dist-client/pages/activity-catalog/activity-stage.d.ts +31 -0
  19. package/dist-client/pages/activity-catalog/activity-stage.js +44 -0
  20. package/dist-client/pages/activity-catalog/activity-stage.js.map +1 -0
  21. package/dist-client/route.d.ts +1 -1
  22. package/dist-client/route.js +3 -0
  23. package/dist-client/route.js.map +1 -1
  24. package/dist-client/tsconfig.tsbuildinfo +1 -1
  25. package/dist-server/service/activity-instance/activity-instance-query.js +21 -7
  26. package/dist-server/service/activity-instance/activity-instance-query.js.map +1 -1
  27. package/dist-server/service/activity-instance/draft-list-states.d.ts +39 -0
  28. package/dist-server/service/activity-instance/draft-list-states.js +51 -0
  29. package/dist-server/service/activity-instance/draft-list-states.js.map +1 -0
  30. package/dist-server/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +10 -10
  32. package/things-factory.config.js +1 -0
  33. package/translations/en.json +17 -1
  34. package/translations/ja.json +18 -2
  35. package/translations/ko.json +18 -2
  36. package/translations/ms.json +17 -1
  37. package/translations/zh.json +18 -2
@@ -0,0 +1,430 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/list-state/ox-page-state.js';
4
+ import '@things-factory/organization';
5
+ import gql from 'graphql-tag';
6
+ import { css, html } from 'lit';
7
+ import { customElement, state } from 'lit/decorators.js';
8
+ import { client } from '@operato/graphql';
9
+ import { i18next, localize } from '@operato/i18n';
10
+ import { openPopup } from '@operato/layout';
11
+ import { navigate, PageView } from '@operato/shell';
12
+ import { DocumentPageStyles, ScrollbarStyles } from '@operato/styles';
13
+ import { ranMutation } from '../../components/say-outcome.js';
14
+ import { activityPanels } from './activity-panels.js';
15
+ import { detailActions } from './activity-detail-actions.js';
16
+ import { canonicalStage } from './activity-stage.js';
17
+ import { guideLines } from './activity-guide.js';
18
+ /**
19
+ * 업무 하나 — **그 업무에 관한 일을 여기서 끝낸다.**
20
+ *
21
+ * ── 왜 이 화면이 생겼나 (2026-09-23) ───────────────────────────────────────
22
+ * 결재선 하나를 세워서 문이 서는지 보려면 화면을 셋 열어야 했고, 역할에 사람이 없으면 넷에서
23
+ * 다섯이었다 [확인함]. 카탈로그는 카드에 「결재선 없음」이라고 적어 놓고 **어디로 가야 하는지는
24
+ * 말하지 않았다** — 그 파일 전체에 `navigate()` 도 `<a href>` 도 없었다. 사람은 메뉴로 돌아가
25
+ * 이름으로 찾아야 했다.
26
+ *
27
+ * 그래서 카탈로그는 고르는 자리로 두고, 고른 뒤의 일은 이 화면 안에서 끝낸다.
28
+ *
29
+ * ── 돌아갈 길을 화면이 갖는다 ─────────────────────────────────────────────
30
+ * 탭 줄에 카탈로그가 남아 있으니 됐다고 볼 수 없다. 탭 줄은 **없을 수 있다** — 페이지가
31
+ * `mdibar: false` 를 적으면 셸이 걷고(`apptool-ui/client/layout/mdibar-visibility.ts`), 무엇보다
32
+ * **주소를 받아서 바로 여는 사람에게는 이전 화면이 아예 없다.** 「이 업무 결재선 좀 봐 주세요」로
33
+ * 링크를 받은 사람이 그 경우다. 그래서 머리에 돌아갈 자리를 직접 둔다.
34
+ *
35
+ * ── 못 하는 일은 까닭을 말한다 ────────────────────────────────────────────
36
+ * 눌리지 않는 단추만 남기지 않는다. 무엇을 할 수 있는지와 못 하면 왜인지는
37
+ * `activity-detail-actions.ts` 가 값으로 답하고, 이 화면은 그 값을 문장으로 바꾼다. 원인을 여기서
38
+ * 지어내지 않는다.
39
+ */
40
+ const DETAIL = gql `
41
+ query ($name: String!) {
42
+ responses: installableActivities {
43
+ items {
44
+ name
45
+ label
46
+ labelTKey
47
+ description
48
+ guide
49
+ guideTKeys
50
+ approvalSteps
51
+ activity {
52
+ id
53
+ }
54
+ readiness {
55
+ stage
56
+ }
57
+ }
58
+ }
59
+ activityByName(name: $name) {
60
+ id
61
+ name
62
+ description
63
+ approvalLine {
64
+ type
65
+ value
66
+ participation
67
+ approver {
68
+ id
69
+ name
70
+ description
71
+ controlNo
72
+ }
73
+ }
74
+ }
75
+ }
76
+ `;
77
+ /** 결재선에서 **서명하는 단계만** 센다. 참조는 알기만 하고 막지 않는다. */
78
+ function approvalSteps(line) {
79
+ return (line || []).filter(one => String(one?.participation || '') !== 'reference').length;
80
+ }
81
+ let ActivityDetailPage = class ActivityDetailPage extends localize(i18next)(PageView) {
82
+ constructor() {
83
+ super(...arguments);
84
+ this.name = '';
85
+ this.entry = null;
86
+ this.activity = null;
87
+ this.loaded = false;
88
+ /** 왜 못 읽었나 — ox-page-state 가 그것을 말하고 다시 시도를 준다. */
89
+ this.loadError = null;
90
+ }
91
+ static { this.styles = [
92
+ ScrollbarStyles,
93
+ DocumentPageStyles,
94
+ css `
95
+ :host {
96
+ display: flex;
97
+ flex-direction: column;
98
+ overflow: auto;
99
+ }
100
+
101
+ /* 돌아갈 자리. 제목 위에 둔다 — 읽기 전에 보인다. */
102
+ a[back] {
103
+ display: inline-flex;
104
+ align-items: center;
105
+ gap: 2px;
106
+ align-self: flex-start;
107
+ margin-bottom: var(--spacing-small);
108
+ color: var(--md-sys-color-on-surface-variant);
109
+ font-size: var(--ui-t-body, 13px);
110
+ text-decoration: none;
111
+ cursor: pointer;
112
+ }
113
+
114
+ a[back]:hover {
115
+ color: var(--md-sys-color-on-surface);
116
+ text-decoration: underline;
117
+ }
118
+
119
+ a[back] md-icon {
120
+ --md-icon-size: 16px;
121
+ }
122
+
123
+ .head {
124
+ display: flex;
125
+ align-items: center;
126
+ flex-wrap: wrap;
127
+ gap: var(--spacing-small);
128
+ }
129
+
130
+ .head .name {
131
+ font: var(--ui-t-title-font, bold 18px var(--theme-font));
132
+ color: var(--md-sys-color-on-surface);
133
+ }
134
+
135
+ .stage,
136
+ .use {
137
+ padding: 1px 8px;
138
+ border-radius: var(--md-sys-shape-corner-full);
139
+ font-size: var(--ui-t-small, 12px);
140
+ background-color: var(--md-sys-color-surface-container-high);
141
+ color: var(--md-sys-color-on-surface-variant);
142
+ }
143
+
144
+ .use:not([idle]) {
145
+ background-color: var(--md-sys-color-secondary-container);
146
+ color: var(--md-sys-color-on-secondary-container);
147
+ }
148
+
149
+ section {
150
+ margin-top: var(--spacing-large, 16px);
151
+ }
152
+
153
+ section > h3 {
154
+ margin: 0 0 var(--spacing-small) 0;
155
+ font-size: var(--ui-t-small, 12px);
156
+ color: var(--md-sys-color-on-surface-variant);
157
+ text-transform: uppercase;
158
+ }
159
+
160
+ .guide p {
161
+ margin: 0 0 4px 0;
162
+ line-height: 1.5;
163
+ }
164
+
165
+ /* 못 하는 까닭 · 비어 있다는 사실. 둘 다 **문장으로** 남는다. */
166
+ .why {
167
+ margin: 4px 0 0 0;
168
+ color: var(--md-sys-color-on-surface-variant);
169
+ font-size: var(--ui-t-body, 13px);
170
+ }
171
+
172
+ .steps {
173
+ display: flex;
174
+ flex-wrap: wrap;
175
+ gap: 4px;
176
+ margin-bottom: var(--spacing-small);
177
+ }
178
+
179
+ .steps span {
180
+ padding: 2px 8px;
181
+ border: 1px solid var(--md-sys-color-outline-variant);
182
+ border-radius: var(--md-sys-shape-corner-full);
183
+ font-size: var(--ui-t-small, 12px);
184
+ }
185
+
186
+ button {
187
+ font: inherit;
188
+ color: var(--md-sys-color-on-surface);
189
+ background-color: var(--md-sys-color-surface-container);
190
+ border: 1px solid var(--md-sys-color-outline-variant);
191
+ border-radius: var(--md-sys-shape-corner-full);
192
+ padding: 6px 14px;
193
+ cursor: pointer;
194
+ }
195
+
196
+ button:hover:not(:disabled) {
197
+ background-color: var(--md-sys-color-surface-container-high);
198
+ }
199
+
200
+ button:disabled {
201
+ opacity: 0.4;
202
+ cursor: default;
203
+ }
204
+ `
205
+ ]; }
206
+ get context() {
207
+ return {
208
+ frame: 'document',
209
+ title: this.entry ? this.shownName(this.entry) : i18next.t('title.activity-detail'),
210
+ help: 'worklist/activity-catalog-page'
211
+ };
212
+ }
213
+ /**
214
+ * 이름이 주소로 온다. `pageUpdated` 는 같은 라우트 안에서 이름만 바뀌어도 다시 오므로, **이름이
215
+ * 실제로 바뀌었을 때만** 다시 읽는다.
216
+ */
217
+ async pageUpdated(changes, lifecycle) {
218
+ if (!this.active)
219
+ return;
220
+ const name = String(lifecycle?.params?.name || '');
221
+ if (name && name !== this.name) {
222
+ this.name = name;
223
+ this.fetch();
224
+ }
225
+ }
226
+ shownName(one) {
227
+ if (!one?.label)
228
+ return String(one?.name ?? '');
229
+ return one.labelTKey ? String(i18next.t(one.labelTKey, { defaultValue: one.label })) : String(one.label);
230
+ }
231
+ async fetch() {
232
+ this.loaded = false;
233
+ this.loadError = null;
234
+ try {
235
+ const response = await client.query({ query: DETAIL, variables: { name: this.name }, fetchPolicy: 'no-cache' });
236
+ /*
237
+ * 목록에 필터가 없어서 전부 받아 하나를 고른다. 업무는 설치된 모듈이 선언하는 것이라 수가
238
+ * 닫혀 있다 [확인함 2026-09-23: 마흔 안쪽]. 그 전제가 깨지면 서버에 이름으로 찾는 갈래를
239
+ * 두는 것이 맞다 — 여기서 자르지 않는다.
240
+ */
241
+ const items = response?.data?.responses?.items || [];
242
+ this.entry = items.find((one) => one.name === this.name) || null;
243
+ this.activity = response?.data?.activityByName || null;
244
+ this.loaded = true;
245
+ }
246
+ catch (thrown) {
247
+ this.loadError = thrown;
248
+ this.loaded = true;
249
+ }
250
+ }
251
+ render() {
252
+ return html `
253
+ <a back @click=${() => navigate('activity-catalog')}>
254
+ <md-icon>chevron_left</md-icon>${i18next.t('button.back-to-activity-catalog')}
255
+ </a>
256
+
257
+ <ox-page-state .loading=${!this.loaded} .error=${this.loadError} @retry=${() => this.fetch()}
258
+ >${this.loaded && !this.loadError ? this.renderDetail() : ''}</ox-page-state
259
+ >
260
+ `;
261
+ }
262
+ renderDetail() {
263
+ if (!this.entry) {
264
+ /* 없는 이름으로 들어온 주소. 빈 화면으로 두지 않는다 — 무엇을 못 찾았는지 이름을 댄다. */
265
+ return html `<div class="why">${i18next.t('text.activity-not-found', { name: this.name })}</div>`;
266
+ }
267
+ const actions = detailActions(this.entry);
268
+ const stage = canonicalStage(this.entry.readiness?.stage);
269
+ return html `
270
+ <div class="head">
271
+ <span class="name">${this.shownName(this.entry)}</span>
272
+ ${stage ? html `<span class="stage">${i18next.t('label.readiness-' + stage)}</span>` : ''}
273
+ ${actions.registered
274
+ ? html `<span class="use" ?idle=${!actions.inUse}
275
+ >${actions.inUse
276
+ ? i18next.t('text.activity-in-use-here')
277
+ : i18next.t('text.activity-not-in-use-here')}</span
278
+ >`
279
+ : ''}
280
+ </div>
281
+
282
+ ${this.renderGuide()} ${this.renderUse(actions)} ${this.renderApprovalLine(actions)} ${this.renderPanels()}
283
+ `;
284
+ }
285
+ /** 이 업무가 무엇인가. `guide` 가 그 자리이고, 없으면 설명이다. */
286
+ renderGuide() {
287
+ const shown = guideLines(this.entry, (key, options) => i18next.t(key, options)).filter(Boolean);
288
+ if (!shown.length && !this.entry.description)
289
+ return html ``;
290
+ return html `
291
+ <section class="guide">
292
+ <h3>${i18next.t('label.activity-what-is-it')}</h3>
293
+ ${shown.length
294
+ ? shown.map(line => html `<p>${line}</p>`)
295
+ : html `<p>${this.entry.description}</p>`}
296
+ </section>
297
+ `;
298
+ }
299
+ renderUse(actions) {
300
+ if (!actions.registered)
301
+ return html ``;
302
+ return html `
303
+ <section>
304
+ <h3>${i18next.t('label.activity-use-here')}</h3>
305
+ ${actions.canInstall
306
+ ? html `<button @click=${() => this.setUse(true)}>${i18next.t('button.activity-install')}</button>`
307
+ : html `<button @click=${() => this.setUse(false)}>${i18next.t('button.activity-uninstall')}</button>`}
308
+ </section>
309
+ `;
310
+ }
311
+ renderApprovalLine(actions) {
312
+ const line = this.activity?.approvalLine || [];
313
+ const steps = approvalSteps(line);
314
+ return html `
315
+ <section>
316
+ <h3>${i18next.t('label.activity-approval-line')}</h3>
317
+
318
+ ${steps
319
+ ? html `<div class="steps">
320
+ ${line.map((one) => html `<span>${one.approver?.name || one.value || ''}</span>`)}
321
+ </div>`
322
+ : ''}
323
+
324
+ <button ?disabled=${!actions.canSetApprovalLine} @click=${() => this.openApprovalLine(line)}>
325
+ ${i18next.t('button.activity-set-approval-line')}
326
+ </button>
327
+
328
+ <p class="why">
329
+ ${actions.approvalLineBlock
330
+ ? i18next.t('text.activity-line-' + actions.approvalLineBlock)
331
+ : steps
332
+ ? i18next.t('text.activity-line-steps', { steps })
333
+ : i18next.t('text.activity-line-none')}
334
+ </p>
335
+ </section>
336
+ `;
337
+ }
338
+ /**
339
+ * 다른 꾸러미가 건 구획.
340
+ *
341
+ * **하나도 안 걸렸으면 그 사실을 말한다.** 빈자리로 두면 「이 앱에는 그 구획이 없다」와
342
+ * 「정할 것이 없다」가 같은 화면이 된다.
343
+ */
344
+ renderPanels() {
345
+ const context = { activityName: this.entry.name, activityId: this.activity?.id };
346
+ const panels = activityPanels(context);
347
+ return html `
348
+ <section>
349
+ <h3>${i18next.t('label.activity-panels')}</h3>
350
+ ${panels.length
351
+ ? panels.map(panel => html `<div>${panel.render(context)}</div>`)
352
+ : html `<p class="why">${i18next.t('text.activity-no-panels')}</p>`}
353
+ </section>
354
+ `;
355
+ }
356
+ async setUse(on) {
357
+ const outcome = await ranMutation(() => client.mutate({
358
+ mutation: on
359
+ ? gql `
360
+ mutation ($name: String!) {
361
+ activateInstallableActivity(name: $name) {
362
+ name
363
+ }
364
+ }
365
+ `
366
+ : gql `
367
+ mutation ($name: String!) {
368
+ deactivateInstallableActivity(name: $name) {
369
+ name
370
+ }
371
+ }
372
+ `,
373
+ variables: { name: this.entry.name }
374
+ }));
375
+ if (!outcome.ok)
376
+ return;
377
+ this.fetch();
378
+ }
379
+ /**
380
+ * 결재선 편집기는 **organization 것을 그대로 부른다.** 이 화면이 자기 편집기를 따로 지으면 같은
381
+ * 결재선이 두 화면에서 다르게 편집된다.
382
+ */
383
+ openApprovalLine(line) {
384
+ const popup = openPopup(html `<approval-line-items-editor-popup
385
+ .value=${line}
386
+ .confirmCallback=${(after) => this.saveApprovalLine(after || [])}
387
+ ></approval-line-items-editor-popup>`, { backdrop: true, size: 'large', title: i18next.t('title.approval-line editor') });
388
+ return popup;
389
+ }
390
+ async saveApprovalLine(after) {
391
+ const outcome = await ranMutation(() => client.mutate({
392
+ mutation: gql `
393
+ mutation ($patches: [ActivityPatch!]!) {
394
+ updateMultipleActivity(patches: $patches) {
395
+ name
396
+ }
397
+ }
398
+ `,
399
+ variables: { patches: [{ id: this.activity.id, approvalLine: after, cuFlag: 'M' }] }
400
+ }));
401
+ if (!outcome.ok)
402
+ return;
403
+ this.fetch();
404
+ }
405
+ };
406
+ __decorate([
407
+ state(),
408
+ __metadata("design:type", Object)
409
+ ], ActivityDetailPage.prototype, "name", void 0);
410
+ __decorate([
411
+ state(),
412
+ __metadata("design:type", Object)
413
+ ], ActivityDetailPage.prototype, "entry", void 0);
414
+ __decorate([
415
+ state(),
416
+ __metadata("design:type", Object)
417
+ ], ActivityDetailPage.prototype, "activity", void 0);
418
+ __decorate([
419
+ state(),
420
+ __metadata("design:type", Object)
421
+ ], ActivityDetailPage.prototype, "loaded", void 0);
422
+ __decorate([
423
+ state(),
424
+ __metadata("design:type", Object)
425
+ ], ActivityDetailPage.prototype, "loadError", void 0);
426
+ ActivityDetailPage = __decorate([
427
+ customElement('activity-detail-page')
428
+ ], ActivityDetailPage);
429
+ export { ActivityDetailPage };
430
+ //# sourceMappingURL=activity-detail-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-detail-page.js","sourceRoot":"","sources":["../../../client/pages/activity-catalog/activity-detail-page.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,sCAAsC,CAAA;AAC7C,OAAO,8BAA8B,CAAA;AAErC,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAA;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAEhD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCjB,CAAA;AAED,gDAAgD;AAChD,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,aAAa,IAAI,EAAE,CAAC,KAAK,WAAW,CAAC,CAAC,MAAM,CAAA;AAC5F,CAAC;AAGM,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;IAA5D;;QAqHY,SAAI,GAAG,EAAE,CAAA;QACT,UAAK,GAAQ,IAAI,CAAA;QACjB,aAAQ,GAAQ,IAAI,CAAA;QACpB,WAAM,GAAG,KAAK,CAAA;QAC/B,mDAAmD;QAClC,cAAS,GAAY,IAAI,CAAA;IAsO5C,CAAC;aA/VQ,WAAM,GAAG;QACd,eAAe;QACf,kBAAkB;QAClB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA8GF;KACF,AAlHY,CAkHZ;IASD,IAAI,OAAO;QACT,OAAO;YACL,KAAK,EAAE,UAAU;YACjB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;YACnF,IAAI,EAAE,gCAAgC;SACvC,CAAA;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,WAAW,CAAC,OAAY,EAAE,SAAc;QAC5C,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAM;QAExB,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;QAElD,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;YAChB,IAAI,CAAC,KAAK,EAAE,CAAA;QACd,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,GAAQ;QACxB,IAAI,CAAC,GAAG,EAAE,KAAK;YAAE,OAAO,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;QAC/C,OAAO,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAC1G,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;QAErB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,CAAA;YAE/G;;;;eAIG;YACH,MAAM,KAAK,GAAG,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,IAAI,EAAE,CAAA;YAEpD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;YACrE,IAAI,CAAC,QAAQ,GAAG,QAAQ,EAAE,IAAI,EAAE,cAAc,IAAI,IAAI,CAAA;YACtD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QACpB,CAAC;QAAC,OAAO,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,GAAG,MAAM,CAAA;YACvB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QACpB,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;uBACQ,GAAG,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC;yCAChB,OAAO,CAAC,CAAC,CAAC,iCAAiC,CAAC;;;gCAGrD,CAAC,IAAI,CAAC,MAAM,WAAW,IAAI,CAAC,SAAS,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE;WACvF,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE;;KAE/D,CAAA;IACH,CAAC;IAEO,YAAY;QAClB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,wDAAwD;YACxD,OAAO,IAAI,CAAA,oBAAoB,OAAO,CAAC,CAAC,CAAC,yBAAyB,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAA;QAClG,CAAC;QAED,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACzC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAEzD,OAAO,IAAI,CAAA;;6BAEc,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;UAC7C,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,uBAAuB,OAAO,CAAC,CAAC,CAAC,kBAAkB,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;UACtF,OAAO,CAAC,UAAU;YAClB,CAAC,CAAC,IAAI,CAAA,2BAA2B,CAAC,OAAO,CAAC,KAAK;iBACxC,OAAO,CAAC,KAAK;gBACd,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;gBACxC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;cAC9C;YACJ,CAAC,CAAC,EAAE;;;QAGN,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,YAAY,EAAE;KAC3G,CAAA;IACH,CAAC;IAED,8CAA8C;IACtC,WAAW;QACjB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAE/F,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW;YAAE,OAAO,IAAI,CAAA,EAAE,CAAA;QAE3D,OAAO,IAAI,CAAA;;cAED,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;UAC1C,KAAK,CAAC,MAAM;YACZ,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAA,MAAM,IAAI,MAAM,CAAC;YACzC,CAAC,CAAC,IAAI,CAAA,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,MAAM;;KAE7C,CAAA;IACH,CAAC;IAEO,SAAS,CAAC,OAAyC;QACzD,IAAI,CAAC,OAAO,CAAC,UAAU;YAAE,OAAO,IAAI,CAAA,EAAE,CAAA;QAEtC,OAAO,IAAI,CAAA;;cAED,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC;UACxC,OAAO,CAAC,UAAU;YAClB,CAAC,CAAC,IAAI,CAAA,kBAAkB,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,WAAW;YAClG,CAAC,CAAC,IAAI,CAAA,kBAAkB,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC,WAAW;;KAE1G,CAAA;IACH,CAAC;IAEO,kBAAkB,CAAC,OAAyC;QAClE,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,YAAY,IAAI,EAAE,CAAA;QAC9C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;QAEjC,OAAO,IAAI,CAAA;;cAED,OAAO,CAAC,CAAC,CAAC,8BAA8B,CAAC;;UAE7C,KAAK;YACL,CAAC,CAAC,IAAI,CAAA;gBACA,IAAI,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,IAAI,CAAA,SAAS,GAAG,CAAC,QAAQ,EAAE,IAAI,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE,SAAS,CAAC;mBAChF;YACT,CAAC,CAAC,EAAE;;4BAEc,CAAC,OAAO,CAAC,kBAAkB,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;YACvF,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC;;;;YAI9C,OAAO,CAAC,iBAAiB;YACzB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC;YAC9D,CAAC,CAAC,KAAK;gBACL,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,CAAC;gBAClD,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC;;;KAG/C,CAAA;IACH,CAAC;IAED;;;;;OAKG;IACK,YAAY;QAClB,MAAM,OAAO,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAA;QAChF,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;QAEtC,OAAO,IAAI,CAAA;;cAED,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;UACtC,MAAM,CAAC,MAAM;YACb,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAA,QAAQ,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YAChE,CAAC,CAAC,IAAI,CAAA,kBAAkB,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,MAAM;;KAEvE,CAAA;IACH,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,EAAW;QAC9B,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,CACrC,MAAM,CAAC,MAAM,CAAC;YACZ,QAAQ,EAAE,EAAE;gBACV,CAAC,CAAC,GAAG,CAAA;;;;;;aAMF;gBACH,CAAC,CAAC,GAAG,CAAA;;;;;;aAMF;YACL,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;SACrC,CAAC,CACH,CAAA;QAED,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,OAAM;QAEvB,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAED;;;OAGG;IACK,gBAAgB,CAAC,IAAW;QAClC,MAAM,KAAK,GAAG,SAAS,CACrB,IAAI,CAAA;iBACO,IAAI;2BACM,CAAC,KAAoB,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAAE,CAAC;2CAC5C,EACrC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC,EAAE,CAClF,CAAA;QAED,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,KAAY;QACzC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,CACrC,MAAM,CAAC,MAAM,CAAC;YACZ,QAAQ,EAAE,GAAG,CAAA;;;;;;SAMZ;YACD,SAAS,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE;SACrF,CAAC,CACH,CAAA;QAED,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,OAAM;QAEvB,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;;AA1OgB;IAAhB,KAAK,EAAE;;gDAAkB;AACT;IAAhB,KAAK,EAAE;;iDAA0B;AACjB;IAAhB,KAAK,EAAE;;oDAA6B;AACpB;IAAhB,KAAK,EAAE;;kDAAuB;AAEd;IAAhB,KAAK,EAAE;;qDAAkC;AA1H/B,kBAAkB;IAD9B,aAAa,CAAC,sBAAsB,CAAC;GACzB,kBAAkB,CAgW9B","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/list-state/ox-page-state.js'\nimport '@things-factory/organization'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { navigate, PageView } from '@operato/shell'\nimport { DocumentPageStyles, ScrollbarStyles } from '@operato/styles'\n\nimport { ranMutation } from '../../components/say-outcome.js'\nimport { activityPanels } from './activity-panels.js'\nimport { detailActions } from './activity-detail-actions.js'\nimport { canonicalStage } from './activity-stage.js'\nimport { guideLines } from './activity-guide.js'\n\n/**\n * 업무 하나 — **그 업무에 관한 일을 여기서 끝낸다.**\n *\n * ── 왜 이 화면이 생겼나 (2026-09-23) ───────────────────────────────────────\n * 결재선 하나를 세워서 문이 서는지 보려면 화면을 셋 열어야 했고, 역할에 사람이 없으면 넷에서\n * 다섯이었다 [확인함]. 카탈로그는 카드에 「결재선 없음」이라고 적어 놓고 **어디로 가야 하는지는\n * 말하지 않았다** — 그 파일 전체에 `navigate()` 도 `<a href>` 도 없었다. 사람은 메뉴로 돌아가\n * 이름으로 찾아야 했다.\n *\n * 그래서 카탈로그는 고르는 자리로 두고, 고른 뒤의 일은 이 화면 안에서 끝낸다.\n *\n * ── 돌아갈 길을 화면이 갖는다 ─────────────────────────────────────────────\n * 탭 줄에 카탈로그가 남아 있으니 됐다고 볼 수 없다. 탭 줄은 **없을 수 있다** — 페이지가\n * `mdibar: false` 를 적으면 셸이 걷고(`apptool-ui/client/layout/mdibar-visibility.ts`), 무엇보다\n * **주소를 받아서 바로 여는 사람에게는 이전 화면이 아예 없다.** 「이 업무 결재선 좀 봐 주세요」로\n * 링크를 받은 사람이 그 경우다. 그래서 머리에 돌아갈 자리를 직접 둔다.\n *\n * ── 못 하는 일은 까닭을 말한다 ────────────────────────────────────────────\n * 눌리지 않는 단추만 남기지 않는다. 무엇을 할 수 있는지와 못 하면 왜인지는\n * `activity-detail-actions.ts` 가 값으로 답하고, 이 화면은 그 값을 문장으로 바꾼다. 원인을 여기서\n * 지어내지 않는다.\n */\nconst DETAIL = gql`\n query ($name: String!) {\n responses: installableActivities {\n items {\n name\n label\n labelTKey\n description\n guide\n guideTKeys\n approvalSteps\n activity {\n id\n }\n readiness {\n stage\n }\n }\n }\n activityByName(name: $name) {\n id\n name\n description\n approvalLine {\n type\n value\n participation\n approver {\n id\n name\n description\n controlNo\n }\n }\n }\n }\n`\n\n/** 결재선에서 **서명하는 단계만** 센다. 참조는 알기만 하고 막지 않는다. */\nfunction approvalSteps(line?: any[]): number {\n return (line || []).filter(one => String(one?.participation || '') !== 'reference').length\n}\n\n@customElement('activity-detail-page')\nexport class ActivityDetailPage extends localize(i18next)(PageView) {\n static styles = [\n ScrollbarStyles,\n DocumentPageStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: auto;\n }\n\n /* 돌아갈 자리. 제목 위에 둔다 — 읽기 전에 보인다. */\n a[back] {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n align-self: flex-start;\n margin-bottom: var(--spacing-small);\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-body, 13px);\n text-decoration: none;\n cursor: pointer;\n }\n\n a[back]:hover {\n color: var(--md-sys-color-on-surface);\n text-decoration: underline;\n }\n\n a[back] md-icon {\n --md-icon-size: 16px;\n }\n\n .head {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: var(--spacing-small);\n }\n\n .head .name {\n font: var(--ui-t-title-font, bold 18px var(--theme-font));\n color: var(--md-sys-color-on-surface);\n }\n\n .stage,\n .use {\n padding: 1px 8px;\n border-radius: var(--md-sys-shape-corner-full);\n font-size: var(--ui-t-small, 12px);\n background-color: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n .use:not([idle]) {\n background-color: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n\n section {\n margin-top: var(--spacing-large, 16px);\n }\n\n section > h3 {\n margin: 0 0 var(--spacing-small) 0;\n font-size: var(--ui-t-small, 12px);\n color: var(--md-sys-color-on-surface-variant);\n text-transform: uppercase;\n }\n\n .guide p {\n margin: 0 0 4px 0;\n line-height: 1.5;\n }\n\n /* 못 하는 까닭 · 비어 있다는 사실. 둘 다 **문장으로** 남는다. */\n .why {\n margin: 4px 0 0 0;\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-body, 13px);\n }\n\n .steps {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-bottom: var(--spacing-small);\n }\n\n .steps span {\n padding: 2px 8px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: var(--md-sys-shape-corner-full);\n font-size: var(--ui-t-small, 12px);\n }\n\n button {\n font: inherit;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: var(--md-sys-shape-corner-full);\n padding: 6px 14px;\n cursor: pointer;\n }\n\n button:hover:not(:disabled) {\n background-color: var(--md-sys-color-surface-container-high);\n }\n\n button:disabled {\n opacity: 0.4;\n cursor: default;\n }\n `\n ]\n\n @state() private name = ''\n @state() private entry: any = null\n @state() private activity: any = null\n @state() private loaded = false\n /** 왜 못 읽었나 — ox-page-state 가 그것을 말하고 다시 시도를 준다. */\n @state() private loadError: unknown = null\n\n get context() {\n return {\n frame: 'document',\n title: this.entry ? this.shownName(this.entry) : i18next.t('title.activity-detail'),\n help: 'worklist/activity-catalog-page'\n }\n }\n\n /**\n * 이름이 주소로 온다. `pageUpdated` 는 같은 라우트 안에서 이름만 바뀌어도 다시 오므로, **이름이\n * 실제로 바뀌었을 때만** 다시 읽는다.\n */\n async pageUpdated(changes: any, lifecycle: any) {\n if (!this.active) return\n\n const name = String(lifecycle?.params?.name || '')\n\n if (name && name !== this.name) {\n this.name = name\n this.fetch()\n }\n }\n\n private shownName(one: any): string {\n if (!one?.label) return String(one?.name ?? '')\n return one.labelTKey ? String(i18next.t(one.labelTKey, { defaultValue: one.label })) : String(one.label)\n }\n\n private async fetch() {\n this.loaded = false\n this.loadError = null\n\n try {\n const response = await client.query({ query: DETAIL, variables: { name: this.name }, fetchPolicy: 'no-cache' })\n\n /*\n * 목록에 필터가 없어서 전부 받아 하나를 고른다. 업무는 설치된 모듈이 선언하는 것이라 수가\n * 닫혀 있다 [확인함 2026-09-23: 마흔 안쪽]. 그 전제가 깨지면 서버에 이름으로 찾는 갈래를\n * 두는 것이 맞다 — 여기서 자르지 않는다.\n */\n const items = response?.data?.responses?.items || []\n\n this.entry = items.find((one: any) => one.name === this.name) || null\n this.activity = response?.data?.activityByName || null\n this.loaded = true\n } catch (thrown) {\n this.loadError = thrown\n this.loaded = true\n }\n }\n\n render() {\n return html`\n <a back @click=${() => navigate('activity-catalog')}>\n <md-icon>chevron_left</md-icon>${i18next.t('button.back-to-activity-catalog')}\n </a>\n\n <ox-page-state .loading=${!this.loaded} .error=${this.loadError} @retry=${() => this.fetch()}\n >${this.loaded && !this.loadError ? this.renderDetail() : ''}</ox-page-state\n >\n `\n }\n\n private renderDetail() {\n if (!this.entry) {\n /* 없는 이름으로 들어온 주소. 빈 화면으로 두지 않는다 — 무엇을 못 찾았는지 이름을 댄다. */\n return html`<div class=\"why\">${i18next.t('text.activity-not-found', { name: this.name })}</div>`\n }\n\n const actions = detailActions(this.entry)\n const stage = canonicalStage(this.entry.readiness?.stage)\n\n return html`\n <div class=\"head\">\n <span class=\"name\">${this.shownName(this.entry)}</span>\n ${stage ? html`<span class=\"stage\">${i18next.t('label.readiness-' + stage)}</span>` : ''}\n ${actions.registered\n ? html`<span class=\"use\" ?idle=${!actions.inUse}\n >${actions.inUse\n ? i18next.t('text.activity-in-use-here')\n : i18next.t('text.activity-not-in-use-here')}</span\n >`\n : ''}\n </div>\n\n ${this.renderGuide()} ${this.renderUse(actions)} ${this.renderApprovalLine(actions)} ${this.renderPanels()}\n `\n }\n\n /** 이 업무가 무엇인가. `guide` 가 그 자리이고, 없으면 설명이다. */\n private renderGuide() {\n const shown = guideLines(this.entry, (key, options) => i18next.t(key, options)).filter(Boolean)\n\n if (!shown.length && !this.entry.description) return html``\n\n return html`\n <section class=\"guide\">\n <h3>${i18next.t('label.activity-what-is-it')}</h3>\n ${shown.length\n ? shown.map(line => html`<p>${line}</p>`)\n : html`<p>${this.entry.description}</p>`}\n </section>\n `\n }\n\n private renderUse(actions: ReturnType<typeof detailActions>) {\n if (!actions.registered) return html``\n\n return html`\n <section>\n <h3>${i18next.t('label.activity-use-here')}</h3>\n ${actions.canInstall\n ? html`<button @click=${() => this.setUse(true)}>${i18next.t('button.activity-install')}</button>`\n : html`<button @click=${() => this.setUse(false)}>${i18next.t('button.activity-uninstall')}</button>`}\n </section>\n `\n }\n\n private renderApprovalLine(actions: ReturnType<typeof detailActions>) {\n const line = this.activity?.approvalLine || []\n const steps = approvalSteps(line)\n\n return html`\n <section>\n <h3>${i18next.t('label.activity-approval-line')}</h3>\n\n ${steps\n ? html`<div class=\"steps\">\n ${line.map((one: any) => html`<span>${one.approver?.name || one.value || ''}</span>`)}\n </div>`\n : ''}\n\n <button ?disabled=${!actions.canSetApprovalLine} @click=${() => this.openApprovalLine(line)}>\n ${i18next.t('button.activity-set-approval-line')}\n </button>\n\n <p class=\"why\">\n ${actions.approvalLineBlock\n ? i18next.t('text.activity-line-' + actions.approvalLineBlock)\n : steps\n ? i18next.t('text.activity-line-steps', { steps })\n : i18next.t('text.activity-line-none')}\n </p>\n </section>\n `\n }\n\n /**\n * 다른 꾸러미가 건 구획.\n *\n * **하나도 안 걸렸으면 그 사실을 말한다.** 빈자리로 두면 「이 앱에는 그 구획이 없다」와\n * 「정할 것이 없다」가 같은 화면이 된다.\n */\n private renderPanels() {\n const context = { activityName: this.entry.name, activityId: this.activity?.id }\n const panels = activityPanels(context)\n\n return html`\n <section>\n <h3>${i18next.t('label.activity-panels')}</h3>\n ${panels.length\n ? panels.map(panel => html`<div>${panel.render(context)}</div>`)\n : html`<p class=\"why\">${i18next.t('text.activity-no-panels')}</p>`}\n </section>\n `\n }\n\n private async setUse(on: boolean) {\n const outcome = await ranMutation(() =>\n client.mutate({\n mutation: on\n ? gql`\n mutation ($name: String!) {\n activateInstallableActivity(name: $name) {\n name\n }\n }\n `\n : gql`\n mutation ($name: String!) {\n deactivateInstallableActivity(name: $name) {\n name\n }\n }\n `,\n variables: { name: this.entry.name }\n })\n )\n\n if (!outcome.ok) return\n\n this.fetch()\n }\n\n /**\n * 결재선 편집기는 **organization 것을 그대로 부른다.** 이 화면이 자기 편집기를 따로 지으면 같은\n * 결재선이 두 화면에서 다르게 편집된다.\n */\n private openApprovalLine(line: any[]) {\n const popup = openPopup(\n html`<approval-line-items-editor-popup\n .value=${line}\n .confirmCallback=${(after?: any[] | null) => this.saveApprovalLine(after || [])}\n ></approval-line-items-editor-popup>`,\n { backdrop: true, size: 'large', title: i18next.t('title.approval-line editor') }\n )\n\n return popup\n }\n\n private async saveApprovalLine(after: any[]) {\n const outcome = await ranMutation(() =>\n client.mutate({\n mutation: gql`\n mutation ($patches: [ActivityPatch!]!) {\n updateMultipleActivity(patches: $patches) {\n name\n }\n }\n `,\n variables: { patches: [{ id: this.activity.id, approvalLine: after, cuFlag: 'M' }] }\n })\n )\n\n if (!outcome.ok) return\n\n this.fetch()\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 이 업무가 무엇인가 — 카드와 상세가 **같은 문장**을 그리게 한다.
3
+ *
4
+ * `guide` 가 원문이고 `guideTKeys` 는 서버가 줄마다 계산해 보낸 번역 열쇠다. 줄 수를 정하는 것은
5
+ * **원문 쪽**이다. 열쇠 쪽으로 세면 열쇠가 하나 더 오거나 덜 오는 판에서 줄이 늘거나 사라지고,
6
+ * 그것이 오류로는 안 나온다.
7
+ *
8
+ * 번역 함수를 받아서 쓴다 — 이 판단을 i18next 없이 시험할 수 있게 하려는 것이다.
9
+ */
10
+ export type Translate = (key: string, options: {
11
+ defaultValue: string;
12
+ }) => string;
13
+ export declare function guideLines(entry: {
14
+ guide?: string[] | null;
15
+ guideTKeys?: string[] | null;
16
+ } | null | undefined, translate: Translate): string[];
@@ -0,0 +1,5 @@
1
+ export function guideLines(entry, translate) {
2
+ const keys = entry?.guideTKeys || [];
3
+ return (entry?.guide || []).map((line, at) => (keys[at] ? String(translate(keys[at], { defaultValue: line })) : line));
4
+ }
5
+ //# sourceMappingURL=activity-guide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-guide.js","sourceRoot":"","sources":["../../../client/pages/activity-catalog/activity-guide.ts"],"names":[],"mappings":"AAWA,MAAM,UAAU,UAAU,CACxB,KAAmF,EACnF,SAAoB;IAEpB,MAAM,IAAI,GAAG,KAAK,EAAE,UAAU,IAAI,EAAE,CAAA;IAEpC,OAAO,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;AACxH,CAAC","sourcesContent":["/**\n * 이 업무가 무엇인가 — 카드와 상세가 **같은 문장**을 그리게 한다.\n *\n * `guide` 가 원문이고 `guideTKeys` 는 서버가 줄마다 계산해 보낸 번역 열쇠다. 줄 수를 정하는 것은\n * **원문 쪽**이다. 열쇠 쪽으로 세면 열쇠가 하나 더 오거나 덜 오는 판에서 줄이 늘거나 사라지고,\n * 그것이 오류로는 안 나온다.\n *\n * 번역 함수를 받아서 쓴다 — 이 판단을 i18next 없이 시험할 수 있게 하려는 것이다.\n */\nexport type Translate = (key: string, options: { defaultValue: string }) => string\n\nexport function guideLines(\n entry: { guide?: string[] | null; guideTKeys?: string[] | null } | null | undefined,\n translate: Translate\n): string[] {\n const keys = entry?.guideTKeys || []\n\n return (entry?.guide || []).map((line, at) => (keys[at] ? String(translate(keys[at], { defaultValue: line })) : line))\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import type { TemplateResult } from 'lit';
2
+ /**
3
+ * 업무 상세에 다른 꾸러미가 구획을 거는 문.
4
+ *
5
+ * ── 왜 문이 필요한가 ───────────────────────────────────────────────────────
6
+ * 업무 상세는 「이 업무가 어떻게 처리되나」를 한 자리에서 끝내는 화면이다. 그런데 그 답의 일부를
7
+ * worklist 가 모른다 — 기준정보 변경 정책은 `@things-factory/ops-master` 가 알고, 그 둘은 서로를
8
+ * 의존하지 않는다 [확인함 2026-09-23: worklist 의존에 ops-master 0건, 그 반대도 0건].
9
+ *
10
+ * 그래서 **worklist 가 문을 열고, 앱이 건다.** ops-master 는 구획 컴포넌트만 내놓고 worklist 를
11
+ * 모른다. 둘을 잇는 값을 아는 것은 둘 다 물고 있는 앱뿐이다(아키텍트 판정 2026-09-23). 서버 쪽
12
+ * 같은 이음새가 먼저 이 모양으로 섰다 — `registerApprovalRouteInspector` 는 ops-master 가 열고
13
+ * plant 가 자기 짝을 건다.
14
+ *
15
+ * ── 안 걸린 것이 오류보고없이 지나가면 안 된다 ────────────────────────────
16
+ * 구획이 하나도 안 걸린 화면과 「정할 것이 없는」 화면은 **같은 빈자리로 보인다.** 그래서 이 문은
17
+ * 걸린 것의 이름을 남기고, 화면은 그 목록이 비었을 때 비었다고 말한다. 판정의 조건 1 이다.
18
+ *
19
+ * ── 구획은 자기 자료를 자기가 읽는다 ──────────────────────────────────────
20
+ * 이 문은 **자리와 이름만** 넘긴다. 구획이 그릴 값을 worklist 가 받아다 넘기지 않는다 — 그러면 두
21
+ * 꾸러미가 같은 사실을 두 벌로 세고, 한쪽이 낡아도 알 수 없다. 판정의 조건 2 다.
22
+ */
23
+ export type ActivityPanelContext = {
24
+ /** 업무의 이름. 콜백을 찾는 키이므로 번역하지 않는다. */
25
+ activityName: string;
26
+ /** 설치된 업무만 갖는다. 없으면 이 업무는 이 도메인에 아직 설치되지 않았다. */
27
+ activityId?: string;
28
+ };
29
+ export type ActivityPanel = {
30
+ /** 이 구획의 이름. 목록에 남고 중복을 가린다. 사람에게 보이는 글자가 아니다. */
31
+ name: string;
32
+ /** 사람에게 보이는 이름은 **열쇠로 받는다.** 값으로 받으면 언어를 바꿔도 안 바뀐다. */
33
+ labelKey: string;
34
+ /** 이 업무에 이 구획이 해당하나. 없으면 늘 해당한다. */
35
+ appliesTo?: (context: ActivityPanelContext) => boolean;
36
+ render: (context: ActivityPanelContext) => TemplateResult;
37
+ };
38
+ export type PanelRejection = 'duplicate-name' | 'no-name' | 'no-label-key' | 'no-render';
39
+ /**
40
+ * 구획을 건다. 같은 이름이 두 번 오면 **먼저 온 것이 선다** — 뒤에 온 것이 앞의 것을 조용히 덮으면
41
+ * 어느 꾸러미가 그리고 있는지 알 수 없다.
42
+ */
43
+ export declare function registerActivityPanel(panel: ActivityPanel): void;
44
+ /** 걸린 구획. 건 차례 그대로다 — 앱이 적은 순서가 화면의 순서다. */
45
+ export declare function activityPanels(context?: ActivityPanelContext): ActivityPanel[];
46
+ /** 거절된 등록. 화면이 아니라 개발 중에 읽는 값이고, 빈 배열이 정상이다. */
47
+ export declare function rejectedActivityPanels(): {
48
+ name: string;
49
+ reason: PanelRejection;
50
+ }[];
51
+ /** 시험을 위한 자리. 제품 코드는 부르지 않는다. */
52
+ export declare function forgetActivityPanels(): void;
@@ -0,0 +1,34 @@
1
+ const panels = [];
2
+ const rejected = [];
3
+ /**
4
+ * 구획을 건다. 같은 이름이 두 번 오면 **먼저 온 것이 선다** — 뒤에 온 것이 앞의 것을 조용히 덮으면
5
+ * 어느 꾸러미가 그리고 있는지 알 수 없다.
6
+ */
7
+ export function registerActivityPanel(panel) {
8
+ const name = String(panel?.name ?? '').trim();
9
+ if (!name)
10
+ return void rejected.push({ name: '(no name)', reason: 'no-name' });
11
+ if (panels.some(one => one.name === name))
12
+ return void rejected.push({ name, reason: 'duplicate-name' });
13
+ if (!String(panel.labelKey ?? '').trim())
14
+ return void rejected.push({ name, reason: 'no-label-key' });
15
+ if (typeof panel.render !== 'function')
16
+ return void rejected.push({ name, reason: 'no-render' });
17
+ panels.push({ ...panel, name });
18
+ }
19
+ /** 걸린 구획. 건 차례 그대로다 — 앱이 적은 순서가 화면의 순서다. */
20
+ export function activityPanels(context) {
21
+ if (!context)
22
+ return [...panels];
23
+ return panels.filter(one => (one.appliesTo ? one.appliesTo(context) : true));
24
+ }
25
+ /** 거절된 등록. 화면이 아니라 개발 중에 읽는 값이고, 빈 배열이 정상이다. */
26
+ export function rejectedActivityPanels() {
27
+ return [...rejected];
28
+ }
29
+ /** 시험을 위한 자리. 제품 코드는 부르지 않는다. */
30
+ export function forgetActivityPanels() {
31
+ panels.length = 0;
32
+ rejected.length = 0;
33
+ }
34
+ //# sourceMappingURL=activity-panels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-panels.js","sourceRoot":"","sources":["../../../client/pages/activity-catalog/activity-panels.ts"],"names":[],"mappings":"AA0CA,MAAM,MAAM,GAAoB,EAAE,CAAA;AAClC,MAAM,QAAQ,GAA+C,EAAE,CAAA;AAE/D;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAoB;IACxD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAE7C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;IAC9E,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAA;IACxG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;IACrG,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;IAEhG,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACjC,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,cAAc,CAAC,OAA8B;IAC3D,IAAI,CAAC,OAAO;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IAChC,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,sBAAsB;IACpC,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAA;AACtB,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IACjB,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAA;AACrB,CAAC","sourcesContent":["import type { TemplateResult } from 'lit'\n\n/**\n * 업무 상세에 다른 꾸러미가 구획을 거는 문.\n *\n * ── 왜 문이 필요한가 ───────────────────────────────────────────────────────\n * 업무 상세는 「이 업무가 어떻게 처리되나」를 한 자리에서 끝내는 화면이다. 그런데 그 답의 일부를\n * worklist 가 모른다 — 기준정보 변경 정책은 `@things-factory/ops-master` 가 알고, 그 둘은 서로를\n * 의존하지 않는다 [확인함 2026-09-23: worklist 의존에 ops-master 0건, 그 반대도 0건].\n *\n * 그래서 **worklist 가 문을 열고, 앱이 건다.** ops-master 는 구획 컴포넌트만 내놓고 worklist 를\n * 모른다. 둘을 잇는 값을 아는 것은 둘 다 물고 있는 앱뿐이다(아키텍트 판정 2026-09-23). 서버 쪽\n * 같은 이음새가 먼저 이 모양으로 섰다 — `registerApprovalRouteInspector` 는 ops-master 가 열고\n * plant 가 자기 짝을 건다.\n *\n * ── 안 걸린 것이 오류보고없이 지나가면 안 된다 ────────────────────────────\n * 구획이 하나도 안 걸린 화면과 「정할 것이 없는」 화면은 **같은 빈자리로 보인다.** 그래서 이 문은\n * 걸린 것의 이름을 남기고, 화면은 그 목록이 비었을 때 비었다고 말한다. 판정의 조건 1 이다.\n *\n * ── 구획은 자기 자료를 자기가 읽는다 ──────────────────────────────────────\n * 이 문은 **자리와 이름만** 넘긴다. 구획이 그릴 값을 worklist 가 받아다 넘기지 않는다 — 그러면 두\n * 꾸러미가 같은 사실을 두 벌로 세고, 한쪽이 낡아도 알 수 없다. 판정의 조건 2 다.\n */\nexport type ActivityPanelContext = {\n /** 업무의 이름. 콜백을 찾는 키이므로 번역하지 않는다. */\n activityName: string\n /** 설치된 업무만 갖는다. 없으면 이 업무는 이 도메인에 아직 설치되지 않았다. */\n activityId?: string\n}\n\nexport type ActivityPanel = {\n /** 이 구획의 이름. 목록에 남고 중복을 가린다. 사람에게 보이는 글자가 아니다. */\n name: string\n /** 사람에게 보이는 이름은 **열쇠로 받는다.** 값으로 받으면 언어를 바꿔도 안 바뀐다. */\n labelKey: string\n /** 이 업무에 이 구획이 해당하나. 없으면 늘 해당한다. */\n appliesTo?: (context: ActivityPanelContext) => boolean\n render: (context: ActivityPanelContext) => TemplateResult\n}\n\nexport type PanelRejection = 'duplicate-name' | 'no-name' | 'no-label-key' | 'no-render'\n\nconst panels: ActivityPanel[] = []\nconst rejected: { name: string; reason: PanelRejection }[] = []\n\n/**\n * 구획을 건다. 같은 이름이 두 번 오면 **먼저 온 것이 선다** — 뒤에 온 것이 앞의 것을 조용히 덮으면\n * 어느 꾸러미가 그리고 있는지 알 수 없다.\n */\nexport function registerActivityPanel(panel: ActivityPanel): void {\n const name = String(panel?.name ?? '').trim()\n\n if (!name) return void rejected.push({ name: '(no name)', reason: 'no-name' })\n if (panels.some(one => one.name === name)) return void rejected.push({ name, reason: 'duplicate-name' })\n if (!String(panel.labelKey ?? '').trim()) return void rejected.push({ name, reason: 'no-label-key' })\n if (typeof panel.render !== 'function') return void rejected.push({ name, reason: 'no-render' })\n\n panels.push({ ...panel, name })\n}\n\n/** 걸린 구획. 건 차례 그대로다 — 앱이 적은 순서가 화면의 순서다. */\nexport function activityPanels(context?: ActivityPanelContext): ActivityPanel[] {\n if (!context) return [...panels]\n return panels.filter(one => (one.appliesTo ? one.appliesTo(context) : true))\n}\n\n/** 거절된 등록. 화면이 아니라 개발 중에 읽는 값이고, 빈 배열이 정상이다. */\nexport function rejectedActivityPanels(): { name: string; reason: PanelRejection }[] {\n return [...rejected]\n}\n\n/** 시험을 위한 자리. 제품 코드는 부르지 않는다. */\nexport function forgetActivityPanels(): void {\n panels.length = 0\n rejected.length = 0\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 업무의 단계 — 선언된 값을 읽고, 차례를 정한다.
3
+ *
4
+ * 카탈로그 목록과 업무 상세가 같은 답을 내야 해서 여기 한 벌만 둔다. 두 벌이 되면 한쪽만 고쳐도
5
+ * 오류가 안 나고, 같은 업무가 목록과 상세에서 다른 단계로 보인다.
6
+ */
7
+ /**
8
+ * 카드 차례 — **쓸 수 있는 것이 위.**
9
+ *
10
+ * 사용자는 지금 오는 업무를 보러 오고, 우리는 남은 것을 보러 온다. 두 독자를 한 화면에 두면서
11
+ * 순서로 구분한다: 위쪽은 실제로 오는 업무, 아래쪽은 아직 없는 것과 여기 두지 않기로 한 것.
12
+ *
13
+ * **「업무함 아님」은 「예정」과 붙여 두지 않는다.** 섞이면 판단이 약속으로 읽힌다 — 안 만들기로
14
+ * 정한 것이 곧 될 것처럼 보인다.
15
+ */
16
+ export declare const STAGE_ORDER: string[];
17
+ /**
18
+ * 전선에서 온 단계를 **선언된 값으로 되돌린다.**
19
+ *
20
+ * 서버의 enum 철자와 화면의 철자가 판마다 달라진 적이 있어서, 글자만 남기고 맞춘다. 못 맞추면
21
+ * 빈 문자열이다 — 모르는 것을 아무 단계로 채우지 않는다.
22
+ */
23
+ export declare function canonicalStage(raw?: string): string;
24
+ export declare function stageRank(stage?: string): number;
25
+ /**
26
+ * 이 업무가 **설치될 수 있는 것인가.**
27
+ *
28
+ * 아직 만들지 않은 것(예정 · 업무함 아님)과 선언이 없는 것은 설치라는 말이 성립하지 않는다.
29
+ * 그런 카드에 「아직 사용 안 함」을 붙이면 「예정」과 같은 말을 두 번 하는 것이다.
30
+ */
31
+ export declare function isRegistered(stage?: string): boolean;