@things-factory/worklist 10.1.186 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/worklist",
3
- "version": "10.1.186",
3
+ "version": "10.1.188",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "exports": {
@@ -54,15 +54,15 @@
54
54
  "@operato/moment-timezone-es": "^10.0.0",
55
55
  "@operato/p13n": "^10.0.0",
56
56
  "@operato/popup": "^10.15.0",
57
- "@things-factory/attachment-base": "^10.1.183",
58
- "@things-factory/auth-base": "^10.1.183",
59
- "@things-factory/board-service": "^10.1.183",
60
- "@things-factory/context-ui": "^10.1.179",
61
- "@things-factory/i18n-base": "^10.1.183",
62
- "@things-factory/organization": "^10.1.186",
63
- "@things-factory/personalization": "^10.1.183",
64
- "@things-factory/scheduler-client": "^10.1.183",
65
- "@things-factory/shell": "^10.1.179"
57
+ "@things-factory/attachment-base": "^10.1.188",
58
+ "@things-factory/auth-base": "^10.1.188",
59
+ "@things-factory/board-service": "^10.1.188",
60
+ "@things-factory/context-ui": "^10.1.188",
61
+ "@things-factory/i18n-base": "^10.1.188",
62
+ "@things-factory/organization": "^10.1.188",
63
+ "@things-factory/personalization": "^10.1.188",
64
+ "@things-factory/scheduler-client": "^10.1.188",
65
+ "@things-factory/shell": "^10.1.188"
66
66
  },
67
- "gitHead": "9d45de8ca71f05141df673e5ffe48bfce5b3ce9f"
67
+ "gitHead": "c5e75e2e0b565e0666472bc835f5f1538fb18f48"
68
68
  }
@@ -1,26 +0,0 @@
1
- import '@operato/list-state/ox-page-state.js';
2
- import { PageView } from '@operato/shell';
3
- declare const WorkDoorsPage_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
4
- export declare class WorkDoorsPage extends WorkDoorsPage_base {
5
- static styles: import("lit").CSSResult[];
6
- private doors?;
7
- private counts;
8
- private todo;
9
- private seatsStanding;
10
- private seatsOffered;
11
- private loadError;
12
- get context(): {
13
- title: string;
14
- description: string;
15
- frame: string;
16
- };
17
- pageInitialized(): Promise<void>;
18
- pageUpdated(changes: any, lifecycle: any): Promise<void>;
19
- private load;
20
- /** 결재선은 각 업무에서 지정하고, 역할이 부족할 때만 별도로 안내한다. */
21
- private renderNext;
22
- render(): import("lit-html").TemplateResult<1>;
23
- private renderDoors;
24
- private renderDoor;
25
- }
26
- export {};
@@ -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"]}