@things-factory/organization 10.1.27 → 10.1.29

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