@things-factory/organization 8.0.0-beta.8 → 8.0.0

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 (94) hide show
  1. package/client/bootstrap.ts +23 -0
  2. package/client/component/approval-line-brief.ts +119 -0
  3. package/client/component/approval-line-items-editor-popup.ts +91 -0
  4. package/client/component/approval-line-items-editor.ts +325 -0
  5. package/client/component/approval-line-selector.ts +235 -0
  6. package/client/component/approval-line-templates-manager.ts +229 -0
  7. package/client/component/approval-line-view.ts +122 -0
  8. package/client/component/assignees-editor-popup.ts +79 -0
  9. package/client/component/assignees-editor.ts +217 -0
  10. package/client/component/assignees-view.ts +55 -0
  11. package/client/component/department-selector.ts +151 -0
  12. package/client/component/department-view.ts +107 -0
  13. package/client/component/index.ts +16 -0
  14. package/client/component/recipients-editor-popup.ts +79 -0
  15. package/client/component/recipients-editor.ts +212 -0
  16. package/client/component/recipients-view.ts +55 -0
  17. package/client/grist-editor/grist-editor-approval-line.ts +70 -0
  18. package/client/grist-editor/grist-editor-assignees.ts +69 -0
  19. package/client/grist-editor/grist-editor-department-object.ts +78 -0
  20. package/client/grist-editor/grist-editor-recipients.ts +69 -0
  21. package/client/grist-editor/grist-renderer-approval-line.ts +13 -0
  22. package/client/grist-editor/grist-renderer-assignees.ts +13 -0
  23. package/client/grist-editor/grist-renderer-department-object.ts +13 -0
  24. package/client/grist-editor/grist-renderer-recipients.ts +13 -0
  25. package/client/index.ts +2 -0
  26. package/client/pages/approval-line/common-approval-line-templates-page.ts +382 -0
  27. package/client/pages/approval-line/my-approval-line-templates-page.ts +385 -0
  28. package/client/pages/department/department-importer.ts +87 -0
  29. package/client/pages/department/department-list-page.ts +450 -0
  30. package/client/pages/department/department-tree-page.ts +379 -0
  31. package/client/pages/employee/employee-importer.ts +87 -0
  32. package/client/pages/employee/employee-list-page.ts +772 -0
  33. package/client/pages/employee/employees-by-department.ts +519 -0
  34. package/client/route.ts +27 -0
  35. package/client/tsconfig.json +13 -0
  36. package/client/types/approval-line.ts +52 -0
  37. package/client/types/contact.ts +51 -0
  38. package/client/types/department.ts +29 -0
  39. package/client/types/employee.ts +50 -0
  40. package/client/types/index.ts +5 -0
  41. package/client/types/org-member.ts +27 -0
  42. package/dist-client/bootstrap.js +1 -8
  43. package/dist-client/bootstrap.js.map +1 -1
  44. package/dist-client/pages/employee/employee-list-page.js +3 -3
  45. package/dist-client/pages/employee/employee-list-page.js.map +1 -1
  46. package/dist-client/pages/employee/employees-by-department.js +2 -2
  47. package/dist-client/pages/employee/employees-by-department.js.map +1 -1
  48. package/dist-client/tsconfig.tsbuildinfo +1 -1
  49. package/dist-server/service/employee/employee-history.d.ts +2 -6
  50. package/dist-server/service/employee/employee-history.js +3 -23
  51. package/dist-server/service/employee/employee-history.js.map +1 -1
  52. package/dist-server/service/employee/employee-query.js +1 -1
  53. package/dist-server/service/employee/employee-query.js.map +1 -1
  54. package/dist-server/service/employee/employee-type.d.ts +5 -13
  55. package/dist-server/service/employee/employee-type.js +7 -39
  56. package/dist-server/service/employee/employee-type.js.map +1 -1
  57. package/dist-server/service/employee/employee.d.ts +2 -6
  58. package/dist-server/service/employee/employee.js +3 -23
  59. package/dist-server/service/employee/employee.js.map +1 -1
  60. package/dist-server/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +12 -12
  62. package/server/controllers/register-employee-as-system-user.ts +136 -0
  63. package/server/index.ts +3 -0
  64. package/server/migrations/1723861013111-seed-organization-codes.ts +127 -0
  65. package/server/migrations/index.ts +9 -0
  66. package/server/routes.ts +26 -0
  67. package/server/service/approval-line/approval-line-item.ts +42 -0
  68. package/server/service/approval-line/approval-line-mutation.ts +394 -0
  69. package/server/service/approval-line/approval-line-query.ts +208 -0
  70. package/server/service/approval-line/approval-line-type.ts +63 -0
  71. package/server/service/approval-line/approval-line.ts +123 -0
  72. package/server/service/approval-line/index.ts +7 -0
  73. package/server/service/department/department-history.ts +141 -0
  74. package/server/service/department/department-mutation.ts +231 -0
  75. package/server/service/department/department-query.ts +131 -0
  76. package/server/service/department/department-type.ts +74 -0
  77. package/server/service/department/department.ts +116 -0
  78. package/server/service/department/event-subscriber.ts +17 -0
  79. package/server/service/department/index.ts +9 -0
  80. package/server/service/employee/employee-history.ts +173 -0
  81. package/server/service/employee/employee-mutation.ts +386 -0
  82. package/server/service/employee/employee-query.ts +172 -0
  83. package/server/service/employee/employee-type.ts +176 -0
  84. package/server/service/employee/employee.ts +177 -0
  85. package/server/service/employee/event-subscriber.ts +17 -0
  86. package/server/service/employee/index.ts +9 -0
  87. package/server/service/index.ts +39 -0
  88. package/server/tsconfig.json +10 -0
  89. package/dist-client/filters-form/filter-department-object.d.ts +0 -3
  90. package/dist-client/filters-form/filter-department-object.js +0 -8
  91. package/dist-client/filters-form/filter-department-object.js.map +0 -1
  92. package/dist-client/filters-form/ox-filter-department-object.d.ts +0 -15
  93. package/dist-client/filters-form/ox-filter-department-object.js +0 -130
  94. package/dist-client/filters-form/ox-filter-department-object.js.map +0 -1
@@ -0,0 +1,772 @@
1
+ import '@operato/data-grist'
2
+ import '@operato/context/ox-context-page-toolbar.js'
3
+ import '@things-factory/contact/dist-client'
4
+
5
+ import gql from 'graphql-tag'
6
+ import { css, html } from 'lit'
7
+ import { customElement, property, query } from 'lit/decorators.js'
8
+ import { connect } from 'pwa-helpers/connect-mixin'
9
+
10
+ import { ScopedElementsMixin } from '@open-wc/scoped-elements'
11
+ import { Contact } from '@operato/contact/ox-contact.js'
12
+ import { DataGrist, FetchOption, GristRecord } from '@operato/data-grist'
13
+ import { client } from '@operato/graphql'
14
+ import { i18next, localize } from '@operato/i18n'
15
+ import { notify, openPopup } from '@operato/layout'
16
+ import { PageView, store } from '@operato/shell'
17
+ import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
18
+ import { isMobileDevice } from '@operato/utils'
19
+ import { p13n } from '@operato/p13n'
20
+
21
+ import { EmployeeImporter } from './employee-importer'
22
+
23
+ @customElement('employee-list-page')
24
+ export class EmployeeListPage extends connect(store)(p13n(localize(i18next)(ScopedElementsMixin(PageView)))) {
25
+ static styles = [
26
+ ScrollbarStyles,
27
+ CommonGristStyles,
28
+ CommonHeaderStyles,
29
+ css`
30
+ :host {
31
+ display: flex;
32
+
33
+ width: 100%;
34
+
35
+ --ox-pfp-size: 40px;
36
+
37
+ --grid-record-emphasized-background-color: #8b0000;
38
+ --grid-record-emphasized-color: #ff6b6b;
39
+ }
40
+
41
+ ox-grist {
42
+ overflow-y: auto;
43
+ flex: 1;
44
+ }
45
+
46
+ .header {
47
+ grid-template-areas: 'filters actions';
48
+ }
49
+ `
50
+ ]
51
+
52
+ static get scopedElements() {
53
+ return {
54
+ 'employee-importer': EmployeeImporter
55
+ }
56
+ }
57
+
58
+ @property({ type: Object }) gristConfig: any
59
+ @property({ type: String }) mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'CARD' : 'GRID'
60
+
61
+ @query('ox-grist') private grist!: DataGrist
62
+
63
+ get context() {
64
+ return {
65
+ title: i18next.t('title.employee list'),
66
+ search: {
67
+ handler: (search: string) => {
68
+ this.grist.searchText = search
69
+ },
70
+ value: this.grist?.searchText || ''
71
+ },
72
+ filter: {
73
+ handler: () => {
74
+ this.grist.toggleHeadroom()
75
+ }
76
+ },
77
+ help: 'organization/employee',
78
+ actions: [
79
+ {
80
+ icon: 'save',
81
+ title: i18next.t('button.save'),
82
+ action: this.onUpdateEmployee.bind(this)
83
+ },
84
+ {
85
+ icon: 'delete',
86
+ title: i18next.t('button.delete'),
87
+ action: this.onDeleteEmployee.bind(this),
88
+ emphasis: {
89
+ danger: true
90
+ }
91
+ }
92
+ ],
93
+ exportable: {
94
+ name: i18next.t('title.employee list'),
95
+ data: this.exportHandler.bind(this)
96
+ },
97
+ importable: {
98
+ handler: this.importHandler.bind(this)
99
+ },
100
+ toolbar: false
101
+ }
102
+ }
103
+
104
+ render() {
105
+ const mode = this.mode || (isMobileDevice() ? 'CARD' : 'GRID')
106
+
107
+ return html`
108
+ <ox-grist
109
+ .mode=${mode}
110
+ .config=${this.gristConfig}
111
+ .fetchHandler=${this.fetchHandler.bind(this)}
112
+ .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}
113
+ >
114
+ <div slot="headroom" class="header">
115
+ <div class="filters">
116
+ <ox-filters-form class="filter" autofocus without-search></ox-filters-form>
117
+ </div>
118
+
119
+ <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
120
+ </div>
121
+
122
+ <ox-grist-personalizer slot="setting"></ox-grist-personalizer>
123
+ </ox-grist>
124
+ `
125
+ }
126
+
127
+ async pageInitialized(lifecycle: any) {
128
+ this.gristConfig = {
129
+ pagination: { pages: [50, 100, 200] },
130
+ list: {
131
+ thumbnail: 'profile',
132
+ fields: ['controlNo', 'name'],
133
+ details: ['email', 'department', 'hiredOn', 'updatedAt']
134
+ },
135
+ columns: [
136
+ { type: 'gutter', gutterName: 'sequence', fixed: true },
137
+ { type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true },
138
+ {
139
+ type: 'gutter',
140
+ gutterName: 'button',
141
+ header: i18next.t('field.register-account'),
142
+ icon: record => (record.id && !record.user ? 'badge' : ''),
143
+ iconOnly: false,
144
+ title: record => (record.id && !record.user ? i18next.t('button.register-account') : ''),
145
+ width: 80,
146
+ handlers: {
147
+ click: (columns, data, column, record, rowIndex) => {
148
+ if (!record || !record.id || record.user) {
149
+ /* TODO record가 새로 추가된 것이면 리턴하도록 한다. */
150
+ return
151
+ }
152
+
153
+ this.attachSystemUser(record)
154
+ }
155
+ }
156
+ },
157
+ {
158
+ type: 'image',
159
+ name: 'profile',
160
+ header: i18next.t('button.edit-contact'),
161
+ width: 80,
162
+ record: {
163
+ align: 'center',
164
+ renderer: function (value, column, record, rowIndex, field) {
165
+ return html`<ox-pfp-view .profile=${record.profile} .name=${record.name || '+'}></ox-pfp-view>`
166
+ }
167
+ },
168
+ handlers: {
169
+ click: async (columns, data, column, record, rowIndex) => {
170
+ if (record && record.contact) {
171
+ this.openContactPopup(record)
172
+ } else {
173
+ this.openContactSelector(record)
174
+ }
175
+ }
176
+ }
177
+ },
178
+ {
179
+ type: 'string',
180
+ name: 'controlNo',
181
+ fixed: true,
182
+ header: i18next.t('field.control-no'),
183
+ record: {
184
+ editable: true,
185
+ mandatory: true
186
+ },
187
+ filter: 'search',
188
+ sortable: true,
189
+ width: 105
190
+ },
191
+ {
192
+ type: 'string',
193
+ name: 'name',
194
+ fixed: true,
195
+ header: i18next.t('field.name'),
196
+ record: {
197
+ editable: true,
198
+ mandatory: true
199
+ },
200
+ filter: 'search',
201
+ sortable: true,
202
+ width: 100
203
+ },
204
+ {
205
+ type: 'string',
206
+ name: 'alias',
207
+ header: i18next.t('label.alias'),
208
+ record: {
209
+ editable: true
210
+ },
211
+ filter: 'search',
212
+ sortable: false,
213
+ width: 110
214
+ },
215
+ {
216
+ type: 'resource-object',
217
+ name: 'user',
218
+ header: i18next.t('field.system-user'),
219
+ record: {
220
+ editable: true,
221
+ options: {
222
+ title: i18next.t('title.lookup system-user'),
223
+ queryName: 'users',
224
+ basicArgs: { filters: [{ name: 'userType', operator: 'eq', value: 'user' }] },
225
+ descriptionField: 'email',
226
+ columns: [
227
+ { name: 'id', hidden: true },
228
+ { name: 'name', header: i18next.t('field.name'), filter: 'search' },
229
+ { name: 'email', header: i18next.t('field.email'), filter: 'search' }
230
+ ],
231
+ list: { fields: ['name', 'email'] }
232
+ }
233
+ },
234
+ sortable: false,
235
+ filter: 'search',
236
+ width: 100
237
+ },
238
+ {
239
+ type: 'code',
240
+ name: 'type',
241
+ header: i18next.t('field.type'),
242
+ width: 115,
243
+ sortable: true,
244
+ filter: true,
245
+ record: {
246
+ editable: true,
247
+ codeName: 'EMPLOYEE_TYPE',
248
+ selectDispOpt: 'name'
249
+ }
250
+ },
251
+ {
252
+ type: 'department-object',
253
+ name: 'department',
254
+ header: i18next.t('field.department'),
255
+ record: {
256
+ editable: true
257
+ },
258
+ sortable: false,
259
+ filter: true,
260
+ width: 130
261
+ },
262
+ {
263
+ type: 'resource-object',
264
+ name: 'supervisor',
265
+ header: i18next.t('field.supervisor'),
266
+ record: {
267
+ editable: true,
268
+ options: {
269
+ title: i18next.t('title.employee list'),
270
+ queryName: 'employees',
271
+ pagination: { pages: [50, 100, 200] },
272
+ basicArgs: {
273
+ filters: [
274
+ {
275
+ name: 'active',
276
+ operator: 'eq',
277
+ value: true
278
+ }
279
+ ]
280
+ },
281
+ list: { fields: ['controlNo', 'name', 'alias', 'hiredOn'] },
282
+ columns: [
283
+ { name: 'id', hidden: true },
284
+ {
285
+ name: 'controlNo',
286
+ width: 120,
287
+ header: { renderer: () => i18next.t('field.control-no') },
288
+ filter: 'search',
289
+ sortable: true
290
+ },
291
+ {
292
+ name: 'name',
293
+ width: 120,
294
+ header: { renderer: () => i18next.t('field.name') },
295
+ filter: 'search',
296
+ sortable: true
297
+ },
298
+ {
299
+ name: 'alias',
300
+ width: 150,
301
+ header: { renderer: () => i18next.t('label.alias') },
302
+ filter: 'search',
303
+ sortable: true
304
+ },
305
+ {
306
+ type: 'code',
307
+ name: 'type',
308
+ width: 110,
309
+ header: { renderer: () => i18next.t('label.type') },
310
+ record: {
311
+ editable: false,
312
+ codeName: 'EMPLOYEE_TYPE',
313
+ selectDispOpt: 'name'
314
+ }
315
+ },
316
+ {
317
+ type: 'code',
318
+ name: 'jobPosition',
319
+ width: 110,
320
+ header: { renderer: () => i18next.t('label.job-position') },
321
+ record: {
322
+ editable: false,
323
+ codeName: 'JOB_POSITION',
324
+ selectDispOpt: 'name'
325
+ }
326
+ },
327
+ {
328
+ type: 'code',
329
+ name: 'jobResponsibility',
330
+ width: 200,
331
+ header: { renderer: () => i18next.t('label.job-responsibility') },
332
+ record: {
333
+ editable: false,
334
+ codeName: 'JOB_RESPONSIBILITY',
335
+ selectDispOpt: 'name'
336
+ }
337
+ },
338
+ {
339
+ type: 'date',
340
+ name: 'hiredOn',
341
+ header: { renderer: () => i18next.t('field.hired-on') },
342
+ width: 120
343
+ }
344
+ ],
345
+ valueField: 'id',
346
+ nameField: 'name',
347
+ descriptionField: 'controlNo'
348
+ }
349
+ },
350
+ sortable: false,
351
+ width: 120
352
+ },
353
+ {
354
+ type: 'string',
355
+ name: 'email',
356
+ header: i18next.t('field.email'),
357
+ width: 200,
358
+ record: {
359
+ editable: false,
360
+ renderer: function (value, column, record, rowIndex, field) {
361
+ return record.contact ? record.contact.email : ''
362
+ }
363
+ },
364
+ sortable: false
365
+ },
366
+ {
367
+ type: 'string',
368
+ name: 'phone',
369
+ header: i18next.t('field.phone'),
370
+ width: 130,
371
+ record: {
372
+ editable: false,
373
+ renderer: function (value, column, record, rowIndex, field) {
374
+ return record.contact ? record.contact.phone : ''
375
+ }
376
+ },
377
+ sortable: false
378
+ },
379
+ {
380
+ type: 'code',
381
+ name: 'jobResponsibility',
382
+ header: i18next.t('label.job-responsibility'),
383
+ width: 175,
384
+ record: {
385
+ editable: true,
386
+ codeName: 'JOB_RESPONSIBILITY',
387
+ selectDispOpt: 'name'
388
+ },
389
+ filter: true
390
+ },
391
+ {
392
+ type: 'code',
393
+ name: 'jobPosition',
394
+ header: i18next.t('label.job-position'),
395
+ width: 100,
396
+ record: {
397
+ editable: true,
398
+ codeName: 'JOB_POSITION',
399
+ selectDispOpt: 'name'
400
+ },
401
+ filter: true
402
+ },
403
+ {
404
+ type: 'date',
405
+ name: 'hiredOn',
406
+ header: i18next.t('field.hired-on'),
407
+ width: 120,
408
+ record: {
409
+ editable: true
410
+ },
411
+ sortable: true
412
+ },
413
+ {
414
+ type: 'date',
415
+ name: 'retiredAt',
416
+ header: i18next.t('label.retired-at'),
417
+ width: 120,
418
+ record: {
419
+ editable: true
420
+ }
421
+ },
422
+ {
423
+ type: 'checkbox',
424
+ name: 'active',
425
+ label: true,
426
+ header: i18next.t('field.active'),
427
+ width: 70,
428
+ record: {
429
+ align: 'center',
430
+ editable: true
431
+ },
432
+ filter: true,
433
+ sortable: false
434
+ },
435
+ {
436
+ type: 'string',
437
+ name: 'note',
438
+ header: i18next.t('field.note'),
439
+ width: 200,
440
+ record: {
441
+ editable: true
442
+ },
443
+ filter: 'search'
444
+ },
445
+ {
446
+ type: 'resource-object',
447
+ name: 'updater',
448
+ header: i18next.t('field.updater'),
449
+ width: 90,
450
+ sortable: false
451
+ },
452
+ {
453
+ type: 'datetime',
454
+ name: 'updatedAt',
455
+ header: i18next.t('field.updated_at'),
456
+ width: 180,
457
+ sortable: true
458
+ },
459
+ {
460
+ type: 'resource-object',
461
+ name: 'contact',
462
+ hidden: true
463
+ }
464
+ ],
465
+ rows: {
466
+ selectable: {
467
+ multiple: true
468
+ }
469
+ },
470
+ sorters: [
471
+ {
472
+ name: 'controlNo'
473
+ }
474
+ ]
475
+ }
476
+ }
477
+
478
+ async pageUpdated(changes: any, lifecycle: any) {
479
+ if (this.active) {
480
+ // do something here when this page just became as active
481
+ }
482
+ }
483
+
484
+ async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [] }: FetchOption) {
485
+ const response = await client.query({
486
+ query: gql`
487
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
488
+ responses: employees(filters: $filters, pagination: $pagination, sortings: $sortings) {
489
+ items {
490
+ id
491
+ controlNo
492
+ name
493
+ alias
494
+ type
495
+ jobResponsibility
496
+ jobPosition
497
+ active
498
+ email
499
+ phone
500
+ user {
501
+ id
502
+ name
503
+ }
504
+ department {
505
+ id
506
+ controlNo
507
+ name
508
+ description
509
+ }
510
+ supervisor {
511
+ id
512
+ name
513
+ controlNo
514
+ }
515
+ note
516
+ hiredOn
517
+ retiredAt
518
+ contact {
519
+ id
520
+ email
521
+ phone
522
+ address
523
+ }
524
+ profile {
525
+ left
526
+ top
527
+ zoom
528
+ picture
529
+ }
530
+ updater {
531
+ id
532
+ name
533
+ }
534
+ updatedAt
535
+ }
536
+ total
537
+ }
538
+ }
539
+ `,
540
+ variables: {
541
+ filters,
542
+ pagination: { page, limit },
543
+ sortings
544
+ }
545
+ })
546
+
547
+ const records = response.data.responses.items
548
+
549
+ return {
550
+ total: response.data.responses.total || 0,
551
+ records
552
+ }
553
+ }
554
+
555
+ async onDeleteEmployee() {
556
+ if (confirm(i18next.t('text.sure_to_x', { x: i18next.t('text.delete') }))) {
557
+ const ids = this.grist.selected.map(record => record.id)
558
+ if (ids && ids.length > 0) {
559
+ const response = await client.mutate({
560
+ mutation: gql`
561
+ mutation ($ids: [String!]!) {
562
+ deleteEmployees(ids: $ids)
563
+ }
564
+ `,
565
+ variables: {
566
+ ids
567
+ }
568
+ })
569
+
570
+ if (!response.errors) {
571
+ this.grist.fetch()
572
+ notify({
573
+ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.delete') })
574
+ })
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ async onUpdateEmployee() {
581
+ let patches = this.grist.dirtyRecords
582
+ if (patches && patches.length) {
583
+ patches = patches.map(patch => {
584
+ let patchField: any = patch.id ? { id: patch.id } : {}
585
+ const dirtyFields = patch.__dirtyfields__
586
+ for (let key in dirtyFields) {
587
+ patchField[key] = dirtyFields[key].after
588
+ }
589
+ patchField.cuFlag = patch.__dirty__
590
+
591
+ return patchField
592
+ })
593
+
594
+ const response = await client.mutate({
595
+ mutation: gql`
596
+ mutation ($patches: [EmployeePatch!]!) {
597
+ updateMultipleEmployee(patches: $patches) {
598
+ name
599
+ }
600
+ }
601
+ `,
602
+ variables: {
603
+ patches
604
+ }
605
+ })
606
+
607
+ if (!response.errors) {
608
+ this.grist.fetch()
609
+ }
610
+ }
611
+ }
612
+
613
+ async exportHandler() {
614
+ const exportTargets = this.grist.selected.length ? this.grist.selected : this.grist.dirtyData.records
615
+ const targetFieldSet = new Set([
616
+ 'id',
617
+ 'controlNo',
618
+ 'name',
619
+ 'alias',
620
+ 'type',
621
+ 'hiredOn',
622
+ 'retiredAt',
623
+ 'active',
624
+ 'note'
625
+ ])
626
+
627
+ return exportTargets.map(employee => {
628
+ let tempObj = {}
629
+ for (const field of targetFieldSet) {
630
+ tempObj[field] = employee[field]
631
+ }
632
+
633
+ return tempObj
634
+ })
635
+ }
636
+
637
+ async importHandler(records) {
638
+ const popup = openPopup(
639
+ html`
640
+ <employee-importer
641
+ .employees=${records}
642
+ @imported=${() => {
643
+ history.back()
644
+ this.grist.fetch()
645
+ }}
646
+ ></employee-importer>
647
+ `,
648
+ {
649
+ backdrop: true,
650
+ size: 'large',
651
+ title: i18next.t('title.import employee')
652
+ }
653
+ )
654
+
655
+ popup.onclosed = () => {
656
+ this.grist.fetch()
657
+ }
658
+ }
659
+
660
+ async openContactPopup(record: GristRecord) {
661
+ if (!record.contact?.id) {
662
+ return
663
+ }
664
+
665
+ const popup = openPopup(
666
+ html`
667
+ <contact-popup
668
+ .contactId=${record.contact.id}
669
+ @change=${() => {
670
+ this.grist.fetch()
671
+ }}
672
+ @detach=${() => {
673
+ this.detachContact(record)
674
+ }}
675
+ detachable
676
+ ></contact-popup>
677
+ `,
678
+ {
679
+ backdrop: true,
680
+ size: 'large',
681
+ title: i18next.t('title.contact')
682
+ }
683
+ )
684
+ }
685
+
686
+ async openContactSelector(record: GristRecord) {
687
+ const popup = openPopup(
688
+ html`
689
+ <contact-selector .confirmCallback=${selected => this.attachContact(record, selected)}></contact-selector>
690
+ `,
691
+ {
692
+ backdrop: true,
693
+ size: 'large',
694
+ title: i18next.t('title.contact')
695
+ }
696
+ )
697
+ }
698
+
699
+ async attachContact(record: GristRecord, contact: Contact) {
700
+ if (record.id) {
701
+ const response = await client.mutate({
702
+ mutation: gql`
703
+ mutation ($id: String!, $contactId: String!) {
704
+ attachContact(id: $id, contactId: $contactId) {
705
+ id
706
+ }
707
+ }
708
+ `,
709
+ variables: {
710
+ id: record.id,
711
+ contactId: contact.id
712
+ }
713
+ })
714
+
715
+ this.grist.fetch()
716
+ } else if (contact?.id) {
717
+ this.grist.addRecord({
718
+ name: contact.name,
719
+ contact: {
720
+ id: contact.id,
721
+ name: contact.name,
722
+ email: (contact as any).email,
723
+ phone: (contact as any).phone,
724
+ address: (contact as any).address
725
+ }
726
+ })
727
+ this.grist.refresh()
728
+ await this.grist.requestUpdate()
729
+
730
+ this.grist.checkDirties()
731
+ }
732
+ }
733
+
734
+ async detachContact(record) {
735
+ if (record.id) {
736
+ const response = await client.mutate({
737
+ mutation: gql`
738
+ mutation ($id: String!) {
739
+ detachContact(id: $id) {
740
+ id
741
+ }
742
+ }
743
+ `,
744
+ variables: {
745
+ id: record.id
746
+ }
747
+ })
748
+
749
+ this.grist.fetch()
750
+ } else {
751
+ record.contact = null
752
+ this.grist.refresh()
753
+ }
754
+ }
755
+
756
+ async attachSystemUser(record) {
757
+ if (!record.user) {
758
+ await client.mutate({
759
+ mutation: gql`
760
+ mutation ($employeeId: String!) {
761
+ registerEmployeeAsSystemUser(employeeId: $employeeId)
762
+ }
763
+ `,
764
+ variables: {
765
+ employeeId: record.id
766
+ }
767
+ })
768
+ }
769
+
770
+ this.grist.fetch()
771
+ }
772
+ }