@things-factory/worklist 10.1.185 → 10.1.188

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 (45) hide show
  1. package/dist-client/my-work-menus.d.ts +0 -5
  2. package/dist-client/my-work-menus.js +1 -2
  3. package/dist-client/my-work-menus.js.map +1 -1
  4. package/dist-client/pages/activity-catalog/activity-catalog-page.d.ts +18 -74
  5. package/dist-client/pages/activity-catalog/activity-catalog-page.js +292 -527
  6. package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
  7. package/dist-client/pages/work-door/door-links.d.ts +1 -9
  8. package/dist-client/pages/work-door/door-links.js +1 -11
  9. package/dist-client/pages/work-door/door-links.js.map +1 -1
  10. package/dist-client/pages/work-door/door-sentence.d.ts +26 -14
  11. package/dist-client/pages/work-door/door-sentence.js +58 -60
  12. package/dist-client/pages/work-door/door-sentence.js.map +1 -1
  13. package/dist-client/route.d.ts +1 -1
  14. package/dist-client/route.js +0 -3
  15. package/dist-client/route.js.map +1 -1
  16. package/dist-client/tsconfig.tsbuildinfo +1 -1
  17. package/dist-server/service/activity/activity-query.js +2 -31
  18. package/dist-server/service/activity/activity-query.js.map +1 -1
  19. package/dist-server/service/activity/approval-line-seats.d.ts +12 -0
  20. package/dist-server/service/activity/approval-line-seats.js +68 -0
  21. package/dist-server/service/activity/approval-line-seats.js.map +1 -0
  22. package/dist-server/service/activity-instance/activity-instance-query.js +2 -54
  23. package/dist-server/service/activity-instance/activity-instance-query.js.map +1 -1
  24. package/dist-server/service/work-door/work-door-resolver.js +7 -2
  25. package/dist-server/service/work-door/work-door-resolver.js.map +1 -1
  26. package/dist-server/service/work-door/work-door-service.d.ts +8 -1
  27. package/dist-server/service/work-door/work-door-service.js +11 -2
  28. package/dist-server/service/work-door/work-door-service.js.map +1 -1
  29. package/dist-server/service/work-door/work-door-type.d.ts +2 -0
  30. package/dist-server/service/work-door/work-door-type.js +8 -0
  31. package/dist-server/service/work-door/work-door-type.js.map +1 -1
  32. package/dist-server/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +11 -11
  34. package/tests/work-door-door-words.test.ts +1 -1
  35. package/things-factory.config.js +1 -6
  36. package/translations/en.json +11 -45
  37. package/translations/es.json +11 -45
  38. package/translations/ja.json +11 -45
  39. package/translations/ko.json +11 -45
  40. package/translations/ms.json +11 -45
  41. package/translations/zh.json +11 -45
  42. package/dist-client/pages/work-door/work-doors-page.d.ts +0 -26
  43. package/dist-client/pages/work-door/work-doors-page.js +0 -365
  44. package/dist-client/pages/work-door/work-doors-page.js.map +0 -1
  45. package/tests/work-door-sentence.test.ts +0 -199
@@ -1,365 +0,0 @@
1
- import { __decorate, __metadata } from "tslib";
2
- import '@operato/list-state/ox-page-state.js';
3
- import gql from 'graphql-tag';
4
- import { css, html } from 'lit';
5
- import { customElement, state } from 'lit/decorators.js';
6
- import { client } from '@operato/graphql';
7
- import { i18next, localize } from '@operato/i18n';
8
- import { PageView } from '@operato/shell';
9
- import { DocumentPageStyles } from '@operato/styles';
10
- import { detailAddress, opensDetail } from './door-links.js';
11
- import { doorLabel, doorSentence, doorsSummary, whenNoLineTag } from './door-sentence.js';
12
- import { kindWord, reasonWord, standingWord, whenNoLineBecauseWord, whenNoLineWord } from './door-words.js';
13
- /**
14
- * 결재 문 현황 — **이 공장에서 어느 승인 단계가 실제로 서 있나.**
15
- *
16
- * ═══════════════════════════════════════════════════════════════════════════
17
- * ── 왜 이 화면이 있나 ──────────────────────────────────────────────────────
18
- * 양식은 제품이 세우고 **결재선은 그 공장이 채운다.** 채운 것이 「우리는 이 단계를 쓴다」는 선언이라,
19
- * 비어 있는 것은 결함이 아니라 선택이다. 그런데 **그 선택이 어디에도 안 보였다.** 업무 카탈로그는
20
- * 「제품이 이 문을 만들었나」를 보여 주고, 이 물음에는 답하지 않는다.
21
- *
22
- * 실측(2026-09-11): 미라텍에 양식 11개가 다 서 있고 **11개 전부 결재자가 없었다.** 그 공장에서는
23
- * 실적 정정도 폐기도 승인 없이 반영되는데, 화면에는 문이 있는 것처럼 보였다.
24
- *
25
- * ── 비어 있는 것을 「승인 없음」으로 그리지 않는다 ─────────────────────────
26
- * 빈 결재선의 결과는 **문마다 선언돼 있다**(ADR-0077). `run` 문은 올린 사람이 자기 건을 승인한 것이
27
- * 되고, `refuse` 문은 올리는 것 자체가 거절된다. 서버는 그 사실을 값으로 보내고 **문장은 이 화면이
28
- * 번역으로 짓는다**(§`door-sentence.ts`) — 서버가 한국어로 지어 보내던 때는 다른 언어 사용자도 한국어를
29
- * 읽었고, 거절하는 문에도 「자기 건을 승인한다」가 적혔다.
30
- *
31
- * ── 할 일이 있는 것부터 ────────────────────────────────────────────────────
32
- * 선언 순서로 늘어놓으면 서 있는 것과 빈 것이 섞이고, 읽는 사람이 열한 줄을 훑어야 한다. **정할
33
- * 것이 남은 것을 위로** 올린다 — 이 화면을 여는 이유가 그것이다.
34
- * ═══════════════════════════════════════════════════════════════════════════
35
- */
36
- const DOORS = gql `
37
- {
38
- workDoors {
39
- todo
40
- on
41
- off
42
- missing
43
- notUsed
44
- seatsStanding
45
- seatsOffered
46
- doors {
47
- name
48
- labelKey
49
- kind
50
- standing
51
- reason
52
- approvers
53
- from
54
- whenNoLine
55
- whenNoLineBecause
56
- followsLabelKey
57
- assigneeRole
58
- notUsedBecause
59
- }
60
- }
61
- }
62
- `;
63
- /**
64
- * 조치할 것 · 서 있는 것 · 안 쓰기로 한 것 순. 같은 무리 안에서는 선언 순서를 지킨다.
65
- *
66
- * 안 쓰기로 한 것을 맨 아래 두는 이유는 **그것이 할 일이 아니어서**다. 위에 섞으면 이 화면을 여는
67
- * 이유(무엇을 채워야 하나)가 안 보인다.
68
- */
69
- const ORDER = { missing: 0, off: 1, on: 2, 'not-used': 3 };
70
- let WorkDoorsPage = class WorkDoorsPage extends localize(i18next)(PageView) {
71
- constructor() {
72
- super(...arguments);
73
- this.counts = { on: 0, off: 0, missing: 0, notUsed: 0, todo: 0 };
74
- this.todo = 0;
75
- this.seatsStanding = 0;
76
- this.seatsOffered = 0;
77
- this.loadError = null;
78
- }
79
- static { this.styles = [
80
- DocumentPageStyles,
81
- css `
82
- /*
83
- * ── One door, one row: what it is on the left, what it does on the right ──
84
- * The sentence ran on from the end of the name, so each line started at a different place and wrapped
85
- * under the name's badge ("없습니 / 다" at narrow widths). The name, its kind and its state now hold a
86
- * column of their own and the sentence wraps inside its column, at the section's width (ADR-0117).
87
- * The rows share one grid (subgrid), so every sentence starts at the same x.
88
- */
89
- .doors {
90
- display: grid;
91
- grid-template-columns: max-content minmax(0, 1fr);
92
- column-gap: var(--spacing-huge, 24px);
93
- margin: 0;
94
- padding: 0;
95
- list-style: none;
96
- }
97
-
98
- .door {
99
- grid-column: 1 / -1;
100
- display: grid;
101
- grid-template-columns: subgrid;
102
- row-gap: var(--spacing-small, 4px);
103
- padding: var(--spacing-large, 12px) 0;
104
- border-top: 1px solid var(--md-sys-color-outline-variant);
105
- }
106
-
107
- .door:first-child {
108
- border-top: none;
109
- }
110
-
111
- @media (max-width: 600px) {
112
- .doors {
113
- grid-template-columns: minmax(0, 1fr);
114
- }
115
- }
116
-
117
- .what {
118
- display: flex;
119
- flex-wrap: wrap;
120
- align-items: baseline;
121
- gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);
122
- }
123
-
124
- .name {
125
- font-weight: 600;
126
- }
127
-
128
- /* Kind in words: an empty approval line and an empty assignment line do opposite things. */
129
- .kind {
130
- color: var(--ui-muted);
131
- font-size: var(--ui-t-small, 11.5px);
132
- }
133
-
134
- /* The state says it in words as well as colour: colour alone says nothing to someone who cannot see it. */
135
- .mark {
136
- padding: 0 var(--spacing-small, 4px);
137
- border: 1px solid currentColor;
138
- border-radius: var(--md-sys-shape-corner-small, 4px);
139
- font-size: var(--ui-t-micro, 11px);
140
- font-weight: 600;
141
- line-height: 1.6;
142
- white-space: nowrap;
143
- }
144
-
145
- .mark.on {
146
- color: var(--md-sys-color-primary);
147
- }
148
-
149
- .mark.off {
150
- color: var(--md-sys-color-tertiary);
151
- }
152
-
153
- .mark.missing {
154
- color: var(--md-sys-color-error);
155
- }
156
-
157
- /* Not a task, so quieter. Not removed: that it was turned off has to be readable. */
158
- .mark.not-used {
159
- color: var(--md-sys-color-outline);
160
- }
161
-
162
- .door.not-used .what,
163
- .door.not-used .says {
164
- opacity: 0.7;
165
- }
166
-
167
- .says {
168
- display: flex;
169
- flex-direction: column;
170
- gap: 2px;
171
- min-width: 0;
172
- }
173
-
174
- /* What an empty line does here is declared per door (ADR-0077): one quieter line under the sentence. */
175
- .when,
176
- .from {
177
- color: var(--ui-muted);
178
- font-size: var(--ui-t-small, 11.5px);
179
- }
180
-
181
- .state {
182
- margin: 0 0 var(--spacing-medium, 8px);
183
- font-weight: 600;
184
- }
185
-
186
- ol.next {
187
- margin: 0;
188
- padding-left: var(--spacing-huge, 24px);
189
- display: flex;
190
- flex-direction: column;
191
- gap: var(--spacing-medium, 8px);
192
- }
193
- `
194
- ]; }
195
- get context() {
196
- return {
197
- title: i18next.t('menu.work-doors'),
198
- description: i18next.t('text.work-doors-lead'),
199
- frame: 'document'
200
- };
201
- }
202
- async pageInitialized() {
203
- await this.load();
204
- }
205
- async pageUpdated(changes, lifecycle) {
206
- if (lifecycle.active && !this.doors) {
207
- await this.load();
208
- }
209
- }
210
- async load() {
211
- this.loadError = null;
212
- let response;
213
- try {
214
- response = await client.query({ query: DOORS, fetchPolicy: 'no-cache' });
215
- }
216
- catch (error) {
217
- this.loadError = error;
218
- return;
219
- }
220
- /*
221
- * 못 읽은 것을 빈 목록으로 그리지 않는다. 「결재 문이 하나도 없습니다」와 「못 읽었습니다」는
222
- * 사람이 할 일이 다르다 — 앞은 설치를 보러 갈 일이고 뒤는 다시 눌러 볼 일이다.
223
- * ox-page-state 가 서버의 문장과 다시 읽기를 그린다(ADR-0060 결정 8).
224
- */
225
- if (response.errors?.length) {
226
- this.loadError = response.errors[0];
227
- return;
228
- }
229
- /*
230
- * 전선의 철자를 여기서 **한 번** 옮긴다(§`door-words`). 안 옮기면 i18n 키도 CSS 클래스도
231
- * 어긋나는데 **오류가 하나도 안 난다** — 키 글자가 화면에 뜨고 상태 색이 투명해질 뿐이다.
232
- */
233
- this.doors = (response.data?.workDoors?.doors ?? []).map((one) => ({
234
- ...one,
235
- standing: standingWord(one.standing) ?? one.standing,
236
- kind: kindWord(one.kind) ?? one.kind,
237
- reason: reasonWord(one.reason) ?? one.reason,
238
- whenNoLine: whenNoLineWord(one.whenNoLine),
239
- whenNoLineBecause: whenNoLineBecauseWord(one.whenNoLineBecause)
240
- }));
241
- const read = response.data?.workDoors ?? {};
242
- this.todo = read.todo ?? 0;
243
- this.counts = {
244
- on: read.on ?? 0,
245
- off: read.off ?? 0,
246
- missing: read.missing ?? 0,
247
- notUsed: read.notUsed ?? 0,
248
- todo: this.todo
249
- };
250
- this.seatsStanding = response.data?.workDoors?.seatsStanding ?? 0;
251
- this.seatsOffered = response.data?.workDoors?.seatsOffered ?? 0;
252
- }
253
- /** 결재선은 각 업무에서 지정하고, 역할이 부족할 때만 별도로 안내한다. */
254
- renderNext() {
255
- if (!this.todo || this.seatsStanding >= this.seatsOffered) {
256
- return '';
257
- }
258
- return html `
259
- <section class="page-section">
260
- <div class="page-section-header"><h3>${i18next.t('title.work-doors-next')}</h3></div>
261
- <ol class="next">
262
- <li>
263
- ${i18next.t('text.work-doors-next-seats', {
264
- standing: this.seatsStanding,
265
- offered: this.seatsOffered
266
- })}
267
- <a class="page-nav-link" href="roles">${i18next.t('field.roles')}</a>
268
- </li>
269
- </ol>
270
- </section>
271
- `;
272
- }
273
- render() {
274
- return html `<div class="page">
275
- <ox-page-state .loading=${!this.doors && !this.loadError} .error=${this.loadError} @retry=${() => this.load()}
276
- >${this.doors && !this.loadError ? this.renderDoors() : ''}</ox-page-state
277
- >
278
- </div>`;
279
- }
280
- renderDoors() {
281
- const sorted = [...this.doors].sort((a, b) => ORDER[a.standing] - ORDER[b.standing]);
282
- return html `
283
- <section class="page-section">
284
- <div class="page-section-header"><h3>${i18next.t('title.work-doors')}</h3></div>
285
- <p class="state">${doorsSummary(i18next.t, this.counts)}</p>
286
- <ul class="doors">
287
- ${sorted.map(door => this.renderDoor(door))}
288
- </ul>
289
- </section>
290
-
291
- ${this.renderNext()}
292
- `;
293
- }
294
- renderDoor(door) {
295
- const label = doorLabel(i18next.t, door);
296
- const when = whenNoLineTag(i18next.t, door);
297
- return html `
298
- <li class="door ${door.standing}">
299
- <div class="what">
300
- ${opensDetail(door)
301
- ? html `<a class="name" href=${detailAddress(door.name)}>${label}</a>`
302
- : html `<span class="name">${label}</span>`}
303
- <span class="kind">${i18next.t(`text.door-kind-${door.kind}`)}</span>
304
- <span class="mark ${door.standing}">${i18next.t(`text.door-standing-${door.standing}`)}</span>
305
- </div>
306
- <div class="says">
307
- <span>
308
- ${doorSentence(i18next.t, door)}
309
- ${door.kind === 'approval' && door.standing !== 'missing' && opensDetail(door)
310
- ? html `<a class="page-nav-link" href=${detailAddress(door.name)}
311
- >${i18next.t('button.activity-set-approval-line')}</a
312
- >`
313
- : ''}
314
- </span>
315
- ${when ? html `<span class="when">${when}</span>` : ''}
316
- ${
317
- /*
318
- * 자기 것이 아닌 양식을 쓰는 것은 정상이지만, 안 말하면 「정한 적 없는데」가 된다.
319
- *
320
- * ⚠ **차 있는 것과 비어 있는 것은 다른 말이다.** 「본사에서 정했습니다」를 결재자가 없는
321
- * 줄에 달면 한 줄이 스스로 모순된다 — 정한 것이 없는데 정했다고 적힌다. 비어 있으면
322
- * 말할 수 있는 것은 **양식의 임자**뿐이고, 그 사실은 「여기서는 못 고칩니다」를 뜻해서
323
- * 오히려 더 중요하다.
324
- */
325
- door.from
326
- ? html `<span class="from"
327
- >${door.standing === 'on'
328
- ? i18next.t('text.door-filled-from', { name: door.from })
329
- : i18next.t('text.door-form-from', { name: door.from })}</span
330
- >`
331
- : ''}
332
- </div>
333
- </li>
334
- `;
335
- }
336
- };
337
- __decorate([
338
- state(),
339
- __metadata("design:type", Array)
340
- ], WorkDoorsPage.prototype, "doors", void 0);
341
- __decorate([
342
- state(),
343
- __metadata("design:type", Object)
344
- ], WorkDoorsPage.prototype, "counts", void 0);
345
- __decorate([
346
- state(),
347
- __metadata("design:type", Object)
348
- ], WorkDoorsPage.prototype, "todo", void 0);
349
- __decorate([
350
- state(),
351
- __metadata("design:type", Object)
352
- ], WorkDoorsPage.prototype, "seatsStanding", void 0);
353
- __decorate([
354
- state(),
355
- __metadata("design:type", Object)
356
- ], WorkDoorsPage.prototype, "seatsOffered", void 0);
357
- __decorate([
358
- state(),
359
- __metadata("design:type", Object)
360
- ], WorkDoorsPage.prototype, "loadError", void 0);
361
- WorkDoorsPage = __decorate([
362
- customElement('work-doors-page')
363
- ], WorkDoorsPage);
364
- export { WorkDoorsPage };
365
- //# sourceMappingURL=work-doors-page.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"work-doors-page.js","sourceRoot":"","sources":["../../../client/pages/work-door/work-doors-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/list-state/ox-page-state.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { PageView } from '@operato/shell'\nimport { DocumentPageStyles } from '@operato/styles'\n\nimport { detailAddress, opensDetail } from './door-links.js'\nimport { DoorCounts, DoorFacts, doorLabel, doorSentence, doorsSummary, whenNoLineTag } from './door-sentence.js'\nimport { kindWord, reasonWord, standingWord, whenNoLineBecauseWord, whenNoLineWord } from './door-words.js'\n\n/**\n * 결재 문 현황 — **이 공장에서 어느 승인 단계가 실제로 서 있나.**\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 이 화면이 있나 ──────────────────────────────────────────────────────\n * 양식은 제품이 세우고 **결재선은 그 공장이 채운다.** 채운 것이 「우리는 이 단계를 쓴다」는 선언이라,\n * 비어 있는 것은 결함이 아니라 선택이다. 그런데 **그 선택이 어디에도 안 보였다.** 업무 카탈로그는\n * 「제품이 이 문을 만들었나」를 보여 주고, 이 물음에는 답하지 않는다.\n *\n * 실측(2026-09-11): 미라텍에 양식 11개가 다 서 있고 **11개 전부 결재자가 없었다.** 그 공장에서는\n * 실적 정정도 폐기도 승인 없이 반영되는데, 화면에는 문이 있는 것처럼 보였다.\n *\n * ── 비어 있는 것을 「승인 없음」으로 그리지 않는다 ─────────────────────────\n * 빈 결재선의 결과는 **문마다 선언돼 있다**(ADR-0077). `run` 문은 올린 사람이 자기 건을 승인한 것이\n * 되고, `refuse` 문은 올리는 것 자체가 거절된다. 서버는 그 사실을 값으로 보내고 **문장은 이 화면이\n * 번역으로 짓는다**(§`door-sentence.ts`) — 서버가 한국어로 지어 보내던 때는 다른 언어 사용자도 한국어를\n * 읽었고, 거절하는 문에도 「자기 건을 승인한다」가 적혔다.\n *\n * ── 할 일이 있는 것부터 ────────────────────────────────────────────────────\n * 선언 순서로 늘어놓으면 서 있는 것과 빈 것이 섞이고, 읽는 사람이 열한 줄을 훑어야 한다. **정할\n * 것이 남은 것을 위로** 올린다 — 이 화면을 여는 이유가 그것이다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nconst DOORS = gql`\n {\n workDoors {\n todo\n on\n off\n missing\n notUsed\n seatsStanding\n seatsOffered\n doors {\n name\n labelKey\n kind\n standing\n reason\n approvers\n from\n whenNoLine\n whenNoLineBecause\n followsLabelKey\n assigneeRole\n notUsedBecause\n }\n }\n }\n`\n\ntype Door = DoorFacts & {\n kind: 'approval' | 'assignment'\n standing: 'on' | 'off' | 'missing' | 'not-used'\n from?: string\n}\n\n/**\n * 조치할 것 · 서 있는 것 · 안 쓰기로 한 것 순. 같은 무리 안에서는 선언 순서를 지킨다.\n *\n * 안 쓰기로 한 것을 맨 아래 두는 이유는 **그것이 할 일이 아니어서**다. 위에 섞으면 이 화면을 여는\n * 이유(무엇을 채워야 하나)가 안 보인다.\n */\nconst ORDER: Record<Door['standing'], number> = { missing: 0, off: 1, on: 2, 'not-used': 3 }\n\n@customElement('work-doors-page')\nexport class WorkDoorsPage extends localize(i18next)(PageView) {\n static styles = [\n DocumentPageStyles,\n css`\n /*\n * ── One door, one row: what it is on the left, what it does on the right ──\n * The sentence ran on from the end of the name, so each line started at a different place and wrapped\n * under the name's badge (\"없습니 / 다\" at narrow widths). The name, its kind and its state now hold a\n * column of their own and the sentence wraps inside its column, at the section's width (ADR-0117).\n * The rows share one grid (subgrid), so every sentence starts at the same x.\n */\n .doors {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n column-gap: var(--spacing-huge, 24px);\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n .door {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: subgrid;\n row-gap: var(--spacing-small, 4px);\n padding: var(--spacing-large, 12px) 0;\n border-top: 1px solid var(--md-sys-color-outline-variant);\n }\n\n .door:first-child {\n border-top: none;\n }\n\n @media (max-width: 600px) {\n .doors {\n grid-template-columns: minmax(0, 1fr);\n }\n }\n\n .what {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n }\n\n .name {\n font-weight: 600;\n }\n\n /* Kind in words: an empty approval line and an empty assignment line do opposite things. */\n .kind {\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n /* The state says it in words as well as colour: colour alone says nothing to someone who cannot see it. */\n .mark {\n padding: 0 var(--spacing-small, 4px);\n border: 1px solid currentColor;\n border-radius: var(--md-sys-shape-corner-small, 4px);\n font-size: var(--ui-t-micro, 11px);\n font-weight: 600;\n line-height: 1.6;\n white-space: nowrap;\n }\n\n .mark.on {\n color: var(--md-sys-color-primary);\n }\n\n .mark.off {\n color: var(--md-sys-color-tertiary);\n }\n\n .mark.missing {\n color: var(--md-sys-color-error);\n }\n\n /* Not a task, so quieter. Not removed: that it was turned off has to be readable. */\n .mark.not-used {\n color: var(--md-sys-color-outline);\n }\n\n .door.not-used .what,\n .door.not-used .says {\n opacity: 0.7;\n }\n\n .says {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n }\n\n /* What an empty line does here is declared per door (ADR-0077): one quieter line under the sentence. */\n .when,\n .from {\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n .state {\n margin: 0 0 var(--spacing-medium, 8px);\n font-weight: 600;\n }\n\n ol.next {\n margin: 0;\n padding-left: var(--spacing-huge, 24px);\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n }\n `\n ]\n\n @state() private doors?: Door[]\n @state() private counts: DoorCounts = { on: 0, off: 0, missing: 0, notUsed: 0, todo: 0 }\n @state() private todo = 0\n @state() private seatsStanding = 0\n @state() private seatsOffered = 0\n @state() private loadError: unknown = null\n\n get context() {\n return {\n title: i18next.t('menu.work-doors'),\n description: i18next.t('text.work-doors-lead'),\n frame: 'document'\n }\n }\n\n async pageInitialized() {\n await this.load()\n }\n\n async pageUpdated(changes: any, lifecycle: any) {\n if (lifecycle.active && !this.doors) {\n await this.load()\n }\n }\n\n private async load() {\n this.loadError = null\n\n let response: any\n try {\n response = await client.query({ query: DOORS, fetchPolicy: 'no-cache' })\n } catch (error) {\n this.loadError = error\n return\n }\n\n /*\n * 못 읽은 것을 빈 목록으로 그리지 않는다. 「결재 문이 하나도 없습니다」와 「못 읽었습니다」는\n * 사람이 할 일이 다르다 — 앞은 설치를 보러 갈 일이고 뒤는 다시 눌러 볼 일이다.\n * ox-page-state 가 서버의 문장과 다시 읽기를 그린다(ADR-0060 결정 8).\n */\n if (response.errors?.length) {\n this.loadError = response.errors[0]\n return\n }\n\n /*\n * 전선의 철자를 여기서 **한 번** 옮긴다(§`door-words`). 안 옮기면 i18n 키도 CSS 클래스도\n * 어긋나는데 **오류가 하나도 안 난다** — 키 글자가 화면에 뜨고 상태 색이 투명해질 뿐이다.\n */\n this.doors = (response.data?.workDoors?.doors ?? []).map((one: any) => ({\n ...one,\n standing: standingWord(one.standing) ?? one.standing,\n kind: kindWord(one.kind) ?? one.kind,\n reason: reasonWord(one.reason) ?? one.reason,\n whenNoLine: whenNoLineWord(one.whenNoLine),\n whenNoLineBecause: whenNoLineBecauseWord(one.whenNoLineBecause)\n }))\n\n const read = response.data?.workDoors ?? {}\n\n this.todo = read.todo ?? 0\n this.counts = {\n on: read.on ?? 0,\n off: read.off ?? 0,\n missing: read.missing ?? 0,\n notUsed: read.notUsed ?? 0,\n todo: this.todo\n }\n this.seatsStanding = response.data?.workDoors?.seatsStanding ?? 0\n this.seatsOffered = response.data?.workDoors?.seatsOffered ?? 0\n }\n\n /** 결재선은 각 업무에서 지정하고, 역할이 부족할 때만 별도로 안내한다. */\n private renderNext() {\n if (!this.todo || this.seatsStanding >= this.seatsOffered) {\n return ''\n }\n\n return html`\n <section class=\"page-section\">\n <div class=\"page-section-header\"><h3>${i18next.t('title.work-doors-next')}</h3></div>\n <ol class=\"next\">\n <li>\n ${i18next.t('text.work-doors-next-seats', {\n standing: this.seatsStanding,\n offered: this.seatsOffered\n })}\n <a class=\"page-nav-link\" href=\"roles\">${i18next.t('field.roles')}</a>\n </li>\n </ol>\n </section>\n `\n }\n\n render() {\n return html`<div class=\"page\">\n <ox-page-state .loading=${!this.doors && !this.loadError} .error=${this.loadError} @retry=${() => this.load()}\n >${this.doors && !this.loadError ? this.renderDoors() : ''}</ox-page-state\n >\n </div>`\n }\n\n private renderDoors() {\n const sorted = [...this.doors!].sort((a, b) => ORDER[a.standing] - ORDER[b.standing])\n\n return html`\n <section class=\"page-section\">\n <div class=\"page-section-header\"><h3>${i18next.t('title.work-doors')}</h3></div>\n <p class=\"state\">${doorsSummary(i18next.t, this.counts)}</p>\n <ul class=\"doors\">\n ${sorted.map(door => this.renderDoor(door))}\n </ul>\n </section>\n\n ${this.renderNext()}\n `\n }\n\n private renderDoor(door: Door) {\n const label = doorLabel(i18next.t, door)\n const when = whenNoLineTag(i18next.t, door)\n\n return html`\n <li class=\"door ${door.standing}\">\n <div class=\"what\">\n ${opensDetail(door)\n ? html`<a class=\"name\" href=${detailAddress(door.name)}>${label}</a>`\n : html`<span class=\"name\">${label}</span>`}\n <span class=\"kind\">${i18next.t(`text.door-kind-${door.kind}`)}</span>\n <span class=\"mark ${door.standing}\">${i18next.t(`text.door-standing-${door.standing}`)}</span>\n </div>\n <div class=\"says\">\n <span>\n ${doorSentence(i18next.t, door)}\n ${door.kind === 'approval' && door.standing !== 'missing' && opensDetail(door)\n ? html`<a class=\"page-nav-link\" href=${detailAddress(door.name)}\n >${i18next.t('button.activity-set-approval-line')}</a\n >`\n : ''}\n </span>\n ${when ? html`<span class=\"when\">${when}</span>` : ''}\n ${\n /*\n * 자기 것이 아닌 양식을 쓰는 것은 정상이지만, 안 말하면 「정한 적 없는데」가 된다.\n *\n * ⚠ **차 있는 것과 비어 있는 것은 다른 말이다.** 「본사에서 정했습니다」를 결재자가 없는\n * 줄에 달면 한 줄이 스스로 모순된다 — 정한 것이 없는데 정했다고 적힌다. 비어 있으면\n * 말할 수 있는 것은 **양식의 임자**뿐이고, 그 사실은 「여기서는 못 고칩니다」를 뜻해서\n * 오히려 더 중요하다.\n */\n door.from\n ? html`<span class=\"from\"\n >${door.standing === 'on'\n ? i18next.t('text.door-filled-from', { name: door.from })\n : i18next.t('text.door-form-from', { name: door.from })}</span\n >`\n : ''\n }\n </div>\n </li>\n `\n }\n}\n"]}
@@ -1,199 +0,0 @@
1
- import { strict as assert } from 'node:assert'
2
- import { readFileSync } from 'node:fs'
3
- import { join } from 'node:path'
4
-
5
- import {
6
- DoorFacts,
7
- doorLabel,
8
- doorSentence,
9
- doorsSummary,
10
- whenNoLineTag
11
- } from '../client/pages/work-door/door-sentence.js'
12
- import { DOOR_REASON_WORD, WHEN_NO_LINE_BECAUSE_WORD, WHEN_NO_LINE_WORD } from '../client/pages/work-door/door-words.js'
13
-
14
- /*
15
- * ═══════════════════════════════════════════════════════════════════════════
16
- * The work-doors screen writes its own sentences from the facts the server sends. These read them
17
- * through the real translation files, so a missing key or a sentence that says the wrong
18
- * consequence shows here and not on a customer's screen.
19
- *
20
- * The consequence of an empty line is the door's declaration. On :4000 (2026-09-24) four doors that
21
- * refuse an empty line - spec, recipe, material definition and work shift change approvals - read
22
- * "the raiser approves their own the moment they raise it". That was the `run` sentence on every door.
23
- * ═══════════════════════════════════════════════════════════════════════════
24
- */
25
-
26
- const LANGS = ['ko', 'en', 'ja', 'zh', 'ms'] as const
27
-
28
- const said = Object.fromEntries(
29
- LANGS.map(lang => [
30
- lang,
31
- JSON.parse(readFileSync(join(__dirname, '..', 'translations', `${lang}.json`), 'utf-8')) as Record<string, string>
32
- ])
33
- ) as Record<(typeof LANGS)[number], Record<string, string>>
34
-
35
- /** A small `t` over one language: key lookup, `{name}` interpolation, `defaultValue`. Missing keys print themselves, as i18next does. */
36
- function tIn(lang: (typeof LANGS)[number], extra: Record<string, string> = {}) {
37
- const table = { ...said[lang], ...extra }
38
-
39
- return (key: string, options: Record<string, any> = {}) => {
40
- const found = table[key] ?? options.defaultValue ?? key
41
-
42
- return String(found).replace(/\{(\w+)\}/g, (_, name) => (name in options ? String(options[name]) : `{${name}}`))
43
- }
44
- }
45
-
46
- const ko = tIn('ko', {
47
- 'activity.material-definition-change-approval.label': '자재 정의 변경 승인',
48
- 'activity.work-order-priority-change.label': '작업지시 우선순위 변경',
49
- 'activity.station-screen-release.label': '화면 발행 승인'
50
- })
51
-
52
- const door = (over: Partial<DoorFacts>): DoorFacts => ({
53
- name: 'MaterialDefinitionChangeApproval',
54
- labelKey: 'activity.material-definition-change-approval.label',
55
- kind: 'approval',
56
- standing: 'off',
57
- reason: 'line-empty',
58
- approvers: 0,
59
- ...over
60
- })
61
-
62
- describe('work door sentences', () => {
63
- it('tells a refusing door it cannot be raised, and why - never that the raiser approves their own', () => {
64
- const sentence = doorSentence(ko, door({ whenNoLine: 'refuse', whenNoLineBecause: 'approval-is-the-act' }))
65
-
66
- assert.ok(sentence.includes('「자재 정의 변경 승인」 에 결재자가 없습니다'), sentence)
67
- assert.ok(sentence.includes('상신할 수 없습니다'), sentence)
68
- assert.ok(sentence.includes('승인 자체가 이 업무의 일'), sentence)
69
- assert.ok(!sentence.includes('상신자의 승인'), sentence)
70
- })
71
-
72
- it('keeps the self-approval sentence for a door that runs on an empty line', () => {
73
- const sentence = doorSentence(
74
- ko,
75
- door({ name: 'WorkOrderPriorityChange', labelKey: 'activity.work-order-priority-change.label', whenNoLine: 'run' })
76
- )
77
-
78
- assert.equal(
79
- sentence,
80
- '「작업지시 우선순위 변경」에 결재자가 지정되지 않았습니다. 결재 단계를 거치지 않으며, 상신 즉시 상신자의 승인으로 처리됩니다.'
81
- )
82
- })
83
-
84
- it('says a door that follows its history depends on how the record stood', () => {
85
- const sentence = doorSentence(ko, door({ whenNoLine: 'as-established', whenNoLineBecause: 'approval-is-the-act' }))
86
-
87
- assert.ok(sentence.includes('결재를 통해 생성된 건은'), sentence)
88
- assert.ok(!sentence.includes('상신자의 승인'), sentence)
89
- })
90
-
91
- /** A door with no declaration says only what is known - it does not guess a consequence. */
92
- it('names no consequence for an undeclared door', () => {
93
- assert.equal(doorSentence(ko, door({})), '「자재 정의 변경 승인」 에 결재자가 없습니다.')
94
- })
95
-
96
- it('says where the line is set when a door follows another door', () => {
97
- const sentence = doorSentence(ko, door({ followsLabelKey: 'activity.station-screen-release.label', whenNoLine: 'run' }))
98
-
99
- assert.ok(sentence.includes('「화면 발행 승인」의 결재선을 사용합니다'), sentence)
100
- })
101
-
102
- it('shows the empty-line declaration on a filled row, with the reason for a refusing door', () => {
103
- const tag = whenNoLineTag(
104
- ko,
105
- door({ standing: 'on', reason: 'line-filled', approvers: 1, whenNoLine: 'refuse', whenNoLineBecause: 'irreversible' })
106
- )
107
-
108
- assert.equal(tag, '결재선이 비면 상신할 수 없습니다 (되돌릴 수 없는 일)')
109
- /* An empty row's sentence already says it; saying it twice is noise. */
110
- assert.equal(whenNoLineTag(ko, door({ whenNoLine: 'refuse', whenNoLineBecause: 'irreversible' })), '')
111
- assert.equal(whenNoLineTag(ko, door({ kind: 'assignment', standing: 'on', reason: 'assignee-bound' })), '')
112
- })
113
-
114
- it('keeps the other rows in their Korean wording', () => {
115
- assert.ok(doorSentence(ko, door({ reason: 'role-empty' })).includes('역할을 보유한 사용자가 없습니다'))
116
- assert.ok(doorSentence(ko, door({ reason: 'person-absent' })).includes('지정 결재자가 이 사업장에 등록되어 있지 않습니다'))
117
- assert.ok(doorSentence(ko, door({ kind: 'assignment', reason: 'no-assignee' })).includes('아무에게도 가지 않습니다'))
118
- assert.ok(
119
- doorSentence(ko, door({ kind: 'assignment', standing: 'on', reason: 'assignee-bound', assigneeRole: 'MAINTENANCE-LEAD' })).includes(
120
- 'MAINTENANCE-LEAD'
121
- )
122
- )
123
- assert.ok(doorSentence(ko, door({ standing: 'on', reason: 'line-filled', approvers: 2 })).includes('결재자 2명'))
124
- assert.ok(doorSentence(ko, door({ standing: 'missing', reason: 'missing' })).includes('등록 실패'))
125
- assert.ok(
126
- doorSentence(ko, door({ standing: 'not-used', reason: 'not-used', notUsedBecause: '폐기는 본사가 처리합니다' })).includes(
127
- '폐기는 본사가 처리합니다'
128
- )
129
- )
130
- })
131
-
132
- /** A label with no translation shows the form name, which a person can search for - not the key. */
133
- it('falls back to the form name, never the key', () => {
134
- assert.equal(doorLabel(tIn('ko'), { name: 'SomeDoor', labelKey: 'activity.some-door.label' }), 'SomeDoor')
135
- })
136
-
137
- /*
138
- * Every sentence this screen can build, in every language shipped. A missing one prints its key on
139
- * the screen and nothing throws.
140
- */
141
- it('has every sentence it can build, in every language', () => {
142
- const keys = [
143
- ...Object.values(DOOR_REASON_WORD)
144
- .filter(reason => reason !== 'line-empty')
145
- .map(reason => `text.door-because-${reason}`),
146
- 'text.door-because-line-empty',
147
- ...Object.values(WHEN_NO_LINE_WORD).map(when => `text.door-because-line-empty-${when}`),
148
- ...Object.values(WHEN_NO_LINE_WORD).map(when => `text.door-when-no-line-${when}`),
149
- ...Object.values(WHEN_NO_LINE_BECAUSE_WORD).map(why => `text.door-refuses-because-${why}`),
150
- 'text.door-because-follows',
151
- ...['none', 'all-on', 'done-not-used', 'todo', 'off', 'missing', 'not-used'].map(
152
- one => `text.work-doors-summary-${one}`
153
- )
154
- ]
155
- const missing = LANGS.flatMap(lang => keys.filter(key => !said[lang][key]).map(key => `${lang} · ${key}`))
156
-
157
- assert.deepEqual(missing, [], `\n없는 문구는 화면에 키 글자로 뜹니다:\n${missing.join('\n')}`)
158
- })
159
-
160
- it('reads English to an English reader', () => {
161
- const sentence = doorSentence(tIn('en'), door({ whenNoLine: 'refuse', whenNoLineBecause: 'irreversible' }))
162
-
163
- assert.ok(sentence.includes('cannot be raised until its approval line is filled'), sentence)
164
- assert.ok(!/[가-힣]/.test(sentence), sentence)
165
- })
166
- })
167
-
168
- describe('work door summary', () => {
169
- const counts = (over: object) => ({ on: 0, off: 0, missing: 0, notUsed: 0, todo: 0, ...over })
170
-
171
- it('says everything stands when it does', () => {
172
- const summary = doorsSummary(ko, counts({ on: 2 }))
173
-
174
- assert.equal(summary, '업무 문 2개가 다 서 있습니다.')
175
- })
176
-
177
- it('reads as done when the only unfilled ones were opted out', () => {
178
- const summary = doorsSummary(ko, counts({ on: 1, notUsed: 1 }))
179
-
180
- assert.ok(summary.includes('채울 것이 남지 않았습니다'))
181
- assert.ok(summary.includes('1개는 이 사업장이 쓰지 않기로'))
182
- })
183
-
184
- it('leads with how many are left to fill', () => {
185
- const summary = doorsSummary(ko, counts({ on: 1, off: 2, notUsed: 1, todo: 2 }))
186
-
187
- assert.ok(summary.startsWith('채울 것이 2개 남았습니다.'))
188
- assert.ok(summary.includes('1개는 이 사업장이 쓰지 않기로 한 것이라 세지 않았습니다'))
189
- })
190
-
191
- it('names a door that never installed as a defect', () => {
192
- assert.ok(doorsSummary(ko, counts({ on: 1, missing: 1, todo: 1 })).includes('시스템 오류를 확인해야 합니다'))
193
- })
194
-
195
- /** Zero doors is a different fact from "none of them stand" and must not be reported as it. */
196
- it('does not report an empty product as a plant with nothing standing', () => {
197
- assert.equal(doorsSummary(ko, counts({})), '이 제품이 세우는 업무 양식이 없습니다.')
198
- })
199
- })