@things-factory/ops-master 10.1.35 → 10.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist-client/bootstrap.d.ts +1 -0
  2. package/dist-client/bootstrap.js +26 -0
  3. package/dist-client/bootstrap.js.map +1 -1
  4. package/dist-client/master-change-policy-axes.d.ts +26 -0
  5. package/dist-client/master-change-policy-axes.js +18 -0
  6. package/dist-client/master-change-policy-axes.js.map +1 -0
  7. package/dist-client/master-change-policy-panel.d.ts +49 -0
  8. package/dist-client/master-change-policy-panel.js +393 -0
  9. package/dist-client/master-change-policy-panel.js.map +1 -0
  10. package/dist-client/pages/master/applied-when-dialog.d.ts +45 -0
  11. package/dist-client/pages/master/applied-when-dialog.js +139 -0
  12. package/dist-client/pages/master/applied-when-dialog.js.map +1 -0
  13. package/dist-client/pages/master/assignment-fields.d.ts +27 -0
  14. package/dist-client/pages/master/assignment-fields.js +62 -0
  15. package/dist-client/pages/master/assignment-fields.js.map +1 -0
  16. package/dist-client/pages/master/equipment.js +1 -0
  17. package/dist-client/pages/master/equipment.js.map +1 -1
  18. package/dist-client/pages/master/master-page-base.d.ts +12 -0
  19. package/dist-client/pages/master/master-page-base.js +38 -4
  20. package/dist-client/pages/master/master-page-base.js.map +1 -1
  21. package/dist-client/pages/master/operational-location.js +1 -0
  22. package/dist-client/pages/master/operational-location.js.map +1 -1
  23. package/dist-client/tsconfig.tsbuildinfo +1 -1
  24. package/dist-server/service/approval-route.d.ts +37 -0
  25. package/dist-server/service/approval-route.js +34 -0
  26. package/dist-server/service/approval-route.js.map +1 -0
  27. package/dist-server/service/assignment/applied-when.d.ts +24 -0
  28. package/dist-server/service/assignment/applied-when.js +16 -0
  29. package/dist-server/service/assignment/applied-when.js.map +1 -0
  30. package/dist-server/service/assignment/assignment-history.d.ts +92 -0
  31. package/dist-server/service/assignment/assignment-history.js +289 -0
  32. package/dist-server/service/assignment/assignment-history.js.map +1 -0
  33. package/dist-server/service/assignment/assignment-write.d.ts +42 -0
  34. package/dist-server/service/assignment/assignment-write.js +101 -0
  35. package/dist-server/service/assignment/assignment-write.js.map +1 -0
  36. package/dist-server/service/assignment/master-value-claim.d.ts +135 -0
  37. package/dist-server/service/assignment/master-value-claim.js +174 -0
  38. package/dist-server/service/assignment/master-value-claim.js.map +1 -0
  39. package/dist-server/service/equipment/equipment-master-resolver.d.ts +9 -1
  40. package/dist-server/service/equipment/equipment-master-resolver.js +6 -3
  41. package/dist-server/service/equipment/equipment-master-resolver.js.map +1 -1
  42. package/dist-server/service/index.d.ts +6 -1
  43. package/dist-server/service/index.js +10 -0
  44. package/dist-server/service/index.js.map +1 -1
  45. package/dist-server/service/location/location-master-resolver.d.ts +4 -1
  46. package/dist-server/service/location/location-master-resolver.js +6 -3
  47. package/dist-server/service/location/location-master-resolver.js.map +1 -1
  48. package/dist-server/service/master-change-policy.d.ts +65 -2
  49. package/dist-server/service/master-change-policy.js +166 -21
  50. package/dist-server/service/master-change-policy.js.map +1 -1
  51. package/dist-server/service/master-crud.d.ts +11 -0
  52. package/dist-server/service/master-crud.js +12 -1
  53. package/dist-server/service/master-crud.js.map +1 -1
  54. package/dist-server/service/material/material-revision-service.js +13 -0
  55. package/dist-server/service/material/material-revision-service.js.map +1 -1
  56. package/dist-server/service/recipe/recipe-revision-service.js +13 -0
  57. package/dist-server/service/recipe/recipe-revision-service.js.map +1 -1
  58. package/dist-server/service/test-spec/test-spec-revision-service.js +14 -0
  59. package/dist-server/service/test-spec/test-spec-revision-service.js.map +1 -1
  60. package/dist-server/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +6 -4
  62. package/translations/en.json +59 -2
  63. package/translations/ja.json +58 -1
  64. package/translations/ko.json +59 -2
  65. package/translations/ms.json +58 -1
  66. package/translations/zh.json +60 -1
@@ -0,0 +1,45 @@
1
+ import { LitElement } from 'lit';
2
+ import { CLAIMED_FIELDS } from './assignment-fields.js';
3
+ /**
4
+ * **언제부터 그러한가를 묻는 자리.**
5
+ *
6
+ * ── 왜 날짜 칸 하나가 아닌가 ───────────────────────────────────────────────
7
+ * 그리드에 날짜 열을 하나 더하면 사람은 그것을 **또 하나의 값**으로 읽는다. 이 셋(자리 배정 · 근무조
8
+ * 선언 · 부모 관계)은 값이 아니라 **언제부터 그렇게 볼 것인가**를 함께 정하는 일이고, 과거를 적으면
9
+ * **지연 기록인지 정정인지**까지 갈라야 한다. 그래서 저장 직전에 한 번 묻는다.
10
+ *
11
+ * ```
12
+ * 바뀌기 전 → 바뀐 뒤
13
+ * 적용 시작 기본값 「지금부터」
14
+ * 과거 시각이면 지연 기록인가 이미 적은 것의 정정인가
15
+ * 미래 시각이면 지금 값은 그대로이고 변경이 예약된다고 알린다
16
+ * ```
17
+ *
18
+ * ⚠ **기록 시각은 안 묻는다.** 사람이 넣으면 「언제 알았나」를 사람이 정하게 된다 — 서버가 붙인다.
19
+ */
20
+ export interface AppliedAnswer {
21
+ at?: string;
22
+ kind?: 'change' | 'correction';
23
+ until?: string;
24
+ }
25
+ export interface AssignmentChange {
26
+ rowName: string;
27
+ field: string;
28
+ before: string;
29
+ after: string;
30
+ }
31
+ export declare class AppliedWhenDialog extends LitElement {
32
+ static styles: import("lit").CSSResult;
33
+ changes: AssignmentChange[];
34
+ onAnswer?: (answer: AppliedAnswer) => void;
35
+ onCancel?: () => void;
36
+ private at;
37
+ private kind;
38
+ private until;
39
+ /** 안 적으면 지금부터다 — 그 기본값이 곧 「지금부터」라는 주장이다. */
40
+ private get when();
41
+ private get past();
42
+ private get future();
43
+ render(): import("lit-html").TemplateResult<1>;
44
+ }
45
+ export { CLAIMED_FIELDS };
@@ -0,0 +1,139 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { LitElement, css, html } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { i18next } from '@operato/i18n';
5
+ import { CLAIMED_FIELDS } from './assignment-fields.js';
6
+ let AppliedWhenDialog = class AppliedWhenDialog extends LitElement {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.changes = [];
10
+ this.at = '';
11
+ this.kind = 'change';
12
+ this.until = '';
13
+ }
14
+ static { this.styles = css `
15
+ :host {
16
+ display: block;
17
+ padding: var(--spacing-medium, 12px);
18
+ }
19
+ .what {
20
+ display: grid;
21
+ grid-template-columns: auto 1fr;
22
+ gap: var(--spacing-tiny, 4px) var(--spacing-small, 8px);
23
+ margin-bottom: var(--spacing-medium, 12px);
24
+ }
25
+ .what span {
26
+ color: var(--md-sys-color-on-surface-variant);
27
+ }
28
+ .note {
29
+ color: var(--md-sys-color-on-surface-variant);
30
+ margin-top: var(--spacing-small, 8px);
31
+ }
32
+ .buttons {
33
+ display: flex;
34
+ justify-content: flex-end;
35
+ gap: var(--spacing-small, 8px);
36
+ margin-top: var(--spacing-medium, 12px);
37
+ }
38
+ `; }
39
+ /** 안 적으면 지금부터다 — 그 기본값이 곧 「지금부터」라는 주장이다. */
40
+ get when() {
41
+ return this.at ? new Date(this.at).getTime() : Date.now();
42
+ }
43
+ get past() {
44
+ return this.when < Date.now() - 60000;
45
+ }
46
+ get future() {
47
+ return this.when > Date.now() + 60000;
48
+ }
49
+ render() {
50
+ return html `
51
+ <div class="what">
52
+ ${this.changes.map(one => html `
53
+ <span>${one.rowName} · ${labelOf(one.field)}</span>
54
+ <div>${one.before || i18next.t('text.none')} → <b>${one.after || i18next.t('text.none')}</b></div>
55
+ `)}
56
+ </div>
57
+
58
+ <label>
59
+ ${i18next.t('label.applied-from')}
60
+ <input
61
+ type="datetime-local"
62
+ .value=${this.at}
63
+ @change=${(e) => (this.at = e.target.value)}
64
+ placeholder=${i18next.t('text.from-now')}
65
+ />
66
+ </label>
67
+
68
+ ${this.past
69
+ ? html `
70
+ <div>
71
+ <label>
72
+ <input type="radio" name="kind" ?checked=${this.kind === 'change'} @change=${() => (this.kind = 'change')} />
73
+ ${i18next.t('label.late-record')}
74
+ </label>
75
+ <label>
76
+ <input type="radio" name="kind" ?checked=${this.kind === 'correction'} @change=${() => (this.kind = 'correction')} />
77
+ ${i18next.t('label.correct-what-was-recorded')}
78
+ </label>
79
+ ${this.kind === 'correction'
80
+ ? html `
81
+ <label>
82
+ ${i18next.t('label.applied-until')}
83
+ <input type="datetime-local" .value=${this.until} @change=${(e) => (this.until = e.target.value)} />
84
+ </label>
85
+ `
86
+ : ''}
87
+ </div>
88
+ `
89
+ : ''}
90
+ ${this.future ? html `<div class="note">${i18next.t('text.change-is-scheduled')}</div>` : ''}
91
+
92
+ <div class="buttons">
93
+ <button @click=${() => this.onCancel?.()}>${i18next.t('button.cancel')}</button>
94
+ <button
95
+ @click=${() => this.onAnswer?.({
96
+ ...(this.at ? { at: new Date(this.at).toISOString() } : {}),
97
+ ...(this.past ? { kind: this.kind } : {}),
98
+ ...(this.past && this.kind === 'correction' && this.until ? { until: new Date(this.until).toISOString() } : {})
99
+ })}
100
+ >
101
+ ${i18next.t('button.save')}
102
+ </button>
103
+ </div>
104
+ `;
105
+ }
106
+ };
107
+ __decorate([
108
+ property({ type: Array }),
109
+ __metadata("design:type", Array)
110
+ ], AppliedWhenDialog.prototype, "changes", void 0);
111
+ __decorate([
112
+ property({ type: Object }),
113
+ __metadata("design:type", Function)
114
+ ], AppliedWhenDialog.prototype, "onAnswer", void 0);
115
+ __decorate([
116
+ property({ type: Object }),
117
+ __metadata("design:type", Function)
118
+ ], AppliedWhenDialog.prototype, "onCancel", void 0);
119
+ __decorate([
120
+ state(),
121
+ __metadata("design:type", Object)
122
+ ], AppliedWhenDialog.prototype, "at", void 0);
123
+ __decorate([
124
+ state(),
125
+ __metadata("design:type", String)
126
+ ], AppliedWhenDialog.prototype, "kind", void 0);
127
+ __decorate([
128
+ state(),
129
+ __metadata("design:type", Object)
130
+ ], AppliedWhenDialog.prototype, "until", void 0);
131
+ AppliedWhenDialog = __decorate([
132
+ customElement('applied-when-dialog')
133
+ ], AppliedWhenDialog);
134
+ export { AppliedWhenDialog };
135
+ function labelOf(field) {
136
+ return i18next.t(`field.${field}`, { defaultValue: field });
137
+ }
138
+ export { CLAIMED_FIELDS };
139
+ //# sourceMappingURL=applied-when-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applied-when-dialog.js","sourceRoot":"","sources":["../../../client/pages/master/applied-when-dialog.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\n\nimport { CLAIMED_FIELDS } from './assignment-fields.js'\n\n/**\n * **언제부터 그러한가를 묻는 자리.**\n *\n * ── 왜 날짜 칸 하나가 아닌가 ───────────────────────────────────────────────\n * 그리드에 날짜 열을 하나 더하면 사람은 그것을 **또 하나의 값**으로 읽는다. 이 셋(자리 배정 · 근무조\n * 선언 · 부모 관계)은 값이 아니라 **언제부터 그렇게 볼 것인가**를 함께 정하는 일이고, 과거를 적으면\n * **지연 기록인지 정정인지**까지 갈라야 한다. 그래서 저장 직전에 한 번 묻는다.\n *\n * ```\n * 바뀌기 전 → 바뀐 뒤\n * 적용 시작 기본값 「지금부터」\n * 과거 시각이면 지연 기록인가 이미 적은 것의 정정인가\n * 미래 시각이면 지금 값은 그대로이고 변경이 예약된다고 알린다\n * ```\n *\n * ⚠ **기록 시각은 안 묻는다.** 사람이 넣으면 「언제 알았나」를 사람이 정하게 된다 — 서버가 붙인다.\n */\nexport interface AppliedAnswer {\n at?: string\n kind?: 'change' | 'correction'\n until?: string\n}\n\nexport interface AssignmentChange {\n rowName: string\n field: string\n before: string\n after: string\n}\n\n@customElement('applied-when-dialog')\nexport class AppliedWhenDialog extends LitElement {\n static styles = css`\n :host {\n display: block;\n padding: var(--spacing-medium, 12px);\n }\n .what {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: var(--spacing-tiny, 4px) var(--spacing-small, 8px);\n margin-bottom: var(--spacing-medium, 12px);\n }\n .what span {\n color: var(--md-sys-color-on-surface-variant);\n }\n .note {\n color: var(--md-sys-color-on-surface-variant);\n margin-top: var(--spacing-small, 8px);\n }\n .buttons {\n display: flex;\n justify-content: flex-end;\n gap: var(--spacing-small, 8px);\n margin-top: var(--spacing-medium, 12px);\n }\n `\n\n @property({ type: Array }) changes: AssignmentChange[] = []\n @property({ type: Object }) onAnswer?: (answer: AppliedAnswer) => void\n @property({ type: Object }) onCancel?: () => void\n\n @state() private at = ''\n @state() private kind: 'change' | 'correction' = 'change'\n @state() private until = ''\n\n /** 안 적으면 지금부터다 — 그 기본값이 곧 「지금부터」라는 주장이다. */\n private get when(): number {\n return this.at ? new Date(this.at).getTime() : Date.now()\n }\n\n private get past(): boolean {\n return this.when < Date.now() - 60000\n }\n\n private get future(): boolean {\n return this.when > Date.now() + 60000\n }\n\n render() {\n return html`\n <div class=\"what\">\n ${this.changes.map(\n one => html`\n <span>${one.rowName} · ${labelOf(one.field)}</span>\n <div>${one.before || i18next.t('text.none')} → <b>${one.after || i18next.t('text.none')}</b></div>\n `\n )}\n </div>\n\n <label>\n ${i18next.t('label.applied-from')}\n <input\n type=\"datetime-local\"\n .value=${this.at}\n @change=${(e: Event) => (this.at = (e.target as HTMLInputElement).value)}\n placeholder=${i18next.t('text.from-now')}\n />\n </label>\n\n ${this.past\n ? html`\n <div>\n <label>\n <input type=\"radio\" name=\"kind\" ?checked=${this.kind === 'change'} @change=${() => (this.kind = 'change')} />\n ${i18next.t('label.late-record')}\n </label>\n <label>\n <input type=\"radio\" name=\"kind\" ?checked=${this.kind === 'correction'} @change=${() => (this.kind = 'correction')} />\n ${i18next.t('label.correct-what-was-recorded')}\n </label>\n ${this.kind === 'correction'\n ? html`\n <label>\n ${i18next.t('label.applied-until')}\n <input type=\"datetime-local\" .value=${this.until} @change=${(e: Event) => (this.until = (e.target as HTMLInputElement).value)} />\n </label>\n `\n : ''}\n </div>\n `\n : ''}\n ${this.future ? html`<div class=\"note\">${i18next.t('text.change-is-scheduled')}</div>` : ''}\n\n <div class=\"buttons\">\n <button @click=${() => this.onCancel?.()}>${i18next.t('button.cancel')}</button>\n <button\n @click=${() =>\n this.onAnswer?.({\n ...(this.at ? { at: new Date(this.at).toISOString() } : {}),\n ...(this.past ? { kind: this.kind } : {}),\n ...(this.past && this.kind === 'correction' && this.until ? { until: new Date(this.until).toISOString() } : {})\n })}\n >\n ${i18next.t('button.save')}\n </button>\n </div>\n `\n }\n}\n\nfunction labelOf(field: string): string {\n return i18next.t(`field.${field}`, { defaultValue: field })\n}\n\nexport { CLAIMED_FIELDS }\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * **이력을 받는 칸 셋** — 화면이 저장 직전에 「언제부터」를 묻는 대상.
3
+ *
4
+ * 서버의 `CLAIMED_FIELDS` 와 같은 목록이다. 화면이 서버를 import 하지 않으므로(그러면 빌드가
5
+ * 옮겨 간다) 여기 한 벌을 둔다 — **갈라지면 안 묻고 보내는 칸이 생긴다.**
6
+ */
7
+ export declare const CLAIMED_FIELDS: {
8
+ axis: string;
9
+ field: string;
10
+ }[];
11
+ /** 이 축에서 이력을 받는 칸들. */
12
+ export declare function claimedFieldsOf(axis: string): string[];
13
+ export interface AssignmentChange {
14
+ rowName: string;
15
+ field: string;
16
+ before: string;
17
+ after: string;
18
+ }
19
+ /**
20
+ * 바뀐 것 중 **이력을 받는 칸을 건드린 것**만 고른다.
21
+ *
22
+ * ⚠ **새로 만드는 행은 안 묻는다**(`__dirty__` 가 `+` 다). 그 행은 그 순간 생긴 것이고 「언제부터」는
23
+ * 그 순간이다. 물으면 존재하기 전의 자리를 적을 수 있게 된다.
24
+ *
25
+ * ⚠ **안 바뀐 칸은 애초에 `__dirtyfields__` 에 없다.** 그리드가 이미 가려 준다.
26
+ */
27
+ export declare function assignmentChangesIn(axis: string, patches: readonly Record<string, unknown>[]): AssignmentChange[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * **이력을 받는 칸 셋** — 화면이 저장 직전에 「언제부터」를 묻는 대상.
3
+ *
4
+ * 서버의 `CLAIMED_FIELDS` 와 같은 목록이다. 화면이 서버를 import 하지 않으므로(그러면 빌드가
5
+ * 옮겨 간다) 여기 한 벌을 둔다 — **갈라지면 안 묻고 보내는 칸이 생긴다.**
6
+ */
7
+ export const CLAIMED_FIELDS = [
8
+ { axis: 'equipment', field: 'homeLocation' },
9
+ { axis: 'operationalLocation', field: 'workShiftNames' },
10
+ { axis: 'operationalLocation', field: 'parentId' }
11
+ ];
12
+ /** 이 축에서 이력을 받는 칸들. */
13
+ export function claimedFieldsOf(axis) {
14
+ return CLAIMED_FIELDS.filter(one => one.axis === axis).map(one => one.field);
15
+ }
16
+ /**
17
+ * 바뀐 것 중 **이력을 받는 칸을 건드린 것**만 고른다.
18
+ *
19
+ * ⚠ **새로 만드는 행은 안 묻는다**(`__dirty__` 가 `+` 다). 그 행은 그 순간 생긴 것이고 「언제부터」는
20
+ * 그 순간이다. 물으면 존재하기 전의 자리를 적을 수 있게 된다.
21
+ *
22
+ * ⚠ **안 바뀐 칸은 애초에 `__dirtyfields__` 에 없다.** 그리드가 이미 가려 준다.
23
+ */
24
+ export function assignmentChangesIn(axis, patches) {
25
+ const fields = claimedFieldsOf(axis);
26
+ if (!fields.length) {
27
+ return [];
28
+ }
29
+ const changes = [];
30
+ for (const patch of patches) {
31
+ /*
32
+ * **그리드가 무엇이 바뀌었는지 이미 들고 있다.** `__dirtyfields__` 에 칸마다 before·after 가
33
+ * 있고, 바뀐 행은 `__dirty__` 가 `M` 이다.
34
+ *
35
+ * ⚠ 처음에 `cuFlag` 를 봤다 — 그것은 서버로 보낼 때 붙는 이름이고 **이 시점에는 없다.** 그래서
36
+ * 창이 영영 안 떴다. 화면을 열어 보기 전에는 안 보였다(2026-09-22).
37
+ */
38
+ if (String(patch.__dirty__ ?? '').toUpperCase() !== 'M') {
39
+ continue;
40
+ }
41
+ const dirty = patch.__dirtyfields__;
42
+ for (const field of fields) {
43
+ const moved = dirty?.[field];
44
+ if (!moved) {
45
+ continue;
46
+ }
47
+ const before = show(moved.before);
48
+ const after = show(moved.after);
49
+ if (before !== after) {
50
+ changes.push({ rowName: String(patch.name ?? patch.id), field, before, after });
51
+ }
52
+ }
53
+ }
54
+ return changes;
55
+ }
56
+ function show(value) {
57
+ if (value === null || value === undefined || value === '') {
58
+ return '';
59
+ }
60
+ return Array.isArray(value) ? value.map(String).join(', ') : String(value);
61
+ }
62
+ //# sourceMappingURL=assignment-fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignment-fields.js","sourceRoot":"","sources":["../../../client/pages/master/assignment-fields.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsC;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,EAAE;IAC5C,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACxD,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,UAAU,EAAE;CACnD,CAAA;AAED,uBAAuB;AACvB,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;AAC9E,CAAC;AASD;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,OAA2C;IAC3F,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IAEpC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,OAAO,GAAuB,EAAE,CAAA;IAEtC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B;;;;;;WAMG;QACH,IAAI,MAAM,CAAE,KAAa,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,GAAG,EAAE,CAAC;YACjE,SAAQ;QACV,CAAC;QAED,MAAM,KAAK,GAAI,KAAa,CAAC,eAAoF,CAAA;QAEjH,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;YAE5B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,SAAQ;YACV,CAAC;YAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;YACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAE/B,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;gBACrB,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;YACjF,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,IAAI,CAAC,KAAc;IAC1B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAC1D,OAAO,EAAE,CAAA;IACX,CAAC;IAED,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC5E,CAAC","sourcesContent":["/**\n * **이력을 받는 칸 셋** — 화면이 저장 직전에 「언제부터」를 묻는 대상.\n *\n * 서버의 `CLAIMED_FIELDS` 와 같은 목록이다. 화면이 서버를 import 하지 않으므로(그러면 빌드가\n * 옮겨 간다) 여기 한 벌을 둔다 — **갈라지면 안 묻고 보내는 칸이 생긴다.**\n */\nexport const CLAIMED_FIELDS: { axis: string; field: string }[] = [\n { axis: 'equipment', field: 'homeLocation' },\n { axis: 'operationalLocation', field: 'workShiftNames' },\n { axis: 'operationalLocation', field: 'parentId' }\n]\n\n/** 이 축에서 이력을 받는 칸들. */\nexport function claimedFieldsOf(axis: string): string[] {\n return CLAIMED_FIELDS.filter(one => one.axis === axis).map(one => one.field)\n}\n\nexport interface AssignmentChange {\n rowName: string\n field: string\n before: string\n after: string\n}\n\n/**\n * 바뀐 것 중 **이력을 받는 칸을 건드린 것**만 고른다.\n *\n * ⚠ **새로 만드는 행은 안 묻는다**(`__dirty__` 가 `+` 다). 그 행은 그 순간 생긴 것이고 「언제부터」는\n * 그 순간이다. 물으면 존재하기 전의 자리를 적을 수 있게 된다.\n *\n * ⚠ **안 바뀐 칸은 애초에 `__dirtyfields__` 에 없다.** 그리드가 이미 가려 준다.\n */\nexport function assignmentChangesIn(axis: string, patches: readonly Record<string, unknown>[]): AssignmentChange[] {\n const fields = claimedFieldsOf(axis)\n\n if (!fields.length) {\n return []\n }\n\n const changes: AssignmentChange[] = []\n\n for (const patch of patches) {\n /*\n * **그리드가 무엇이 바뀌었는지 이미 들고 있다.** `__dirtyfields__` 에 칸마다 before·after 가\n * 있고, 바뀐 행은 `__dirty__` 가 `M` 이다.\n *\n * ⚠ 처음에 `cuFlag` 를 봤다 — 그것은 서버로 보낼 때 붙는 이름이고 **이 시점에는 없다.** 그래서\n * 창이 영영 안 떴다. 화면을 열어 보기 전에는 안 보였다(2026-09-22).\n */\n if (String((patch as any).__dirty__ ?? '').toUpperCase() !== 'M') {\n continue\n }\n\n const dirty = (patch as any).__dirtyfields__ as Record<string, { before?: unknown; after?: unknown }> | undefined\n\n for (const field of fields) {\n const moved = dirty?.[field]\n\n if (!moved) {\n continue\n }\n\n const before = show(moved.before)\n const after = show(moved.after)\n\n if (before !== after) {\n changes.push({ rowName: String(patch.name ?? patch.id), field, before, after })\n }\n }\n }\n\n return changes\n}\n\nfunction show(value: unknown): string {\n if (value === null || value === undefined || value === '') {\n return ''\n }\n\n return Array.isArray(value) ? value.map(String).join(', ') : String(value)\n}\n"]}
@@ -13,6 +13,7 @@ let OpsMasterEquipment = class OpsMasterEquipment extends MasterPageBase {
13
13
  get spec() {
14
14
  return {
15
15
  key: 'equipment',
16
+ axis: 'equipment',
16
17
  title: 'equipment',
17
18
  help: 'ops-master/equipment',
18
19
  query: 'equipmentList',
@@ -1 +1 @@
1
- {"version":3,"file":"equipment.js","sourceRoot":"","sources":["../../../client/pages/master/equipment.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAc,MAAM,uBAAuB,CAAA;AAEvF;;;;;;;GAOG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,cAAc;IACpD,IAAc,IAAI;QAChB,OAAO;YACL,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE,sBAAsB;YAC5B,KAAK,EAAE,eAAe;YACtB,QAAQ,EAAE,yBAAyB;YACnC,OAAO,EAAE;gBACP,GAAG,QAAQ;gBACX,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE;gBACtE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE;gBACnE;oBACE,IAAI,EAAE,cAAc;oBACpB,KAAK,EAAE,eAAe;oBACtB,IAAI,EAAE,WAAW;oBACjB,SAAS,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;oBAChD,KAAK,EAAE,GAAG;iBACX;gBACD;;;;;;;;;;;;;;;mBAeG;gBACH,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC1F,GAAG,SAAS;aACb;SACF,CAAA;IACH,CAAC;CACF,CAAA;AAxCY,kBAAkB;IAD9B,aAAa,CAAC,sBAAsB,CAAC;GACzB,kBAAkB,CAwC9B","sourcesContent":["import { customElement } from 'lit/decorators.js'\n\nimport { EFFECTIVE, IDENTITY, MasterPageBase, MasterSpec } from './master-page-base.js'\n\n/**\n * 실제로 돌아가는 설비 하나하나. 실적과 정지 기록이 이것에 붙는다.\n *\n * 근무 캘린더를 정해야 계획 조업 시간이 생기고, 그것이 없으면 가동률이 「모름」이 된다.\n *\n * 화면 골격과 저장은 MasterPageBase 가 갖는다. 이 파일은 **이 화면이 무엇을 보이나**만 말한다 —\n * 자기 인터페이스가 필요해지면 여기서 render 를 덮어쓴다.\n */\n@customElement('ops-master-equipment')\nexport class OpsMasterEquipment extends MasterPageBase {\n protected get spec(): MasterSpec {\n return {\n key: 'equipment',\n title: 'equipment',\n help: 'ops-master/equipment',\n query: 'equipmentList',\n mutation: 'updateMultipleEquipment',\n columns: [\n ...IDENTITY,\n { name: 'kind', label: 'equipment-class', type: 'string', width: 200 },\n { name: 'identity', label: 'identity', type: 'string', width: 200 },\n {\n name: 'homeLocation',\n label: 'home-location',\n type: 'reference',\n reference: { queryName: 'operationalLocations' },\n width: 140\n },\n /*\n * ── 손으로 칠 수 있는 값이 아니다 ─────────────────────────────────\n *\n * `type: 'json'` 은 글자를 그대로 받아 `JSON.parse` 하고, 실패하면 **오류 없이 `null`**\n * 로 둔다(`data-grid-body.js`). 그래서 이 칸을 고치려면 사람이 이런 것을 손으로 쳐야\n * 했고, 오타 하나에 그 설비의 계획 가동 시간이 조용히 사라졌다.\n *\n * [{\"id\":\"A조\",\"fromDayOffset\":0,\"fromTime\":\"06:00\",\"toDayOffset\":0,\"toTime\":\"14:00\"}]\n *\n * 그리고 **정상은 이 칸이 비는 것**이다. 근무조는 자리가 선언하고 설비가 물려받는다\n * (`OperationalLocation.workShiftNames`). 이 칸은 「이 설비만 따로 이렇게 가동한다」는\n * 예외를 적는 자리다 — 예외가 없는 설비에 값이 있으면 화면이 거짓말을 한다.\n *\n * 읽기 전용으로 둔다. 값이 있으면 보이고, 고치는 길은 예외를 제대로 세울 때 만든다.\n * 지금 상태로 열어 두는 것은 고칠 수 있는 척하면서 자료를 잃는 것이다.\n */\n { name: 'workCalendar', label: 'work-calendar', type: 'json', width: 200, readonly: true },\n ...EFFECTIVE\n ]\n }\n }\n}\n"]}
1
+ {"version":3,"file":"equipment.js","sourceRoot":"","sources":["../../../client/pages/master/equipment.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAc,MAAM,uBAAuB,CAAA;AAEvF;;;;;;;GAOG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,cAAc;IACpD,IAAc,IAAI;QAChB,OAAO;YACL,GAAG,EAAE,WAAW;YAChB,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE,sBAAsB;YAC5B,KAAK,EAAE,eAAe;YACtB,QAAQ,EAAE,yBAAyB;YACnC,OAAO,EAAE;gBACP,GAAG,QAAQ;gBACX,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE;gBACtE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE;gBACnE;oBACE,IAAI,EAAE,cAAc;oBACpB,KAAK,EAAE,eAAe;oBACtB,IAAI,EAAE,WAAW;oBACjB,SAAS,EAAE,EAAE,SAAS,EAAE,sBAAsB,EAAE;oBAChD,KAAK,EAAE,GAAG;iBACX;gBACD;;;;;;;;;;;;;;;mBAeG;gBACH,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC1F,GAAG,SAAS;aACb;SACF,CAAA;IACH,CAAC;CACF,CAAA;AAzCY,kBAAkB;IAD9B,aAAa,CAAC,sBAAsB,CAAC;GACzB,kBAAkB,CAyC9B","sourcesContent":["import { customElement } from 'lit/decorators.js'\n\nimport { EFFECTIVE, IDENTITY, MasterPageBase, MasterSpec } from './master-page-base.js'\n\n/**\n * 실제로 돌아가는 설비 하나하나. 실적과 정지 기록이 이것에 붙는다.\n *\n * 근무 캘린더를 정해야 계획 조업 시간이 생기고, 그것이 없으면 가동률이 「모름」이 된다.\n *\n * 화면 골격과 저장은 MasterPageBase 가 갖는다. 이 파일은 **이 화면이 무엇을 보이나**만 말한다 —\n * 자기 인터페이스가 필요해지면 여기서 render 를 덮어쓴다.\n */\n@customElement('ops-master-equipment')\nexport class OpsMasterEquipment extends MasterPageBase {\n protected get spec(): MasterSpec {\n return {\n key: 'equipment',\n axis: 'equipment',\n title: 'equipment',\n help: 'ops-master/equipment',\n query: 'equipmentList',\n mutation: 'updateMultipleEquipment',\n columns: [\n ...IDENTITY,\n { name: 'kind', label: 'equipment-class', type: 'string', width: 200 },\n { name: 'identity', label: 'identity', type: 'string', width: 200 },\n {\n name: 'homeLocation',\n label: 'home-location',\n type: 'reference',\n reference: { queryName: 'operationalLocations' },\n width: 140\n },\n /*\n * ── 손으로 칠 수 있는 값이 아니다 ─────────────────────────────────\n *\n * `type: 'json'` 은 글자를 그대로 받아 `JSON.parse` 하고, 실패하면 **오류 없이 `null`**\n * 로 둔다(`data-grid-body.js`). 그래서 이 칸을 고치려면 사람이 이런 것을 손으로 쳐야\n * 했고, 오타 하나에 그 설비의 계획 가동 시간이 조용히 사라졌다.\n *\n * [{\"id\":\"A조\",\"fromDayOffset\":0,\"fromTime\":\"06:00\",\"toDayOffset\":0,\"toTime\":\"14:00\"}]\n *\n * 그리고 **정상은 이 칸이 비는 것**이다. 근무조는 자리가 선언하고 설비가 물려받는다\n * (`OperationalLocation.workShiftNames`). 이 칸은 「이 설비만 따로 이렇게 가동한다」는\n * 예외를 적는 자리다 — 예외가 없는 설비에 값이 있으면 화면이 거짓말을 한다.\n *\n * 읽기 전용으로 둔다. 값이 있으면 보이고, 고치는 길은 예외를 제대로 세울 때 만든다.\n * 지금 상태로 열어 두는 것은 고칠 수 있는 척하면서 자료를 잃는 것이다.\n */\n { name: 'workCalendar', label: 'work-calendar', type: 'json', width: 200, readonly: true },\n ...EFFECTIVE\n ]\n }\n }\n}\n"]}
@@ -3,6 +3,9 @@ import '@operato/data-grist/ox-grist.js';
3
3
  import { DataGrist, FetchOption } from '@operato/data-grist';
4
4
  import type { ReferenceOptions } from './grist-reference.js';
5
5
  import { PageView } from '@operato/shell';
6
+ import { AssignmentChange } from './assignment-fields.js';
7
+ import { AppliedAnswer } from './applied-when-dialog.js';
8
+ import './applied-when-dialog.js';
6
9
  /**
7
10
  * 마스터 화면이 함께 쓰는 것 — **코드 재사용은 여기서, 화면은 각자.**
8
11
  *
@@ -94,6 +97,13 @@ export interface MasterColumn {
94
97
  export interface MasterSpec {
95
98
  /** 컬럼 설정을 사람마다 따로 저장하는 키. 화면마다 달라야 한다. */
96
99
  key: string;
100
+ /**
101
+ * 이 화면이 다루는 마스터 축.
102
+ *
103
+ * **배정·선언 칸이 있는 축만 「언제부터」를 묻는다**(§`assignment-fields`). 안 적으면 안 묻는다 —
104
+ * 그 축에는 이력을 받는 칸이 없다는 뜻이다.
105
+ */
106
+ axis?: string;
97
107
  /** 제목 문구 키. `title.<이것>`. */
98
108
  title: string;
99
109
  /**
@@ -352,6 +362,8 @@ export declare abstract class MasterPageBase extends MasterPageBase_base {
352
362
  private headerIndex;
353
363
  /** 이 이름들이 이미 있나 — 있으면 그 `id`. 없는 이름은 지도에 없다. */
354
364
  private idsByName;
365
+ /** 「언제부터」를 묻는다. 사람이 그만두면 `undefined` 이고, 그때는 **아무것도 안 보낸다.** */
366
+ protected askAppliedWhen(changes: AssignmentChange[]): Promise<AppliedAnswer | undefined>;
355
367
  protected save(): Promise<void>;
356
368
  }
357
369
  export {};
@@ -7,12 +7,14 @@ import { property, query, state } from 'lit/decorators.js';
7
7
  import { DataGrist, getEditor } from '@operato/data-grist';
8
8
  import { client } from '@operato/graphql';
9
9
  import { i18next, localize } from '@operato/i18n';
10
- import { notify } from '@operato/layout';
10
+ import { notify, openPopup } from '@operato/layout';
11
11
  import { p13n } from '@operato/p13n';
12
12
  import { PageView } from '@operato/shell';
13
13
  import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
14
14
  import { isMobileDevice } from '@operato/utils';
15
15
  import { patchesFor, readImportRows } from './import-rows.js';
16
+ import { assignmentChangesIn } from './assignment-fields.js';
17
+ import './applied-when-dialog.js';
16
18
  /** 열두 축이 공통으로 갖는 유효 기간 — ISA-95. */
17
19
  export const EFFECTIVE = [
18
20
  { name: 'effectiveStart', label: 'effective-start', type: 'date', width: 150 },
@@ -554,6 +556,22 @@ export class MasterPageBase extends p13n(localize(i18next)(PageView)) {
554
556
  });
555
557
  return new Map((response.data?.[this.spec.query]?.items ?? []).map((item) => [item.name, item.id]));
556
558
  }
559
+ /** 「언제부터」를 묻는다. 사람이 그만두면 `undefined` 이고, 그때는 **아무것도 안 보낸다.** */
560
+ askAppliedWhen(changes) {
561
+ return new Promise(resolve => {
562
+ const popup = openPopup(html `<applied-when-dialog
563
+ .changes=${changes}
564
+ .onAnswer=${(answer) => {
565
+ popup?.close?.();
566
+ resolve(answer);
567
+ }}
568
+ .onCancel=${() => {
569
+ popup?.close?.();
570
+ resolve(undefined);
571
+ }}
572
+ ></applied-when-dialog>`, { backdrop: true, size: 'medium', title: i18next.t('title.when-does-this-apply') });
573
+ });
574
+ }
557
575
  async save() {
558
576
  const patches = this.grist.dirtyRecords;
559
577
  if (!patches?.length) {
@@ -566,6 +584,22 @@ export class MasterPageBase extends p13n(localize(i18next)(PageView)) {
566
584
  notify({ message: packed.problems.join('\n'), level: 'error' });
567
585
  return;
568
586
  }
587
+ /*
588
+ * ── 배정과 선언은 「언제부터」를 묻고 보낸다 ─────────────────────────────
589
+ *
590
+ * 자리 배정 · 근무조 선언 · 부모 관계는 가동률 분모를 정한다. 그래서 값만 바꾸는 것이 아니라
591
+ * **언제부터 그렇게 볼 것인가**를 함께 정해야 하고, 과거를 적으면 지연 기록인지 정정인지까지
592
+ * 갈라야 한다(§`applied-when-dialog`).
593
+ *
594
+ * 안 물어도 저장은 된다 — 그때는 「지금부터」라는 주장이 된다. 다만 **묻지 않으면 사람이 그것을
595
+ * 주장한 줄 모른다.**
596
+ */
597
+ const assignments = assignmentChangesIn(this.spec.axis ?? '', patches);
598
+ const applied = assignments.length ? await this.askAppliedWhen(assignments) : undefined;
599
+ /* 그만두었으면 아무것도 안 보낸다 — 절반만 저장되면 화면과 저장된 것이 달라진다. */
600
+ if (assignments.length && !applied) {
601
+ return;
602
+ }
569
603
  const response = await client.mutate({
570
604
  /*
571
605
  * **타입 이름은 `Object` 다.**
@@ -576,14 +610,14 @@ export class MasterPageBase extends p13n(localize(i18next)(PageView)) {
576
610
  * 않았다 — 이 앱의 클라이언트는 서버가 거절해도 던지지 않는다.
577
611
  */
578
612
  mutation: gql `
579
- mutation ($patches: [Object!]!) {
580
- ${this.spec.mutation}(patches: $patches) {
613
+ mutation ($patches: [Object!]!, $applied: Object) {
614
+ ${this.spec.mutation}(patches: $patches, applied: $applied) {
581
615
  id
582
616
  name
583
617
  }
584
618
  }
585
619
  `,
586
- variables: { patches: packed.patches }
620
+ variables: { patches: packed.patches, applied }
587
621
  });
588
622
  /*
589
623
  * 이 클라이언트는 서버가 거절해도 던지지 않는다 — 오류는 `response.errors` 에 담겨 온다.