@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,519 @@
1
+ import '@operato/data-tree'
2
+
3
+ import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
4
+ import { PageView, store } from '@operato/shell'
5
+ import { css, html } from 'lit'
6
+ import { customElement, property, query, state } from 'lit/decorators.js'
7
+ import { ScopedElementsMixin } from '@open-wc/scoped-elements'
8
+ import { client } from '@operato/graphql'
9
+ import { i18next, localize } from '@operato/i18n'
10
+ import { DataGrist, FetchOption, GristRecord } from '@operato/data-grist'
11
+
12
+ import { connect } from 'pwa-helpers/connect-mixin'
13
+ import gql from 'graphql-tag'
14
+
15
+ import { DepartmentImporter } from '../department/department-importer'
16
+ import { Department } from '../../types/department'
17
+
18
+ import { EmployeeListPage } from './employee-list-page'
19
+
20
+ const departmentFragment = gql`
21
+ fragment SubDepartmentFragment on Department {
22
+ id
23
+ controlNo
24
+ name
25
+ description
26
+
27
+ manager {
28
+ id
29
+ name
30
+ controlNo
31
+ photo
32
+ email
33
+ }
34
+ active
35
+ state
36
+ picture
37
+
38
+ updater {
39
+ id
40
+ name
41
+ }
42
+ updatedAt
43
+ }
44
+ `
45
+
46
+ @customElement('employees-by-department')
47
+ export class EmployeesByDepartment extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
48
+ static styles = [
49
+ ScrollbarStyles,
50
+ CommonGristStyles,
51
+ CommonHeaderStyles,
52
+ css`
53
+ :host {
54
+ display: flex;
55
+ flex-direction: row;
56
+ overflow: auto;
57
+ }
58
+
59
+ ox-tree {
60
+ flex: 1;
61
+ overflow: auto;
62
+ }
63
+
64
+ ox-grist {
65
+ flex: 4;
66
+ }
67
+
68
+ ox-filters-form {
69
+ flex: 1;
70
+ }
71
+ `
72
+ ]
73
+
74
+ static get scopedElements() {
75
+ return {
76
+ 'department-importer': DepartmentImporter
77
+ }
78
+ }
79
+
80
+ @query('ox-grist') private grist!: DataGrist
81
+ @property({ type: Object }) gristConfig: any
82
+
83
+ @state() root?: Department
84
+ @state() selected?: Department
85
+
86
+ @query('employee-list-page') employeeListPage!: EmployeeListPage
87
+
88
+ get context() {
89
+ return {
90
+ title: i18next.t('title.employees-by-department'),
91
+ actions: []
92
+ }
93
+ }
94
+
95
+ render() {
96
+ return html`
97
+ <ox-tree
98
+ .data=${this.root}
99
+ .selected=${this.selected}
100
+ @select=${this.onSelect.bind(this)}
101
+ label-property="name"
102
+ ></ox-tree>
103
+
104
+ <ox-grist .config=${this.gristConfig} .fetchHandler=${this.fetchEmployees.bind(this)}>
105
+ <div slot="headroom" class="header">
106
+ <div class="filters">
107
+ <ox-filters-form autofocus></ox-filters-form>
108
+ </div>
109
+ </div>
110
+ </ox-grist>
111
+ `
112
+ }
113
+
114
+ async pageInitialized(lifecycle: any) {
115
+ this.gristConfig = {
116
+ pagination: { pages: [100] },
117
+ list: {
118
+ thumbnail: 'profile',
119
+ fields: ['controlNo', 'name'],
120
+ details: ['email', 'department', 'hiredOn', 'updatedAt']
121
+ },
122
+ columns: [
123
+ { type: 'gutter', gutterName: 'sequence' },
124
+ {
125
+ type: 'string',
126
+ name: 'controlNo',
127
+ header: i18next.t('field.control-no'),
128
+ record: {
129
+ editable: true
130
+ },
131
+ filter: 'search',
132
+ sortable: true,
133
+ width: 105
134
+ },
135
+ {
136
+ type: 'string',
137
+ name: 'name',
138
+ header: i18next.t('field.name'),
139
+ record: {
140
+ editable: true
141
+ },
142
+ filter: 'search',
143
+ sortable: true,
144
+ width: 100
145
+ },
146
+ {
147
+ type: 'string',
148
+ name: 'alias',
149
+ header: i18next.t('label.alias'),
150
+ record: {
151
+ editable: true
152
+ },
153
+ filter: 'search',
154
+ sortable: true,
155
+ width: 110
156
+ },
157
+ {
158
+ type: 'resource-object',
159
+ name: 'user',
160
+ header: i18next.t('field.user'),
161
+ record: {
162
+ editable: true,
163
+ options: {
164
+ title: i18next.t('title.lookup user'),
165
+ queryName: 'users',
166
+ basicArgs: { filters: [{ name: 'userType', operator: 'eq', value: 'user' }] },
167
+ descriptionField: 'email',
168
+ columns: [
169
+ { name: 'id', hidden: true },
170
+ { name: 'name', header: i18next.t('field.name'), filter: 'search' },
171
+ { name: 'email', header: i18next.t('field.email'), filter: 'search' }
172
+ ],
173
+ list: { fields: ['name', 'email'] }
174
+ }
175
+ },
176
+ sortable: true,
177
+ filter: false,
178
+ width: 100
179
+ },
180
+ {
181
+ type: 'code',
182
+ name: 'type',
183
+ header: i18next.t('field.type'),
184
+ width: 115,
185
+ sortable: false,
186
+ filter: false,
187
+ record: {
188
+ editable: true,
189
+ codeName: 'EMPLOYEE_TYPE'
190
+ }
191
+ },
192
+ {
193
+ type: 'department-object',
194
+ name: 'department',
195
+ header: i18next.t('field.department'),
196
+ record: {
197
+ editable: true
198
+ },
199
+ sortable: true,
200
+ filter: false,
201
+ width: 130
202
+ },
203
+ {
204
+ type: 'resource-object',
205
+ name: 'supervisor',
206
+ header: i18next.t('field.supervisor'),
207
+ record: {
208
+ editable: true,
209
+ options: {
210
+ title: i18next.t('title.employee list'),
211
+ queryName: 'employees',
212
+ pagination: { pages: [50, 100, 200] },
213
+ basicArgs: {
214
+ filters: [
215
+ {
216
+ name: 'active',
217
+ operator: 'eq',
218
+ value: true
219
+ }
220
+ ]
221
+ },
222
+ list: { fields: ['controlNo', 'name', 'alias', 'hiredOn'] },
223
+ columns: [
224
+ { name: 'id', hidden: true },
225
+ {
226
+ name: 'controlNo',
227
+ width: 120,
228
+ header: { renderer: () => i18next.t('field.control-no') },
229
+ filter: 'search',
230
+ sortable: true
231
+ },
232
+ {
233
+ name: 'name',
234
+ width: 120,
235
+ header: { renderer: () => i18next.t('field.name') },
236
+ filter: 'search',
237
+ sortable: true
238
+ },
239
+ {
240
+ name: 'alias',
241
+ width: 150,
242
+ header: { renderer: () => i18next.t('label.alias') },
243
+ filter: 'search',
244
+ sortable: true
245
+ },
246
+ {
247
+ type: 'code',
248
+ name: 'type',
249
+ width: 110,
250
+ header: { renderer: () => i18next.t('label.type') },
251
+ record: {
252
+ editable: false,
253
+ codeName: 'EMPLOYEE_TYPE'
254
+ }
255
+ },
256
+ {
257
+ type: 'code',
258
+ name: 'jobPosition',
259
+ width: 110,
260
+ header: { renderer: () => i18next.t('label.job-position') },
261
+ record: {
262
+ editable: false,
263
+ codeName: 'JOB_POSITION'
264
+ }
265
+ },
266
+ {
267
+ type: 'code',
268
+ name: 'jobResponsibility',
269
+ width: 200,
270
+ header: { renderer: () => i18next.t('label.job-responsibility') },
271
+ record: {
272
+ editable: false,
273
+ codeName: 'JOB_RESPONSIBILITY'
274
+ }
275
+ },
276
+ {
277
+ type: 'date',
278
+ name: 'hiredOn',
279
+ header: { renderer: () => i18next.t('field.hired-on') },
280
+ width: 100
281
+ }
282
+ ],
283
+ valueField: 'id',
284
+ nameField: 'name',
285
+ descriptionField: 'controlNo'
286
+ }
287
+ },
288
+ sortable: true,
289
+ width: 120
290
+ },
291
+ {
292
+ type: 'string',
293
+ name: 'email',
294
+ header: i18next.t('field.email'),
295
+ width: 200,
296
+ record: {
297
+ editable: false,
298
+ renderer: function (value, column, record, rowIndex, field) {
299
+ return record.contact ? record.contact.email : ''
300
+ }
301
+ },
302
+ sortable: true
303
+ },
304
+ {
305
+ type: 'string',
306
+ name: 'phone',
307
+ header: i18next.t('field.phone'),
308
+ width: 110,
309
+ record: {
310
+ editable: false,
311
+ renderer: function (value, column, record, rowIndex, field) {
312
+ return record.contact ? record.contact.phone : ''
313
+ }
314
+ },
315
+ sortable: true
316
+ },
317
+ {
318
+ type: 'code',
319
+ name: 'jobResponsibility',
320
+ header: i18next.t('label.job-responsibility'),
321
+ width: 175,
322
+ record: {
323
+ editable: true,
324
+ codeName: 'JOB_RESPONSIBILITY'
325
+ }
326
+ },
327
+ {
328
+ type: 'code',
329
+ name: 'jobPosition',
330
+ header: i18next.t('label.job-position'),
331
+ width: 100,
332
+ record: {
333
+ editable: true,
334
+ codeName: 'JOB_POSITION'
335
+ }
336
+ },
337
+ {
338
+ type: 'date',
339
+ name: 'hiredOn',
340
+ header: i18next.t('field.hired-on'),
341
+ width: 100,
342
+ record: {
343
+ editable: true
344
+ },
345
+ sortable: true
346
+ },
347
+ {
348
+ type: 'date',
349
+ name: 'retiredAt',
350
+ header: i18next.t('label.retired-at'),
351
+ width: 100,
352
+ record: {
353
+ editable: true
354
+ }
355
+ },
356
+ {
357
+ type: 'checkbox',
358
+ name: 'active',
359
+ label: true,
360
+ header: i18next.t('field.active'),
361
+ width: 70,
362
+ record: {
363
+ align: 'center',
364
+ editable: true
365
+ },
366
+ filter: true,
367
+ sortable: true
368
+ },
369
+ {
370
+ type: 'string',
371
+ name: 'note',
372
+ header: i18next.t('field.note'),
373
+ width: 200,
374
+ record: {
375
+ editable: true
376
+ },
377
+ filter: 'search'
378
+ }
379
+ ],
380
+ rows: {
381
+ selectable: {
382
+ multiple: false
383
+ }
384
+ },
385
+ sorters: [
386
+ {
387
+ name: 'controlNo'
388
+ }
389
+ ]
390
+ }
391
+
392
+ this.fetchDepartments()
393
+ }
394
+
395
+ async pageUpdated(changes: any, lifecycle: any) {
396
+ if (this.active) {
397
+ }
398
+ }
399
+
400
+ async onSelect(e: CustomEvent) {
401
+ this.selected = e.detail as Department
402
+ this.grist.fetch()
403
+ this.updateContext()
404
+ }
405
+
406
+ async fetchEmployees({ page = 1, limit = 100, sortings = [], filters = [] }: FetchOption) {
407
+ if (this.selected) {
408
+ filters.push({
409
+ name: 'departmentId',
410
+ operator: 'eq',
411
+ value: this.selected.id
412
+ })
413
+ }
414
+
415
+ const response = await client.query({
416
+ query: gql`
417
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
418
+ responses: employees(filters: $filters, pagination: $pagination, sortings: $sortings) {
419
+ items {
420
+ id
421
+ controlNo
422
+ name
423
+ alias
424
+ type
425
+ jobResponsibility
426
+ jobPosition
427
+ active
428
+ email
429
+ phone
430
+ user {
431
+ id
432
+ name
433
+ }
434
+ department {
435
+ id
436
+ name
437
+ description
438
+ }
439
+ supervisor {
440
+ id
441
+ name
442
+ controlNo
443
+ }
444
+ note
445
+ hiredOn
446
+ retiredAt
447
+ contact {
448
+ id
449
+ email
450
+ phone
451
+ address
452
+ }
453
+ profile {
454
+ left
455
+ top
456
+ zoom
457
+ picture
458
+ }
459
+ updater {
460
+ id
461
+ name
462
+ }
463
+ updatedAt
464
+ }
465
+ total
466
+ }
467
+ }
468
+ `,
469
+ variables: {
470
+ filters,
471
+ pagination: { page, limit },
472
+ sortings
473
+ }
474
+ })
475
+
476
+ const records = response.data.responses.items
477
+ return {
478
+ total: response.data.responses.total || 0,
479
+ records
480
+ }
481
+ }
482
+
483
+ async fetchDepartments() {
484
+ const response = await client.query({
485
+ query: gql`
486
+ query {
487
+ responses: departmentRoots {
488
+ ...SubDepartmentFragment
489
+ children {
490
+ ...SubDepartmentFragment
491
+ children {
492
+ ...SubDepartmentFragment
493
+ children {
494
+ ...SubDepartmentFragment
495
+ children {
496
+ ...SubDepartmentFragment
497
+ children {
498
+ ...SubDepartmentFragment
499
+ children {
500
+ ...SubDepartmentFragment
501
+ children {
502
+ ...SubDepartmentFragment
503
+ }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ ${departmentFragment}
514
+ `
515
+ })
516
+
517
+ this.root = response.data.responses
518
+ }
519
+ }
@@ -0,0 +1,27 @@
1
+ export default function route(page: string) {
2
+ switch (page) {
3
+ case 'employee-list':
4
+ import('./pages/employee/employee-list-page')
5
+ return page
6
+
7
+ case 'department-tree':
8
+ import('./pages/department/department-tree-page')
9
+ return page
10
+
11
+ case 'department-list':
12
+ import('./pages/department/department-list-page')
13
+ return page
14
+
15
+ case 'employees-by-department':
16
+ import('./pages/employee/employees-by-department')
17
+ return page
18
+
19
+ case 'common-approval-line-templates-page':
20
+ import('./pages/approval-line/common-approval-line-templates-page')
21
+ return page
22
+
23
+ case 'my-approval-line-templates-page':
24
+ import('./pages/approval-line/my-approval-line-templates-page')
25
+ return page
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "experimentalDecorators": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "declaration": true,
8
+ "module": "esnext",
9
+ "outDir": "../dist-client",
10
+ "baseUrl": "./"
11
+ },
12
+ "include": ["./**/*"]
13
+ }
@@ -0,0 +1,52 @@
1
+ import { Domain, User, Role } from '@operato/shell'
2
+
3
+ import { Employee } from './employee'
4
+ import { Department } from './department'
5
+ import { OrgMemberTarget, OrgMemberTargetType } from './org-member'
6
+
7
+ export enum ApprovalLineOwnerType {
8
+ Employee = 'Employee',
9
+ Common = 'Common'
10
+ }
11
+
12
+ export class ApprovalLineItem {
13
+ type?: OrgMemberTargetType
14
+ value?: string
15
+ approver?: OrgMemberTarget
16
+ }
17
+
18
+ export class ApprovalLine {
19
+ readonly id?: string
20
+
21
+ domain?: Domain
22
+
23
+ domainId?: string
24
+
25
+ name?: string
26
+
27
+ description?: string
28
+
29
+ model?: ApprovalLineItem[]
30
+
31
+ ownerType?: ApprovalLineOwnerType
32
+
33
+ ownerValue?: string
34
+
35
+ ownerEmployee?: Employee
36
+
37
+ ownerDepartment?: Department
38
+
39
+ ownerRole?: Role
40
+
41
+ createdAt?: Date
42
+
43
+ updatedAt?: Date
44
+
45
+ creator?: User
46
+
47
+ creatorId?: string
48
+
49
+ updater?: User
50
+
51
+ updaterId?: string
52
+ }
@@ -0,0 +1,51 @@
1
+ import { Domain, User } from '@operato/shell'
2
+
3
+ export enum ContactField {
4
+ Name = 'name',
5
+ JobTitle = 'job-title',
6
+ Phone = 'phone',
7
+ Address = 'address',
8
+ Email = 'email',
9
+ Birthday = 'birthday',
10
+ Profile = 'profile',
11
+ Picture = 'picture',
12
+ Department = 'department',
13
+ Company = 'company',
14
+ Homepage = 'homepage'
15
+ }
16
+
17
+ export class ContactItem {
18
+ label?: string
19
+ type?: ContactField
20
+ value?: string
21
+ }
22
+
23
+ export class Profile {
24
+ left?: number
25
+ top?: number
26
+ zoom?: number
27
+
28
+ picture?: string
29
+ }
30
+
31
+ export class Contact {
32
+ readonly id?: string
33
+
34
+ domain?: Domain
35
+ name?: string
36
+ company?: string
37
+ email?: string
38
+ phone?: string
39
+ address?: string
40
+ department?: string
41
+ note?: string
42
+ profile?: Profile
43
+ items?: ContactItem[]
44
+
45
+ createdAt?: Date
46
+ updatedAt?: Date
47
+ deletedAt?: Date
48
+
49
+ creator?: User
50
+ updater?: User
51
+ }
@@ -0,0 +1,29 @@
1
+ import { Domain, User } from '@operato/shell'
2
+ import { Employee } from './employee'
3
+
4
+ export class Department {
5
+ readonly id?: string
6
+
7
+ domain?: Domain
8
+
9
+ controlNo?: string
10
+ name?: string
11
+ description?: string
12
+
13
+ parent?: Department
14
+ children?: Department[]
15
+
16
+ manager?: Employee
17
+
18
+ extension?: string
19
+ picture?: string
20
+
21
+ active?: boolean
22
+
23
+ createdAt?: Date
24
+ updatedAt?: Date
25
+ deletedAt?: Date
26
+
27
+ creator?: User
28
+ updater?: User
29
+ }