@things-factory/worklist 10.1.39 → 10.1.41
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/dist-client/components/activity-starter-form.js +1 -1
- package/dist-client/components/activity-starter-form.js.map +1 -1
- package/dist-client/components/activity-thread-timeline.d.ts +2 -2
- package/dist-client/pages/activity-catalog/activity-detail-page.js +10 -0
- package/dist-client/pages/activity-catalog/activity-detail-page.js.map +1 -1
- package/dist-client/pages/activity-thread/activity-thread-view.d.ts +1 -1
- package/dist-client/pages/work-door/door-sentence.d.ts +63 -0
- package/dist-client/pages/work-door/door-sentence.js +109 -0
- package/dist-client/pages/work-door/door-sentence.js.map +1 -0
- package/dist-client/pages/work-door/door-words.d.ts +7 -0
- package/dist-client/pages/work-door/door-words.js +30 -0
- package/dist-client/pages/work-door/door-words.js.map +1 -1
- package/dist-client/pages/work-door/work-doors-page.d.ts +1 -1
- package/dist-client/pages/work-door/work-doors-page.js +49 -17
- package/dist-client/pages/work-door/work-doors-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/index.d.ts +3 -3
- package/dist-server/service/work-door/work-door-declaration.d.ts +11 -5
- package/dist-server/service/work-door/work-door-declaration.js +5 -3
- package/dist-server/service/work-door/work-door-declaration.js.map +1 -1
- package/dist-server/service/work-door/work-door-resolver.js +1 -1
- package/dist-server/service/work-door/work-door-resolver.js.map +1 -1
- package/dist-server/service/work-door/work-door-service.js +3 -14
- package/dist-server/service/work-door/work-door-service.js.map +1 -1
- package/dist-server/service/work-door/work-door-type.d.ts +43 -6
- package/dist-server/service/work-door/work-door-type.js +102 -26
- package/dist-server/service/work-door/work-door-type.js.map +1 -1
- package/dist-server/service/work-door/work-door.d.ts +65 -16
- package/dist-server/service/work-door/work-door.js +53 -90
- package/dist-server/service/work-door/work-door.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/tests/work-door-door-words.test.ts +35 -1
- package/tests/work-door-not-used-refuse.test.ts +6 -6
- package/tests/work-door-sentence.test.ts +199 -0
- package/tests/work-door.test.ts +63 -56
- package/translations/en.json +26 -1
- package/translations/ja.json +26 -1
- package/translations/ko.json +26 -1
- package/translations/ms.json +26 -1
- package/translations/zh.json +26 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/worklist",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.41",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@operato/moment-timezone-es": "^10.0.0",
|
|
38
38
|
"@operato/p13n": "^10.0.0",
|
|
39
39
|
"@operato/popup": "^10.15.0",
|
|
40
|
-
"@things-factory/attachment-base": "^10.1.
|
|
41
|
-
"@things-factory/auth-base": "^10.1.
|
|
42
|
-
"@things-factory/board-service": "^10.1.
|
|
43
|
-
"@things-factory/context-ui": "^10.1.
|
|
44
|
-
"@things-factory/organization": "^10.1.
|
|
45
|
-
"@things-factory/personalization": "^10.1.
|
|
46
|
-
"@things-factory/scheduler-client": "^10.1.
|
|
47
|
-
"@things-factory/shell": "^10.1.
|
|
40
|
+
"@things-factory/attachment-base": "^10.1.40",
|
|
41
|
+
"@things-factory/auth-base": "^10.1.40",
|
|
42
|
+
"@things-factory/board-service": "^10.1.41",
|
|
43
|
+
"@things-factory/context-ui": "^10.1.40",
|
|
44
|
+
"@things-factory/organization": "^10.1.40",
|
|
45
|
+
"@things-factory/personalization": "^10.1.40",
|
|
46
|
+
"@things-factory/scheduler-client": "^10.1.40",
|
|
47
|
+
"@things-factory/shell": "^10.1.40"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "4f41243edca4c965bc6992a10fb52d75a58fd3a9"
|
|
50
50
|
}
|
|
@@ -2,7 +2,18 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DOOR_KIND_WORD,
|
|
7
|
+
DOOR_REASON_WORD,
|
|
8
|
+
DOOR_STANDING_WORD,
|
|
9
|
+
WHEN_NO_LINE_BECAUSE_WORD,
|
|
10
|
+
WHEN_NO_LINE_WORD,
|
|
11
|
+
kindWord,
|
|
12
|
+
reasonWord,
|
|
13
|
+
standingWord,
|
|
14
|
+
whenNoLineBecauseWord,
|
|
15
|
+
whenNoLineWord
|
|
16
|
+
} from '../client/pages/work-door/door-words.js'
|
|
6
17
|
|
|
7
18
|
/*
|
|
8
19
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -53,6 +64,29 @@ test('every kind the server can send has a word here', () => {
|
|
|
53
64
|
}
|
|
54
65
|
})
|
|
55
66
|
|
|
67
|
+
/*
|
|
68
|
+
* The three enums the row's sentence is built from travel the same way. A reason the screen cannot
|
|
69
|
+
* read picks no sentence; an empty-line declaration it cannot read falls back to saying nothing about
|
|
70
|
+
* the consequence - the very sentence this row got wrong before.
|
|
71
|
+
*/
|
|
72
|
+
test('every reason and empty-line declaration the server can send has a word here, and nothing extra', () => {
|
|
73
|
+
for (const [name, read, table] of [
|
|
74
|
+
['DoorReasonValue', reasonWord, DOOR_REASON_WORD],
|
|
75
|
+
['WhenNoLineValue', whenNoLineWord, WHEN_NO_LINE_WORD],
|
|
76
|
+
['WhenNoLineBecauseValue', whenNoLineBecauseWord, WHEN_NO_LINE_BECAUSE_WORD]
|
|
77
|
+
] as const) {
|
|
78
|
+
const declared = members(name)
|
|
79
|
+
|
|
80
|
+
assert.ok(declared.length >= 3, `${name} 를 ${declared.length}개만 읽었습니다`)
|
|
81
|
+
|
|
82
|
+
for (const [wire, value] of declared) {
|
|
83
|
+
assert.equal(read(wire), value, `${name}.${wire} 이 전선으로 오면 화면이 못 읽습니다`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
assert.deepEqual(Object.keys(table).filter(one => !declared.some(([wire]) => wire === one)), [])
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
56
90
|
test('nothing extra is mapped — a dead entry hides that the server dropped a value', () => {
|
|
57
91
|
const standings = new Set(members('DoorStandingValue').map(one => one[0]))
|
|
58
92
|
const kinds = new Set(members('DoorKindValue').map(one => one[0]))
|
|
@@ -25,14 +25,14 @@ const { Domain, addDataSource, removeDataSource } = require('@things-factory/she
|
|
|
25
25
|
const { User } = require('@things-factory/auth-base')
|
|
26
26
|
|
|
27
27
|
const REFUSING = [
|
|
28
|
-
{ name: 'Scrap',
|
|
29
|
-
{ name: 'Release',
|
|
30
|
-
{ name: 'Defer',
|
|
28
|
+
{ name: 'Scrap', kind: 'approval', whenNoLine: 'refuse', because: 'irreversible' },
|
|
29
|
+
{ name: 'Release', kind: 'approval', whenNoLine: 'refuse', because: 'approval-is-the-act' },
|
|
30
|
+
{ name: 'Defer', kind: 'approval', whenNoLine: 'refuse', because: 'lowers-a-control' }
|
|
31
31
|
]
|
|
32
32
|
const ACCEPTED = [
|
|
33
|
-
{ name: 'Priority',
|
|
34
|
-
{ name: 'Withdraw',
|
|
35
|
-
{ name: 'Dispatch',
|
|
33
|
+
{ name: 'Priority', kind: 'approval', whenNoLine: 'run' },
|
|
34
|
+
{ name: 'Withdraw', kind: 'approval', whenNoLine: 'as-established', because: 'approval-is-the-act' },
|
|
35
|
+
{ name: 'Dispatch', kind: 'assignment' }
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
let db: any
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
})
|
package/tests/work-door.test.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { strict as assert } from 'node:assert'
|
|
2
2
|
|
|
3
|
-
import { doorsToFill, judgeDoor
|
|
3
|
+
import { countDoors, doorsToFill, judgeDoor } from '../server/service/work-door/work-door.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* An empty line is a choice, not a defect — but nothing said which doors were in that state.
|
|
7
7
|
* Miratek had all eleven installed and all eleven empty (measured 2026-09-11), so corrections,
|
|
8
8
|
* scrapping and dispositions all took effect there with nobody approving.
|
|
9
9
|
*/
|
|
10
|
-
const approval = (
|
|
11
|
-
const assignment = (
|
|
10
|
+
const approval = (name: string, over: object = {}) => ({ name, kind: 'approval' as const, whenNoLine: 'run' as const, ...over })
|
|
11
|
+
const assignment = (name: string, over: object = {}) => ({ name, kind: 'assignment' as const, ...over })
|
|
12
12
|
|
|
13
13
|
describe('work door standing', () => {
|
|
14
14
|
/**
|
|
@@ -17,24 +17,24 @@ describe('work door standing', () => {
|
|
|
17
17
|
*/
|
|
18
18
|
it('says where the line is set when a door follows another door', () => {
|
|
19
19
|
const standing = judgeDoor(
|
|
20
|
-
approval('
|
|
21
|
-
follows: '
|
|
20
|
+
approval('StationScreenControlWithdrawal', {
|
|
21
|
+
follows: 'StationScreenRelease',
|
|
22
22
|
installed: { approvalLine: [{}] },
|
|
23
23
|
approvalStanding: { approvers: 1 }
|
|
24
24
|
})
|
|
25
25
|
)
|
|
26
26
|
const empty = judgeDoor(
|
|
27
|
-
approval('
|
|
28
|
-
follows: '
|
|
27
|
+
approval('StationScreenControlWithdrawal', {
|
|
28
|
+
follows: 'StationScreenRelease',
|
|
29
29
|
installed: {},
|
|
30
30
|
approvalStanding: { approvers: 0, refusal: { code: 'approval-line-missing' } }
|
|
31
31
|
})
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
assert.equal(standing.standing, 'on')
|
|
35
|
-
assert.
|
|
35
|
+
assert.equal(standing.followsLabelKey, 'activity.station-screen-release.label')
|
|
36
36
|
assert.equal(empty.standing, 'off')
|
|
37
|
-
assert.
|
|
37
|
+
assert.equal(empty.followsLabelKey, 'activity.station-screen-release.label')
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
it('reads a filled approval line as standing, and says how many it passes', () => {
|
|
@@ -47,7 +47,7 @@ describe('work door standing', () => {
|
|
|
47
47
|
|
|
48
48
|
assert.equal(door.standing, 'on')
|
|
49
49
|
assert.equal(door.approvers, 2)
|
|
50
|
-
assert.
|
|
50
|
+
assert.equal(door.reason, 'line-filled')
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -80,8 +80,7 @@ describe('work door standing', () => {
|
|
|
80
80
|
|
|
81
81
|
assert.equal(door.standing, 'off')
|
|
82
82
|
assert.equal(door.approvers, 0)
|
|
83
|
-
assert.
|
|
84
|
-
assert.ok(!door.because.includes('자기 건을 승인한'))
|
|
83
|
+
assert.equal(door.reason, 'role-empty')
|
|
85
84
|
})
|
|
86
85
|
|
|
87
86
|
/** A named person who is not here is the same defect on the other axis (ADR-0047 ③). */
|
|
@@ -94,8 +93,7 @@ describe('work door standing', () => {
|
|
|
94
93
|
)
|
|
95
94
|
|
|
96
95
|
assert.equal(door.standing, 'off')
|
|
97
|
-
assert.
|
|
98
|
-
assert.ok(!door.because.includes('자기 건을 승인한'))
|
|
96
|
+
assert.equal(door.reason, 'person-absent')
|
|
99
97
|
})
|
|
100
98
|
|
|
101
99
|
/** Not having read the line must not read as "the line is filled". */
|
|
@@ -106,14 +104,49 @@ describe('work door standing', () => {
|
|
|
106
104
|
})
|
|
107
105
|
|
|
108
106
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
107
|
+
* What an empty line does is the door's own declaration (ADR-0077 decision 1). The judge used to
|
|
108
|
+
* write "the raiser approves their own" for every door, and four refusing doors on :4000 carried it
|
|
109
|
+
* (2026-09-24). The declaration now travels with the row, so the screen can say which it is.
|
|
111
110
|
*/
|
|
112
|
-
it('
|
|
113
|
-
const
|
|
111
|
+
it('carries the door\'s own empty-line declaration, and its reason', () => {
|
|
112
|
+
const run = judgeDoor(approval('WorkOrderPriorityChange', { installed: { approvalLine: [] } }))
|
|
113
|
+
const refuse = judgeDoor(
|
|
114
|
+
approval('ScrapExecution', { whenNoLine: 'refuse', whenNoLineBecause: 'irreversible', installed: { approvalLine: [] } })
|
|
115
|
+
)
|
|
116
|
+
const established = judgeDoor(
|
|
117
|
+
approval('NonWorkingWithdrawal', {
|
|
118
|
+
whenNoLine: 'as-established',
|
|
119
|
+
whenNoLineBecause: 'approval-is-the-act',
|
|
120
|
+
installed: { approvalLine: [] }
|
|
121
|
+
})
|
|
122
|
+
)
|
|
114
123
|
|
|
115
|
-
assert.
|
|
116
|
-
assert.
|
|
124
|
+
assert.deepEqual([run.reason, run.whenNoLine, run.whenNoLineBecause], ['line-empty', 'run', undefined])
|
|
125
|
+
assert.deepEqual([refuse.reason, refuse.whenNoLine, refuse.whenNoLineBecause], ['line-empty', 'refuse', 'irreversible'])
|
|
126
|
+
assert.deepEqual(
|
|
127
|
+
[established.whenNoLine, established.whenNoLineBecause],
|
|
128
|
+
['as-established', 'approval-is-the-act']
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
/** An assignment door has no approval line, so it carries no empty-line declaration either. */
|
|
133
|
+
it('carries no empty-line declaration on an assignment door', () => {
|
|
134
|
+
const door = judgeDoor(assignment('WorkOrderDispatch', { whenNoLine: 'run', installed: {} }))
|
|
135
|
+
|
|
136
|
+
assert.equal(door.whenNoLine, undefined)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The label key is computed from the name - never declared. plant declared
|
|
141
|
+
* `activity.MaterialDefinitionChangeApproval.label` while its translations are kebab-cased, and the
|
|
142
|
+
* screen printed twenty-four raw keys (:4000, 2026-09-24).
|
|
143
|
+
*/
|
|
144
|
+
it('computes the label key from the name, the same key the activity catalog reads', () => {
|
|
145
|
+
assert.equal(
|
|
146
|
+
judgeDoor(approval('MaterialDefinitionChangeApproval')).labelKey,
|
|
147
|
+
'activity.material-definition-change-approval.label'
|
|
148
|
+
)
|
|
149
|
+
assert.equal(judgeDoor(approval('WMS Order Recall')).labelKey, 'activity.wms-order-recall.label')
|
|
117
150
|
})
|
|
118
151
|
|
|
119
152
|
/**
|
|
@@ -124,15 +157,15 @@ describe('work door standing', () => {
|
|
|
124
157
|
const door = judgeDoor(assignment('작업 지시 배정', { installed: { assigneeRole: undefined } }))
|
|
125
158
|
|
|
126
159
|
assert.equal(door.standing, 'off')
|
|
127
|
-
assert.
|
|
128
|
-
assert.ok(!door.because.includes('자기 건을 승인'))
|
|
160
|
+
assert.equal(door.reason, 'no-assignee')
|
|
129
161
|
})
|
|
130
162
|
|
|
131
163
|
it('reads a bound role as standing and names it', () => {
|
|
132
164
|
const door = judgeDoor(assignment('정비 배정', { installed: { assigneeRole: 'MAINTENANCE-LEAD' } }))
|
|
133
165
|
|
|
134
166
|
assert.equal(door.standing, 'on')
|
|
135
|
-
assert.
|
|
167
|
+
assert.equal(door.reason, 'assignee-bound')
|
|
168
|
+
assert.equal(door.assigneeRole, 'MAINTENANCE-LEAD')
|
|
136
169
|
})
|
|
137
170
|
|
|
138
171
|
/** An approval line on an assignment door is not what fills it, and must not read as standing. */
|
|
@@ -149,7 +182,7 @@ describe('work door standing', () => {
|
|
|
149
182
|
const door = judgeDoor(approval('초품 검사 승인'))
|
|
150
183
|
|
|
151
184
|
assert.equal(door.standing, 'missing')
|
|
152
|
-
assert.
|
|
185
|
+
assert.equal(door.reason, 'missing')
|
|
153
186
|
})
|
|
154
187
|
|
|
155
188
|
describe('a plant that says it does not use one', () => {
|
|
@@ -161,7 +194,7 @@ describe('work door standing', () => {
|
|
|
161
194
|
const door = judgeDoor(approval('폐기 실행 승인', { notUsedBecause: '폐기는 본사가 처리합니다' }))
|
|
162
195
|
|
|
163
196
|
assert.equal(door.standing, 'not-used')
|
|
164
|
-
assert.
|
|
197
|
+
assert.equal(door.notUsedBecause, '폐기는 본사가 처리합니다')
|
|
165
198
|
})
|
|
166
199
|
|
|
167
200
|
/** Said first, or a plant that opted out keeps being told its missing form is a defect. */
|
|
@@ -169,7 +202,7 @@ describe('work door standing', () => {
|
|
|
169
202
|
const door = judgeDoor(approval('폐기 실행 승인', { notUsedBecause: '안 씁니다', installed: undefined }))
|
|
170
203
|
|
|
171
204
|
assert.equal(door.standing, 'not-used')
|
|
172
|
-
assert.
|
|
205
|
+
assert.equal(door.reason, 'not-used')
|
|
173
206
|
})
|
|
174
207
|
|
|
175
208
|
it('is never counted as something to fill', () => {
|
|
@@ -183,40 +216,14 @@ describe('work door standing', () => {
|
|
|
183
216
|
})
|
|
184
217
|
})
|
|
185
218
|
|
|
186
|
-
describe('
|
|
219
|
+
describe('counts', () => {
|
|
187
220
|
const on = judgeDoor(approval('A', { installed: { approvalLine: [{}] }, approvalStanding: { approvers: 1 } }))
|
|
188
221
|
const off = judgeDoor(approval('B', { installed: { approvalLine: [] } }))
|
|
189
222
|
const missing = judgeDoor(approval('C'))
|
|
190
223
|
const notUsed = judgeDoor(approval('D', { notUsedBecause: '안 씁니다' }))
|
|
191
224
|
|
|
192
|
-
it('
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
assert.ok(said.includes('2개가 다 서 있습니다'))
|
|
196
|
-
assert.ok(!said.includes('결함'))
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
/** Zero left to fill has to read as done even when some doors were opted out. */
|
|
200
|
-
it('reads as done when the only unfilled ones were opted out', () => {
|
|
201
|
-
const said = summarizeDoors([on, notUsed])
|
|
202
|
-
|
|
203
|
-
assert.ok(said.includes('채울 것이 남지 않았습니다'))
|
|
204
|
-
assert.ok(said.includes('1개는 이 공장이 쓰지 않기로'))
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('leads with how many are left to fill', () => {
|
|
208
|
-
const said = summarizeDoors([on, off, off, notUsed])
|
|
209
|
-
|
|
210
|
-
assert.ok(said.startsWith('채울 것이 2개 남았습니다.'))
|
|
211
|
-
assert.ok(said.includes('1개는 이 공장이 쓰지 않기로 한 것이라 세지 않았습니다'))
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('names a door that never installed as a defect', () => {
|
|
215
|
-
assert.ok(summarizeDoors([on, missing]).includes('설정이 아니라 결함'))
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
/** Zero doors is a different fact from "none of them stand" and must not be reported as it. */
|
|
219
|
-
it('does not report an empty product as a plant with nothing standing', () => {
|
|
220
|
-
assert.equal(summarizeDoors([]), '이 제품이 세우는 업무 양식이 없습니다.')
|
|
225
|
+
it('counts each standing apart, so the screen can say the plant in one sentence', () => {
|
|
226
|
+
assert.deepEqual(countDoors([on, off, off, missing, notUsed]), { on: 1, off: 2, missing: 1, notUsed: 1 })
|
|
227
|
+
assert.deepEqual(countDoors([]), { on: 0, off: 0, missing: 0, notUsed: 0 })
|
|
221
228
|
})
|
|
222
229
|
})
|
package/translations/en.json
CHANGED
|
@@ -226,6 +226,24 @@
|
|
|
226
226
|
"menu.work-doors": "approval and assignment forms",
|
|
227
227
|
"text.discard-draft-confirm": "discard this draft?",
|
|
228
228
|
"text.discard-draft-for-good": "a discarded draft cannot be brought back.",
|
|
229
|
+
"text.door-because-assignee-bound": "\"{label}\" goes to {role}.",
|
|
230
|
+
"text.door-because-follows": "it follows the approval line of \"{label}\", so the line is set there.",
|
|
231
|
+
"text.door-because-line-empty": "\"{label}\" has no approvers.",
|
|
232
|
+
"text.door-because-line-empty-as-established": "\"{label}\" has no approvers. a record that stood through approval cannot be raised until the line is filled; one that stood without approval takes effect as soon as it is raised.",
|
|
233
|
+
"text.door-because-line-empty-refuse": "\"{label}\" has no approvers. this task cannot be raised until its approval line is filled ({why}).",
|
|
234
|
+
"text.door-because-line-empty-run": "\"{label}\" has no approvers. the step does not stand, and whoever raises it approves their own the moment they raise it.",
|
|
235
|
+
"text.door-because-line-filled": "\"{label}\" passes {count} approvers.",
|
|
236
|
+
"text.door-because-missing": "the \"{label}\" form was not installed in this plant. the plant did not opt out; it failed to install, so check the startup log.",
|
|
237
|
+
"text.door-because-no-assignee": "no role is bound to receive \"{label}\". the task is created but reaches nobody.",
|
|
238
|
+
"text.door-because-not-used": "\"{label}\" is a task this plant decided not to use — {reason}",
|
|
239
|
+
"text.door-because-person-absent": "the person named in the approval line of \"{label}\" is not in this plant. name someone else or add that person for this step to stand.",
|
|
240
|
+
"text.door-because-role-empty": "the role named in the approval line of \"{label}\" has nobody in it. the line is set; put a person in that role for this step to stand.",
|
|
241
|
+
"text.door-refuses-because-approval-is-the-act": "the approval is the work itself",
|
|
242
|
+
"text.door-refuses-because-irreversible": "what it does cannot be undone",
|
|
243
|
+
"text.door-refuses-because-lowers-a-control": "it lowers a control that stands",
|
|
244
|
+
"text.door-when-no-line-as-established": "with no approval line, it depends on how the record stood",
|
|
245
|
+
"text.door-when-no-line-refuse": "with no approval line, it cannot be raised",
|
|
246
|
+
"text.door-when-no-line-run": "with no approval line, it takes effect as soon as it is raised",
|
|
229
247
|
"text.draft-discarded": "the draft was discarded.",
|
|
230
248
|
"text.activity-activated": "{name} is now on for this domain.",
|
|
231
249
|
"text.activity-change-failed": "{name} was not changed: {why}",
|
|
@@ -322,10 +340,17 @@
|
|
|
322
340
|
"text.task_is_saved_successfully": "task is saved successfully",
|
|
323
341
|
"text.task_is_started_successfully": "task is started successfully.",
|
|
324
342
|
"text.timeline-count": "{count} entries",
|
|
325
|
-
"text.work-doors-lead": "shows whether each approval and assignment step has approvers or receivers. without
|
|
343
|
+
"text.work-doors-lead": "shows whether each approval and assignment step has approvers or receivers. without approvers, an approval either takes effect as soon as it is raised or cannot be raised at all, depending on the task; an assignment reaches nobody.",
|
|
326
344
|
"text.work-doors-next-line": "set an approval line on {count} work forms —",
|
|
327
345
|
"text.work-doors-next-seats": "{standing} of {offered} roles are set up. set up roles first to use them in approval lines —",
|
|
328
346
|
"text.work-doors-read-failed": "could not read the approval forms.",
|
|
347
|
+
"text.work-doors-summary-all-on": "all {count} work forms are set.",
|
|
348
|
+
"text.work-doors-summary-done-not-used": "nothing left to fill. {on} work forms are set and {notUsed} are ones this plant decided not to use.",
|
|
349
|
+
"text.work-doors-summary-missing": "{count} were never installed. that is a defect, not a setting.",
|
|
350
|
+
"text.work-doors-summary-none": "this product installs no work forms.",
|
|
351
|
+
"text.work-doors-summary-not-used": "{count} this plant decided not to use are not counted.",
|
|
352
|
+
"text.work-doors-summary-off": "{count} are installed with nobody on them — this plant has not yet decided who does them.",
|
|
353
|
+
"text.work-doors-summary-todo": "forms left to fill: {count}.",
|
|
329
354
|
"text.write-your-comment-as-reference": "write your comment.",
|
|
330
355
|
"title.activity": "activity",
|
|
331
356
|
"title.activity application": "activity application",
|
package/translations/ja.json
CHANGED
|
@@ -226,6 +226,24 @@
|
|
|
226
226
|
"menu.work-doors": "承認・割当様式の状況",
|
|
227
227
|
"text.discard-draft-confirm": "この下書きを破棄しますか?",
|
|
228
228
|
"text.discard-draft-for-good": "破棄した下書きは元に戻せません。",
|
|
229
|
+
"text.door-because-assignee-bound": "「{label}」 は {role} が受け取ります。",
|
|
230
|
+
"text.door-because-follows": "「{label}」 の承認ルートに従うため、承認ルートはそちらで設定します。",
|
|
231
|
+
"text.door-because-line-empty": "「{label}」 に承認者がいません。",
|
|
232
|
+
"text.door-because-line-empty-as-established": "「{label}」 に承認者がいません。承認を経て成立した件は承認ルートを設定するまで起票できず、承認なしで成立した件は起票した時点で反映されます。",
|
|
233
|
+
"text.door-because-line-empty-refuse": "「{label}」 に承認者がいません。承認ルートを設定するまでこの業務は起票できません({why})。",
|
|
234
|
+
"text.door-because-line-empty-run": "「{label}」 に承認者がいません。この段階は成立せず、起票した人が起票した時点で自分の件を承認したことになります。",
|
|
235
|
+
"text.door-because-line-filled": "「{label}」 は承認者 {count}名を経ます。",
|
|
236
|
+
"text.door-because-missing": "「{label}」 の様式がこの工場に設置されていません。工場が使わないと決めたのではなく設置できなかったものなので、起動記録を確認してください。",
|
|
237
|
+
"text.door-because-no-assignee": "「{label}」 を受け取るロールが割り当てられていません。この業務は作成されても誰にも届きません。",
|
|
238
|
+
"text.door-because-not-used": "「{label}」 はこの工場が使わないと決めた業務です — {reason}",
|
|
239
|
+
"text.door-because-person-absent": "「{label}」 の承認ルートが指定した人がこの工場にいません。別の人を指定するか、その人を追加するとこの段階が成立します。",
|
|
240
|
+
"text.door-because-role-empty": "「{label}」 の承認ルートが指定したロールに人がいません。承認ルートは決まっているので、そのロールに人を入れるとこの段階が成立します。",
|
|
241
|
+
"text.door-refuses-because-approval-is-the-act": "承認そのものがこの業務",
|
|
242
|
+
"text.door-refuses-because-irreversible": "取り消せない作業",
|
|
243
|
+
"text.door-refuses-because-lowers-a-control": "かかっている統制を下げる作業",
|
|
244
|
+
"text.door-when-no-line-as-established": "承認ルートが空ならその件がどう成立したかによります",
|
|
245
|
+
"text.door-when-no-line-refuse": "承認ルートが空なら起票できません",
|
|
246
|
+
"text.door-when-no-line-run": "承認ルートが空なら起票した時点で反映されます",
|
|
229
247
|
"text.draft-discarded": "下書きを破棄しました。",
|
|
230
248
|
"text.activity-activated": "{name} をこの$t(term.domain)で使用するようにしました。",
|
|
231
249
|
"text.activity-change-failed": "{name} を変更できませんでした: {why}",
|
|
@@ -322,10 +340,17 @@
|
|
|
322
340
|
"text.task_is_saved_successfully": "業務が正常に保存されました。",
|
|
323
341
|
"text.task_is_started_successfully": "業務が正常に開始されました。",
|
|
324
342
|
"text.timeline-count": "{count}件",
|
|
325
|
-
"text.work-doors-lead": "
|
|
343
|
+
"text.work-doors-lead": "承認・割当の各段階で承認者と受取人が決まっているかを表示します。承認者がいないと、承認は業務によって起票と同時に反映されるか起票できなくなり、割当は誰にも届きません。",
|
|
326
344
|
"text.work-doors-next-line": "業務様式 {count}件に承認ラインを指定してください —",
|
|
327
345
|
"text.work-doors-next-seats": "ロール {offered}件中 {standing}件が設定されています。承認ラインに使うロールを先に設定してください —",
|
|
328
346
|
"text.work-doors-read-failed": "承認様式の状況を読み込めませんでした。",
|
|
347
|
+
"text.work-doors-summary-all-on": "業務様式 {count}件がすべて設定されています。",
|
|
348
|
+
"text.work-doors-summary-done-not-used": "設定すべきものは残っていません。業務様式 {on}件が設定済みで、{notUsed}件はこの工場が使わないと決めたものです。",
|
|
349
|
+
"text.work-doors-summary-missing": "{count}件は様式がまったく設置されていません。これは設定ではなく欠陥です。",
|
|
350
|
+
"text.work-doors-summary-none": "この製品が設置する業務様式はありません。",
|
|
351
|
+
"text.work-doors-summary-not-used": "{count}件はこの工場が使わないと決めたものなので数えていません。",
|
|
352
|
+
"text.work-doors-summary-off": "{count}件は様式はあるが担当者がいません — 誰が行うかをこの工場がまだ決めていないものです。",
|
|
353
|
+
"text.work-doors-summary-todo": "設定すべきものが {count}件 残っています。",
|
|
329
354
|
"text.write-your-comment-as-reference": "ご意見をご記入ください。",
|
|
330
355
|
"title.activity": "業務",
|
|
331
356
|
"title.activity application": "業務申込書作成",
|