@things-factory/organization 10.1.26 → 10.1.28

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 (57) hide show
  1. package/dist-client/component/approval-line-items-editor-popup.js +3 -3
  2. package/dist-client/component/approval-line-items-editor-popup.js.map +1 -1
  3. package/dist-client/component/approval-line-items-editor.js +3 -3
  4. package/dist-client/component/approval-line-items-editor.js.map +1 -1
  5. package/dist-client/component/approval-line-selector.js +2 -2
  6. package/dist-client/component/approval-line-selector.js.map +1 -1
  7. package/dist-client/component/approval-line-templates-manager.js +5 -6
  8. package/dist-client/component/approval-line-templates-manager.js.map +1 -1
  9. package/dist-client/component/assignees-editor-popup.js +2 -2
  10. package/dist-client/component/assignees-editor-popup.js.map +1 -1
  11. package/dist-client/component/assignees-editor.js +3 -3
  12. package/dist-client/component/assignees-editor.js.map +1 -1
  13. package/dist-client/component/department-selector.js +2 -2
  14. package/dist-client/component/department-selector.js.map +1 -1
  15. package/dist-client/component/recipients-editor-popup.js +2 -2
  16. package/dist-client/component/recipients-editor-popup.js.map +1 -1
  17. package/dist-client/component/recipients-editor.js +3 -3
  18. package/dist-client/component/recipients-editor.js.map +1 -1
  19. package/dist-client/filters-form/ox-filter-department-object.js +1 -1
  20. package/dist-client/filters-form/ox-filter-department-object.js.map +1 -1
  21. package/dist-client/grist-editor/grist-editor-department-object.js +1 -1
  22. package/dist-client/grist-editor/grist-editor-department-object.js.map +1 -1
  23. package/dist-client/pages/approval-line/approval-line-list-page.d.ts +0 -3
  24. package/dist-client/pages/approval-line/approval-line-list-page.js +6 -18
  25. package/dist-client/pages/approval-line/approval-line-list-page.js.map +1 -1
  26. package/dist-client/pages/approval-line/common-approval-line-templates-page.d.ts +0 -3
  27. package/dist-client/pages/approval-line/common-approval-line-templates-page.js +10 -18
  28. package/dist-client/pages/approval-line/common-approval-line-templates-page.js.map +1 -1
  29. package/dist-client/pages/approval-line/my-approval-line-templates-page.d.ts +0 -3
  30. package/dist-client/pages/approval-line/my-approval-line-templates-page.js +10 -18
  31. package/dist-client/pages/approval-line/my-approval-line-templates-page.js.map +1 -1
  32. package/dist-client/pages/department/department-list-page.d.ts +5 -1
  33. package/dist-client/pages/department/department-list-page.js +41 -29
  34. package/dist-client/pages/department/department-list-page.js.map +1 -1
  35. package/dist-client/pages/employee/employee-list-page.d.ts +18 -0
  36. package/dist-client/pages/employee/employee-list-page.js +437 -229
  37. package/dist-client/pages/employee/employee-list-page.js.map +1 -1
  38. package/dist-client/route.d.ts +1 -1
  39. package/dist-client/route.js +0 -6
  40. package/dist-client/route.js.map +1 -1
  41. package/dist-client/tsconfig.tsbuildinfo +1 -1
  42. package/dist-server/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +9 -6
  44. package/tests/department-query-shape.test.ts +130 -0
  45. package/tests/screen-words.test.ts +110 -0
  46. package/things-factory.config.js +0 -2
  47. package/translations/en.json +11 -4
  48. package/translations/ja.json +11 -4
  49. package/translations/ko.json +11 -4
  50. package/translations/ms.json +12 -4
  51. package/translations/zh.json +11 -4
  52. package/dist-client/pages/department/department-tree-page.d.ts +0 -43
  53. package/dist-client/pages/department/department-tree-page.js +0 -336
  54. package/dist-client/pages/department/department-tree-page.js.map +0 -1
  55. package/dist-client/pages/employee/employees-by-department.d.ts +0 -32
  56. package/dist-client/pages/employee/employees-by-department.js +0 -435
  57. package/dist-client/pages/employee/employees-by-department.js.map +0 -1
@@ -1,116 +1,318 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import '@operato/data-grist';
3
+ import '@operato/data-tree';
4
+ import '@operato/list-state/ox-empty-note.js';
5
+ import '@operato/list-state/ox-page-state.js';
3
6
  import '@things-factory/contact/dist-client';
4
7
  import gql from 'graphql-tag';
5
8
  import { css, html } from 'lit';
6
- import { customElement, property, query } from 'lit/decorators.js';
9
+ import { customElement, property, query, state } from 'lit/decorators.js';
7
10
  import { ScopedElementsMixin } from '@open-wc/scoped-elements';
11
+ import { OxPrompt } from '@operato/popup';
8
12
  import { DataGrist } from '@operato/data-grist';
9
13
  import { client } from '@operato/graphql';
10
14
  import { i18next, localize } from '@operato/i18n';
11
15
  import { notify, openPopup } from '@operato/layout';
12
16
  import { PageView } from '@operato/shell';
13
- import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles';
17
+ import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
14
18
  import { isMobileDevice } from '@operato/utils';
15
19
  import { p13n } from '@operato/p13n';
20
+ import { Department } from '../../types/department';
16
21
  import { EmployeeImporter } from './employee-importer';
17
22
  let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(ScopedElementsMixin(PageView))) {
18
23
  constructor() {
19
24
  super(...arguments);
20
25
  this.mode = isMobileDevice() ? 'CARD' : 'GRID';
26
+ this.departmentsLoaded = false;
27
+ this.departmentsError = null;
28
+ this.railFolded = false;
21
29
  }
22
30
  static { this.styles = [
31
+ /* The work surface frame for a list screen (ADR-0060 decision 8). */
32
+ WorkSurfacePageStyles,
23
33
  ScrollbarStyles,
24
34
  CommonGristStyles,
25
35
  CommonHeaderStyles,
26
36
  css `
27
- :host { display: flex;
28
-
29
- width: 100%;
30
-
37
+ :host {
31
38
  --ox-pfp-size: 40px;
32
-
33
39
  --grid-record-emphasized-background-color: #8b0000;
34
40
  --grid-record-emphasized-color: #ff6b6b;
35
- }
41
+ }
42
+
43
+ .header {
44
+ grid-template-areas: 'filters';
45
+ }
46
+
47
+ /*
48
+ * The rail is how you narrow the list to one department; the list is the content
49
+ * (ADR-0060 decision 8, and the ruling of 2026-09-18 that folded the by-department screen
50
+ * into this one). It folds away, because a screen looking at everyone does not need it.
51
+ */
52
+ [body] {
53
+ flex: 1;
54
+ display: flex;
55
+ flex-direction: row;
56
+ min-height: 0;
57
+ }
58
+
59
+ [rail] {
60
+ display: flex;
61
+ flex-direction: column;
62
+ width: 240px;
63
+ min-width: 240px;
64
+ border-right: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
65
+ overflow: auto;
66
+ }
67
+
68
+ [rail][folded] {
69
+ display: none;
70
+ }
71
+
72
+ [rail-head] {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: var(--spacing-small, 4px);
76
+ padding: var(--spacing-medium, 8px) var(--spacing-medium, 8px) 0;
77
+ font: var(--label-font, inherit);
78
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
79
+ }
80
+
81
+ /* The word keeps its line. Sharing the row with a filler let it shrink and break in half. */
82
+ [rail-head] > span {
83
+ flex: 0 0 auto;
84
+ white-space: nowrap;
85
+ }
36
86
 
37
- ox-grist { overflow-y: auto;
87
+ [rail-head] button,
88
+ /*
89
+ * Going back to everyone. Picking a department narrows the list, and without this there is no
90
+ * way out of that except reloading the screen.
91
+ */
92
+ button[all-departments] {
93
+ width: calc(100% - 16px);
94
+ margin: 4px 8px;
95
+ padding: 6px 10px;
96
+ text-align: left;
97
+ border: 1px solid transparent;
98
+ border-radius: var(--md-sys-shape-corner-small, 6px);
99
+ background: transparent;
100
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
101
+ font: inherit;
102
+ cursor: pointer;
103
+ }
104
+
105
+ button[all-departments][selected] {
106
+ background: var(--md-sys-color-secondary-container);
107
+ color: var(--md-sys-color-on-secondary-container);
108
+ }
109
+
110
+ [rail-toggle] {
111
+ display: flex;
112
+ align-items: center;
113
+ border: none;
114
+ background: transparent;
115
+ color: inherit;
116
+ cursor: pointer;
117
+ padding: 2px;
118
+ }
119
+
120
+ [rail-toggle] {
121
+ align-self: flex-start;
122
+ margin: var(--spacing-medium, 8px) 0 0 var(--spacing-small, 4px);
123
+ }
124
+
125
+ ox-tree {
126
+ flex: 1;
127
+ overflow: auto;
128
+ }
129
+
130
+ ox-grist {
38
131
  flex: 1;
39
- }
132
+ min-width: 0;
133
+ }
40
134
 
41
- .header { grid-template-areas: 'filters';
42
- }
135
+ @media screen and (max-width: 780px) {
136
+ [rail] {
137
+ display: none;
138
+ }
139
+ }
43
140
  `
44
141
  ]; }
45
142
  static get scopedElements() {
46
- return { 'employee-importer': EmployeeImporter
47
- };
143
+ return { 'employee-importer': EmployeeImporter };
48
144
  }
49
145
  get context() {
50
- return { title: i18next.t('title.employee list'),
51
- search: { handler: (search) => {
146
+ return {
147
+ title: i18next.t('title.employee list'),
148
+ search: {
149
+ handler: (search) => {
52
150
  this.grist.searchText = search;
53
151
  },
54
- value: this.grist?.searchText || '' },
55
- filter: { handler: () => {
152
+ value: this.grist?.searchText || ''
153
+ },
154
+ filter: {
155
+ handler: () => {
56
156
  this.grist.toggleHeadroom();
57
157
  }
58
158
  },
59
159
  help: 'organization/employee',
60
160
  actions: [
61
- { icon: 'save',
62
- title: i18next.t('button.save'),
63
- action: this.onUpdateEmployee.bind(this)
64
- },
65
- { icon: 'delete',
161
+ { icon: 'save', title: i18next.t('button.save'), action: this.onUpdateEmployee.bind(this) },
162
+ {
163
+ icon: 'delete',
66
164
  title: i18next.t('button.delete'),
67
165
  action: this.onDeleteEmployee.bind(this),
68
- emphasis: { danger: true
69
- }
166
+ emphasis: { danger: true }
70
167
  }
71
168
  ],
72
- exportable: { name: i18next.t('title.employee list'),
73
- data: this.exportHandler.bind(this)
74
- },
75
- importable: { handler: this.importHandler.bind(this)
76
- } };
169
+ exportable: { name: i18next.t('title.employee list'), data: this.exportHandler.bind(this) },
170
+ importable: { handler: this.importHandler.bind(this) }
171
+ };
77
172
  }
78
173
  render() {
79
174
  const mode = this.mode || (isMobileDevice() ? 'CARD' : 'GRID');
80
175
  return html `
81
- <ox-grist
82
- .mode=${mode}
83
- .config=${this.gristConfig}
84
- .fetchHandler=${this.fetchHandler.bind(this)}
85
- .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
86
- >
87
- <div slot="headroom" class="header">
88
- <div class="filters">
89
- <ox-filters-form class="filter" autofocus without-search></ox-filters-form>
176
+ <div body class="page-fill">
177
+ ${this.railFolded
178
+ ? html `<button
179
+ rail-toggle
180
+ title=${i18next.t('title.department list')}
181
+ @click=${() => (this.railFolded = false)}
182
+ >
183
+ <md-icon>account_tree</md-icon>
184
+ </button>`
185
+ : html `<aside rail>
186
+ <div rail-head>
187
+ <span>${i18next.t('title.department list')}</span>
188
+ <div filler style="flex: 1"></div>
189
+ <button title=${i18next.t('label.fold')} @click=${() => (this.railFolded = true)}>
190
+ <md-icon>chevron_left</md-icon>
191
+ </button>
192
+ </div>
193
+ <ox-page-state
194
+ .loading=${!this.departmentsLoaded && !this.departmentsError}
195
+ .error=${this.departmentsError}
196
+ @retry=${() => this.fetchDepartments()}
197
+ >
198
+ ${this.departments?.length
199
+ ? html `<button
200
+ all-departments
201
+ ?selected=${!this.selectedDepartment}
202
+ @click=${() => this.onSelectDepartment(undefined)}
203
+ >
204
+ ${i18next.t('text.all departments')}
205
+ </button>
206
+ <ox-tree
207
+ .data=${this.departments}
208
+ .selected=${this.selectedDepartment}
209
+ @select=${(e) => this.onSelectDepartment(e.detail)}
210
+ id-property="controlNo"
211
+ label-property="name"
212
+ ></ox-tree>`
213
+ : html `<ox-empty-note
214
+ icon="account_tree"
215
+ title=${i18next.t('text.no departments yet')}
216
+ description=${i18next.t('text.departments are made on the department screen')}
217
+ ></ox-empty-note>`}
218
+ </ox-page-state>
219
+ </aside>`}
220
+
221
+ <ox-grist
222
+ .mode=${mode}
223
+ .config=${this.gristConfig}
224
+ .fetchHandler=${this.fetchHandler.bind(this)}
225
+ .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
226
+ >
227
+ <div slot="headroom" class="header">
228
+ <div class="filters">
229
+ <ox-filters-form class="filter" autofocus without-search></ox-filters-form>
230
+ </div>
90
231
  </div>
91
- </div>
92
232
 
93
- <ox-grist-personalizer slot="setting"></ox-grist-personalizer>
94
- </ox-grist>
233
+ <ox-grist-personalizer slot="setting"></ox-grist-personalizer>
234
+ </ox-grist>
235
+ </div>
95
236
  `;
96
237
  }
238
+ /**
239
+ * 트리에서 고른 부서가 목록의 부서 조건이다 — 조건은 하나고, 그것을 고르는 자리도 하나다.
240
+ */
241
+ onSelectDepartment(department) {
242
+ this.selectedDepartment = department;
243
+ this.grist?.fetch();
244
+ this.updateContext();
245
+ }
246
+ /**
247
+ * The departments the rail draws. departmentRoots answers with a list, and the tree draws from
248
+ * the first root - a fragment spread straight onto the list is refused by the server.
249
+ */
250
+ async fetchDepartments() {
251
+ this.departmentsError = null;
252
+ try {
253
+ const response = await client.query({
254
+ query: gql `
255
+ query {
256
+ responses: departmentRoots {
257
+ items {
258
+ ...EmployeeDepartmentFragment
259
+ children {
260
+ ...EmployeeDepartmentFragment
261
+ children {
262
+ ...EmployeeDepartmentFragment
263
+ children {
264
+ ...EmployeeDepartmentFragment
265
+ children {
266
+ ...EmployeeDepartmentFragment
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ fragment EmployeeDepartmentFragment on Department {
276
+ id
277
+ controlNo
278
+ name
279
+ description
280
+ active
281
+ }
282
+ `
283
+ });
284
+ if (response.errors?.length) {
285
+ throw response.errors[0];
286
+ }
287
+ /* Every root department, not just the first - the rail draws an array. */
288
+ this.departments = response.data.responses?.items;
289
+ this.departmentsLoaded = true;
290
+ }
291
+ catch (error) {
292
+ this.departmentsError = error;
293
+ }
294
+ }
97
295
  async pageInitialized(lifecycle) {
98
- this.gristConfig = { pagination: { pages: [50, 100, 200] },
99
- list: { thumbnail: 'profile',
296
+ this.gristConfig = {
297
+ pagination: { pages: [50, 100, 200] },
298
+ list: {
299
+ thumbnail: 'profile',
100
300
  fields: ['controlNo', 'name'],
101
301
  details: ['email', 'department', 'hiredOn', 'updatedAt']
102
302
  },
103
303
  columns: [
104
304
  { type: 'gutter', gutterName: 'sequence', fixed: true },
105
305
  { type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true },
106
- { type: 'gutter',
306
+ {
307
+ type: 'gutter',
107
308
  gutterName: 'button',
108
309
  header: i18next.t('field.register-account'),
109
310
  icon: record => (record.id && !record.user ? 'badge' : ''),
110
311
  iconOnly: false,
111
312
  title: record => (record.id && !record.user ? i18next.t('button.register-account') : ''),
112
313
  width: 80,
113
- handlers: { click: (columns, data, column, record, rowIndex) => {
314
+ handlers: {
315
+ click: (columns, data, column, record, rowIndex) => {
114
316
  if (!record || !record.id || record.user) {
115
317
  return;
116
318
  }
@@ -118,16 +320,19 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
118
320
  }
119
321
  }
120
322
  },
121
- { type: 'image',
323
+ {
324
+ type: 'image',
122
325
  name: 'profile',
123
326
  header: i18next.t('button.edit-contact'),
124
327
  width: 80,
125
- record: { align: 'center',
328
+ record: {
329
+ align: 'center',
126
330
  renderer: function (value, column, record, rowIndex, field) {
127
331
  return html `<ox-pfp-view .profile=${record.profile} .name=${record.name || '+'}></ox-pfp-view>`;
128
332
  }
129
333
  },
130
- handlers: { click: async (columns, data, column, record, rowIndex) => {
334
+ handlers: {
335
+ click: async (columns, data, column, record, rowIndex) => {
131
336
  if (record && record.contact) {
132
337
  this.openContactPopup(record);
133
338
  }
@@ -137,42 +342,43 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
137
342
  }
138
343
  }
139
344
  },
140
- { type: 'string',
345
+ {
346
+ type: 'string',
141
347
  name: 'controlNo',
142
348
  fixed: true,
143
349
  header: i18next.t('field.control-no'),
144
- record: { editable: true,
145
- mandatory: true
146
- },
350
+ record: { editable: true, mandatory: true },
147
351
  filter: 'search',
148
352
  sortable: true,
149
353
  width: 105
150
354
  },
151
- { type: 'string',
355
+ {
356
+ type: 'string',
152
357
  name: 'name',
153
358
  fixed: true,
154
359
  header: i18next.t('field.name'),
155
- record: { editable: true,
156
- mandatory: true
157
- },
360
+ record: { editable: true, mandatory: true },
158
361
  filter: 'search',
159
362
  sortable: true,
160
363
  width: 100
161
364
  },
162
- { type: 'string',
365
+ {
366
+ type: 'string',
163
367
  name: 'alias',
164
368
  header: i18next.t('label.alias'),
165
- record: { editable: true
166
- },
369
+ record: { editable: true },
167
370
  filter: 'search',
168
371
  sortable: false,
169
372
  width: 110
170
373
  },
171
- { type: 'resource-object',
374
+ {
375
+ type: 'resource-object',
172
376
  name: 'user',
173
377
  header: i18next.t('field.system-user'),
174
- record: { editable: true,
175
- options: { title: i18next.t('title.lookup system-user'),
378
+ record: {
379
+ editable: true,
380
+ options: {
381
+ title: i18next.t('title.lookup system-user'),
176
382
  queryName: 'users',
177
383
  basicArgs: { filters: [{ name: 'userType', operator: 'eq', value: 'user' }] },
178
384
  descriptionField: 'email',
@@ -188,93 +394,82 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
188
394
  filter: 'search',
189
395
  width: 100
190
396
  },
191
- { type: 'code',
397
+ {
398
+ type: 'code',
192
399
  name: 'type',
193
- header: i18next.t('field.type'),
400
+ header: i18next.t('field.type', { defaultValue: 'type' }),
194
401
  width: 115,
195
402
  sortable: true,
196
403
  filter: true,
197
- record: { editable: true,
198
- codeName: 'EMPLOYEE_TYPE',
199
- selectDispOpt: 'name'
200
- }
404
+ record: { editable: true, codeName: 'EMPLOYEE_TYPE', selectDispOpt: 'name' }
201
405
  },
202
- { type: 'department-object',
406
+ {
407
+ type: 'department-object',
203
408
  name: 'department',
204
409
  header: i18next.t('field.department'),
205
- record: { editable: true
206
- },
410
+ record: { editable: true },
207
411
  sortable: false,
208
- filter: true,
412
+ /* The rail narrows by department; a second department field here would be the same condition twice. */
413
+ filter: false,
209
414
  width: 130
210
415
  },
211
- { type: 'resource-object',
416
+ {
417
+ type: 'resource-object',
212
418
  name: 'supervisor',
213
419
  header: i18next.t('field.supervisor'),
214
- record: { editable: true,
215
- options: { title: i18next.t('title.employee list'),
420
+ record: {
421
+ editable: true,
422
+ options: {
423
+ title: i18next.t('title.employee list'),
216
424
  queryName: 'employees',
217
425
  pagination: { pages: [50, 100, 200] },
218
- basicArgs: { filters: [
219
- { name: 'active',
220
- operator: 'eq',
221
- value: true
222
- }
223
- ]
224
- },
426
+ basicArgs: { filters: [{ name: 'active', operator: 'eq', value: true }] },
225
427
  list: { fields: ['controlNo', 'name', 'alias', 'hiredOn'] },
226
428
  columns: [
227
429
  { name: 'id', hidden: true },
228
- { name: 'controlNo',
430
+ {
431
+ name: 'controlNo',
229
432
  width: 120,
230
433
  header: { renderer: () => i18next.t('field.control-no') },
231
434
  filter: 'search',
232
435
  sortable: true
233
436
  },
234
- { name: 'name',
437
+ {
438
+ name: 'name',
235
439
  width: 120,
236
440
  header: { renderer: () => i18next.t('field.name') },
237
441
  filter: 'search',
238
442
  sortable: true
239
443
  },
240
- { name: 'alias',
444
+ {
445
+ name: 'alias',
241
446
  width: 150,
242
447
  header: { renderer: () => i18next.t('label.alias') },
243
448
  filter: 'search',
244
449
  sortable: true
245
450
  },
246
- { type: 'code',
451
+ {
452
+ type: 'code',
247
453
  name: 'type',
248
454
  width: 110,
249
- header: { renderer: () => i18next.t('label.type') },
250
- record: { editable: false,
251
- codeName: 'EMPLOYEE_TYPE',
252
- selectDispOpt: 'name'
253
- }
455
+ header: { renderer: () => i18next.t('label.type', { defaultValue: 'type' }) },
456
+ record: { editable: false, codeName: 'EMPLOYEE_TYPE', selectDispOpt: 'name' }
254
457
  },
255
- { type: 'code',
458
+ {
459
+ type: 'code',
256
460
  name: 'jobPosition',
257
461
  width: 110,
258
462
  header: { renderer: () => i18next.t('label.job-position') },
259
- record: { editable: false,
260
- codeName: 'JOB_POSITION',
261
- selectDispOpt: 'name'
262
- }
463
+ record: { editable: false, codeName: 'JOB_POSITION', selectDispOpt: 'name' }
263
464
  },
264
- { type: 'code',
465
+ {
466
+ type: 'code',
265
467
  name: 'jobResponsibility',
266
468
  width: 200,
267
469
  header: { renderer: () => i18next.t('label.job-responsibility') },
268
- record: { editable: false,
269
- codeName: 'JOB_RESPONSIBILITY',
270
- selectDispOpt: 'name'
271
- }
470
+ record: { editable: false, codeName: 'JOB_RESPONSIBILITY', selectDispOpt: 'name' }
272
471
  },
273
- { type: 'date',
274
- name: 'hiredOn',
275
- header: { renderer: () => i18next.t('field.hired-on') },
276
- width: 120
277
- }
472
+ { type: 'date', name: 'hiredOn', header: { renderer: () => i18next.t('field.hired-on') }, width: 120 }
278
473
  ],
279
474
  valueField: 'id',
280
475
  nameField: 'name',
@@ -284,115 +479,106 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
284
479
  sortable: false,
285
480
  width: 120
286
481
  },
287
- { type: 'string',
482
+ {
483
+ type: 'string',
288
484
  name: 'email',
289
485
  header: i18next.t('field.email'),
290
486
  width: 200,
291
- record: { editable: false,
487
+ record: {
488
+ editable: false,
292
489
  renderer: function (value, column, record, rowIndex, field) {
293
490
  return record.contact ? record.contact.email : '';
294
491
  }
295
492
  },
296
493
  sortable: false
297
494
  },
298
- { type: 'string',
495
+ {
496
+ type: 'string',
299
497
  name: 'phone',
300
498
  header: i18next.t('field.phone'),
301
499
  width: 130,
302
- record: { editable: false,
500
+ record: {
501
+ editable: false,
303
502
  renderer: function (value, column, record, rowIndex, field) {
304
503
  return record.contact ? record.contact.phone : '';
305
504
  }
306
505
  },
307
506
  sortable: false
308
507
  },
309
- { type: 'code',
508
+ {
509
+ type: 'code',
310
510
  name: 'jobResponsibility',
311
511
  header: i18next.t('label.job-responsibility'),
312
512
  width: 175,
313
- record: { editable: true,
314
- codeName: 'JOB_RESPONSIBILITY',
315
- selectDispOpt: 'name'
316
- },
513
+ record: { editable: true, codeName: 'JOB_RESPONSIBILITY', selectDispOpt: 'name' },
317
514
  filter: true
318
515
  },
319
- { type: 'code',
516
+ {
517
+ type: 'code',
320
518
  name: 'jobPosition',
321
519
  header: i18next.t('label.job-position'),
322
520
  width: 100,
323
- record: { editable: true,
324
- codeName: 'JOB_POSITION',
325
- selectDispOpt: 'name'
326
- },
521
+ record: { editable: true, codeName: 'JOB_POSITION', selectDispOpt: 'name' },
327
522
  filter: true
328
523
  },
329
- { type: 'date',
524
+ {
525
+ type: 'date',
330
526
  name: 'hiredOn',
331
527
  header: i18next.t('field.hired-on'),
332
528
  width: 120,
333
- record: { editable: true
334
- },
529
+ record: { editable: true },
335
530
  sortable: true
336
531
  },
337
- { type: 'date',
532
+ {
533
+ type: 'date',
338
534
  name: 'retiredOn',
339
535
  header: i18next.t('label.retired-at'),
340
536
  width: 120,
341
- record: { editable: true
342
- }
537
+ record: { editable: true }
343
538
  },
344
- { type: 'checkbox',
539
+ {
540
+ type: 'checkbox',
345
541
  name: 'active',
346
542
  label: true,
347
543
  header: i18next.t('field.active'),
348
544
  width: 70,
349
- record: { align: 'center',
350
- editable: true
351
- },
545
+ record: { align: 'center', editable: true },
352
546
  filter: true,
353
547
  sortable: false
354
548
  },
355
- { type: 'string',
549
+ {
550
+ type: 'string',
356
551
  name: 'note',
357
552
  header: i18next.t('field.note'),
358
553
  width: 200,
359
- record: { editable: true
360
- },
554
+ record: { editable: true },
361
555
  filter: 'search'
362
556
  },
363
- { type: 'resource-object',
364
- name: 'updater',
365
- header: i18next.t('field.updater'),
366
- width: 90,
367
- sortable: false
368
- },
369
- { type: 'datetime',
370
- name: 'updatedAt',
371
- header: i18next.t('field.updated_at'),
372
- width: 180,
373
- sortable: true
374
- },
375
- { type: 'resource-object',
376
- name: 'contact',
377
- hidden: true
378
- }
557
+ { type: 'resource-object', name: 'updater', header: i18next.t('field.updater'), width: 90, sortable: false },
558
+ { type: 'datetime', name: 'updatedAt', header: i18next.t('field.updated_at'), width: 180, sortable: true },
559
+ { type: 'resource-object', name: 'contact', hidden: true }
379
560
  ],
380
- rows: { selectable: { multiple: true
381
- }
382
- },
383
- sorters: [
384
- { name: 'controlNo'
385
- }
386
- ]
561
+ rows: { selectable: { multiple: true } },
562
+ sorters: [{ name: 'controlNo' }]
387
563
  };
564
+ this.fetchDepartments();
388
565
  }
389
566
  async pageUpdated(changes, lifecycle) {
390
- if (this.active) { // do something here when this page just became as active
567
+ if (this.active) {
568
+ // do something here when this page just became as active
391
569
  }
392
570
  }
393
571
  async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [] }) {
394
- const response = await client.query({ query: gql `
395
- query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) { responses: employees(filters: $filters, pagination: $pagination, sortings: $sortings) { items { id
572
+ /* The rail's selection is the department condition; nothing else sets one. */
573
+ if (this.selectedDepartment) {
574
+ filters = [...filters, { name: 'departmentId', operator: 'eq', value: this.selectedDepartment.id }];
575
+ }
576
+ const response = await client.query({
577
+ query: gql `
578
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
579
+ responses: employees(filters: $filters, pagination: $pagination, sortings: $sortings) {
580
+ items {
581
+ id
396
582
  controlNo
397
583
  name
398
584
  alias
@@ -402,65 +588,66 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
402
588
  active
403
589
  email
404
590
  phone
405
- user { id
591
+ user {
592
+ id
406
593
  name
407
- }
408
- department { id
594
+ }
595
+ department {
596
+ id
409
597
  controlNo
410
598
  name
411
599
  description
412
- }
413
- supervisor { id
600
+ }
601
+ supervisor {
602
+ id
414
603
  name
415
604
  controlNo
416
- }
605
+ }
417
606
  note
418
607
  hiredOn
419
608
  retiredOn
420
- contact { id
609
+ contact {
610
+ id
421
611
  email
422
612
  phone
423
613
  address
424
- }
425
- profile { left
614
+ }
615
+ profile {
616
+ left
426
617
  top
427
618
  zoom
428
619
  picture
429
- }
430
- updater { id
620
+ }
621
+ updater {
622
+ id
431
623
  name
432
- }
624
+ }
433
625
  updatedAt
434
- }
626
+ }
435
627
  total
436
- }
437
- }
628
+ }
629
+ }
438
630
  `,
439
- variables: { filters,
440
- pagination: { page, limit },
441
- sortings
442
- }
631
+ variables: { filters, pagination: { page, limit }, sortings }
443
632
  });
444
633
  const records = response.data.responses.items;
445
- return { total: response.data.responses.total || 0,
446
- records
447
- };
634
+ return { total: response.data.responses.total || 0, records };
448
635
  }
449
636
  async onDeleteEmployee() {
450
- if (confirm(i18next.t('text.sure_to_x', { x: i18next.t('text.delete') }))) {
637
+ if (await OxPrompt.confirmDelete({ count: this.grist?.selected?.length })) {
451
638
  const ids = this.grist.selected.map(record => record.id);
452
639
  if (ids && ids.length > 0) {
453
- const response = await client.mutate({ mutation: gql `
454
- mutation ($ids: [String!]!) { deleteEmployees(ids: $ids)
455
- }
640
+ const response = await client.mutate({
641
+ mutation: gql `
642
+ mutation ($ids: [String!]!) {
643
+ deleteEmployees(ids: $ids)
644
+ }
456
645
  `,
457
- variables: { ids
458
- }
646
+ variables: { ids }
459
647
  });
460
648
  if (!response.errors) {
461
649
  this.grist.fetch();
462
- notify({ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.delete') })
463
- });
650
+ notify({ message: i18next.t('text.deleted') });
464
651
  }
465
652
  }
466
653
  }
@@ -477,13 +664,15 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
477
664
  patchField.cuFlag = patch.__dirty__;
478
665
  return patchField;
479
666
  });
480
- const response = await client.mutate({ mutation: gql `
481
- mutation ($patches: [EmployeePatch!]!) { updateMultipleEmployee(patches: $patches) { name
482
- }
483
- }
667
+ const response = await client.mutate({
668
+ mutation: gql `
669
+ mutation ($patches: [EmployeePatch!]!) {
670
+ updateMultipleEmployee(patches: $patches) {
671
+ name
672
+ }
673
+ }
484
674
  `,
485
- variables: { patches
486
- }
675
+ variables: { patches }
487
676
  });
488
677
  if (!response.errors) {
489
678
  this.grist.fetch();
@@ -520,10 +709,7 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
520
709
  this.grist.fetch();
521
710
  }}
522
711
  ></employee-importer>
523
- `, { backdrop: true,
524
- size: 'large',
525
- title: i18next.t('title.import employee')
526
- });
712
+ `, { backdrop: true, size: 'large', title: i18next.t('title.import employee') });
527
713
  popup.onclosed = () => {
528
714
  this.grist.fetch();
529
715
  };
@@ -543,35 +729,32 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
543
729
  }}
544
730
  detachable
545
731
  ></contact-popup>
546
- `, { backdrop: true,
547
- size: 'large',
548
- title: i18next.t('title.contact')
549
- });
732
+ `, { backdrop: true, size: 'large', title: i18next.t('title.contact') });
550
733
  }
551
734
  async openContactSelector(record) {
552
735
  const popup = openPopup(html `
553
736
  <contact-selector .confirmCallback=${selected => this.attachContact(record, selected)}></contact-selector>
554
- `, { backdrop: true,
555
- size: 'large',
556
- title: i18next.t('title.contact')
557
- });
737
+ `, { backdrop: true, size: 'large', title: i18next.t('title.contact') });
558
738
  }
559
739
  async attachContact(record, contact) {
560
740
  if (record.id) {
561
- const response = await client.mutate({ mutation: gql `
562
- mutation ($id: String!, $contactId: String!) { attachContact(id: $id, contactId: $contactId) { id
563
- }
564
- }
741
+ const response = await client.mutate({
742
+ mutation: gql `
743
+ mutation ($id: String!, $contactId: String!) {
744
+ attachContact(id: $id, contactId: $contactId) {
745
+ id
746
+ }
747
+ }
565
748
  `,
566
- variables: { id: record.id,
567
- contactId: contact.id
568
- }
749
+ variables: { id: record.id, contactId: contact.id }
569
750
  });
570
751
  this.grist.fetch();
571
752
  }
572
753
  else if (contact?.id) {
573
- this.grist.addRecord({ name: contact.name,
574
- contact: { id: contact.id,
754
+ this.grist.addRecord({
755
+ name: contact.name,
756
+ contact: {
757
+ id: contact.id,
575
758
  name: contact.name,
576
759
  email: contact.email,
577
760
  phone: contact.phone,
@@ -585,13 +768,15 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
585
768
  }
586
769
  async detachContact(record) {
587
770
  if (record.id) {
588
- const response = await client.mutate({ mutation: gql `
589
- mutation ($id: String!) { detachContact(id: $id) { id
590
- }
591
- }
771
+ const response = await client.mutate({
772
+ mutation: gql `
773
+ mutation ($id: String!) {
774
+ detachContact(id: $id) {
775
+ id
776
+ }
777
+ }
592
778
  `,
593
- variables: { id: record.id
594
- }
779
+ variables: { id: record.id }
595
780
  });
596
781
  this.grist.fetch();
597
782
  }
@@ -602,12 +787,13 @@ let EmployeeListPage = class EmployeeListPage extends p13n(localize(i18next)(Sco
602
787
  }
603
788
  async attachSystemUser(record) {
604
789
  if (!record.user) {
605
- await client.mutate({ mutation: gql `
606
- mutation ($employeeId: String!) { registerEmployeeAsSystemUser(employeeId: $employeeId)
607
- }
790
+ await client.mutate({
791
+ mutation: gql `
792
+ mutation ($employeeId: String!) {
793
+ registerEmployeeAsSystemUser(employeeId: $employeeId)
794
+ }
608
795
  `,
609
- variables: { employeeId: record.id
610
- }
796
+ variables: { employeeId: record.id }
611
797
  });
612
798
  }
613
799
  this.grist.fetch();
@@ -623,8 +809,30 @@ __decorate([
623
809
  ], EmployeeListPage.prototype, "mode", void 0);
624
810
  __decorate([
625
811
  query('ox-grist'),
626
- __metadata("design:type", DataGrist)
812
+ __metadata("design:type", DataGrist
813
+ /** The department tree, its selection, and whether the rail is showing. */
814
+ )
627
815
  ], EmployeeListPage.prototype, "grist", void 0);
816
+ __decorate([
817
+ state(),
818
+ __metadata("design:type", Array)
819
+ ], EmployeeListPage.prototype, "departments", void 0);
820
+ __decorate([
821
+ state(),
822
+ __metadata("design:type", Department)
823
+ ], EmployeeListPage.prototype, "selectedDepartment", void 0);
824
+ __decorate([
825
+ state(),
826
+ __metadata("design:type", Object)
827
+ ], EmployeeListPage.prototype, "departmentsLoaded", void 0);
828
+ __decorate([
829
+ state(),
830
+ __metadata("design:type", Object)
831
+ ], EmployeeListPage.prototype, "departmentsError", void 0);
832
+ __decorate([
833
+ state(),
834
+ __metadata("design:type", Object)
835
+ ], EmployeeListPage.prototype, "railFolded", void 0);
628
836
  EmployeeListPage = __decorate([
629
837
  customElement('employee-list-page')
630
838
  ], EmployeeListPage);