@things-factory/organization 10.1.223 → 10.1.227

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 (69) hide show
  1. package/dist-client/pages/employee-profile/employee-profile-files.d.ts +23 -0
  2. package/dist-client/pages/employee-profile/employee-profile-files.js +50 -0
  3. package/dist-client/pages/employee-profile/employee-profile-files.js.map +1 -0
  4. package/dist-client/pages/employee-profile/employee-profile-form.d.ts +63 -0
  5. package/dist-client/pages/employee-profile/employee-profile-form.js +132 -0
  6. package/dist-client/pages/employee-profile/employee-profile-form.js.map +1 -0
  7. package/dist-client/pages/employee-profile/employee-profile-list-page.d.ts +52 -0
  8. package/dist-client/pages/employee-profile/employee-profile-list-page.js +206 -0
  9. package/dist-client/pages/employee-profile/employee-profile-list-page.js.map +1 -0
  10. package/dist-client/pages/employee-profile/employee-profile-preview.d.ts +28 -0
  11. package/dist-client/pages/employee-profile/employee-profile-preview.js +179 -0
  12. package/dist-client/pages/employee-profile/employee-profile-preview.js.map +1 -0
  13. package/dist-client/pages/employee-profile/my-employee-profile-page.d.ts +56 -0
  14. package/dist-client/pages/employee-profile/my-employee-profile-page.js +656 -0
  15. package/dist-client/pages/employee-profile/my-employee-profile-page.js.map +1 -0
  16. package/dist-client/route.d.ts +1 -1
  17. package/dist-client/route.js +7 -0
  18. package/dist-client/route.js.map +1 -1
  19. package/dist-client/tsconfig.tsbuildinfo +1 -1
  20. package/dist-server/ai-entry/employee-profile-entry.d.ts +10 -0
  21. package/dist-server/ai-entry/employee-profile-entry.js +88 -0
  22. package/dist-server/ai-entry/employee-profile-entry.js.map +1 -0
  23. package/dist-server/ai-entry/entry-target-shape.d.ts +11 -1
  24. package/dist-server/ai-entry/entry-target-shape.js.map +1 -1
  25. package/dist-server/ai-entry/organization-entry-targets.d.ts +8 -0
  26. package/dist-server/ai-entry/organization-entry-targets.js +26 -1
  27. package/dist-server/ai-entry/organization-entry-targets.js.map +1 -1
  28. package/dist-server/role-tasks.js +4 -1
  29. package/dist-server/role-tasks.js.map +1 -1
  30. package/dist-server/service/employee-profile/employee-profile-document.d.ts +122 -0
  31. package/dist-server/service/employee-profile/employee-profile-document.js +439 -0
  32. package/dist-server/service/employee-profile/employee-profile-document.js.map +1 -0
  33. package/dist-server/service/employee-profile/employee-profile-mutation.d.ts +6 -0
  34. package/dist-server/service/employee-profile/employee-profile-mutation.js +74 -0
  35. package/dist-server/service/employee-profile/employee-profile-mutation.js.map +1 -0
  36. package/dist-server/service/employee-profile/employee-profile-query.d.ts +14 -0
  37. package/dist-server/service/employee-profile/employee-profile-query.js +137 -0
  38. package/dist-server/service/employee-profile/employee-profile-query.js.map +1 -0
  39. package/dist-server/service/employee-profile/employee-profile-rows.d.ts +29 -0
  40. package/dist-server/service/employee-profile/employee-profile-rows.js +122 -0
  41. package/dist-server/service/employee-profile/employee-profile-rows.js.map +1 -0
  42. package/dist-server/service/employee-profile/employee-profile-rules.d.ts +61 -0
  43. package/dist-server/service/employee-profile/employee-profile-rules.js +104 -0
  44. package/dist-server/service/employee-profile/employee-profile-rules.js.map +1 -0
  45. package/dist-server/service/employee-profile/employee-profile-type.d.ts +35 -0
  46. package/dist-server/service/employee-profile/employee-profile-type.js +110 -0
  47. package/dist-server/service/employee-profile/employee-profile-type.js.map +1 -0
  48. package/dist-server/service/employee-profile/employee-profile.d.ts +24 -0
  49. package/dist-server/service/employee-profile/employee-profile.js +112 -0
  50. package/dist-server/service/employee-profile/employee-profile.js.map +1 -0
  51. package/dist-server/service/employee-profile/index.d.ts +5 -0
  52. package/dist-server/service/employee-profile/index.js +9 -0
  53. package/dist-server/service/employee-profile/index.js.map +1 -0
  54. package/dist-server/service/index.d.ts +3 -2
  55. package/dist-server/service/index.js +6 -2
  56. package/dist-server/service/index.js.map +1 -1
  57. package/dist-server/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +8 -6
  59. package/tests/employee-profile-db.test.ts +247 -0
  60. package/tests/employee-profile-document.test.ts +219 -0
  61. package/tests/employee-profile-entry.test.ts +72 -0
  62. package/tests/employee-profile-rules.test.ts +103 -0
  63. package/things-factory.config.js +3 -0
  64. package/translations/en.json +78 -1
  65. package/translations/es.json +78 -1
  66. package/translations/ja.json +78 -1
  67. package/translations/ko.json +78 -1
  68. package/translations/ms.json +78 -1
  69. package/translations/zh.json +78 -1
@@ -0,0 +1,656 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/list-state/ox-empty-note.js';
4
+ import '@operato/list-state/ox-page-state.js';
5
+ import gql from 'graphql-tag';
6
+ import { css, html, nothing } from 'lit';
7
+ import { customElement, state } from 'lit/decorators.js';
8
+ import { client } from '@operato/graphql';
9
+ import { i18next, localize } from '@operato/i18n';
10
+ import { notify } from '@operato/layout';
11
+ import { PageView } from '@operato/shell';
12
+ import { ButtonStyles, DocumentPageStyles, FormFieldStyles } from '@operato/styles';
13
+ import { getLanguages } from '@things-factory/auth-base/dist-client';
14
+ import { addSkills, blankRow, completenessOf, headlineOf, periodLabelOf, PROFILE_SECTIONS, profileFormOf, profileInputOf } from './employee-profile-form';
15
+ import { readerLanguageOf } from './employee-profile-files';
16
+ import { openProfilePreview } from './employee-profile-preview';
17
+ const PROFILE_FIELDS = `
18
+ id
19
+ summary
20
+ skills
21
+ educations { school major degree startedOn endedOn }
22
+ certificates { name issuer acquiredOn }
23
+ careers { company department position startedOn endedOn duties }
24
+ projects { name client startedOn endedOn role duties technologies }
25
+ updatedAt
26
+ `;
27
+ const SUMMARY_LIMIT = 500;
28
+ /**
29
+ * 내 프로필 — the employee writes their own career profile: a summary, skills, and the rows of education,
30
+ * certificates, past jobs and projects; sales attaches it to quotations (ADR-0169 decisions 2 · 5). Saved whole.
31
+ *
32
+ * The page is built to be filled in: who it is about and how complete it is stand at the top, each missing essential a
33
+ * click away; each row is a card whose headline reads like the document's line; a section with nothing yet says what
34
+ * it is for; and the document itself is one click away (PDF), as sales will hand it in. An account not linked to an
35
+ * employee record has no profile to write, and is told so before anything is typed.
36
+ */
37
+ let MyEmployeeProfilePage = class MyEmployeeProfilePage extends localize(i18next)(PageView) {
38
+ constructor() {
39
+ super(...arguments);
40
+ this.form = profileFormOf(null);
41
+ /** Who the profile is about - the employee record's basics. */
42
+ this.me = null;
43
+ /** Whether the account is linked to an employee record - unknown until read. */
44
+ this.linked = null;
45
+ this.loading = false;
46
+ this.error = null;
47
+ this.busy = false;
48
+ this.updatedAt = null;
49
+ /** The form as last read or saved - what is written differs from it until saved. */
50
+ this.saved = JSON.stringify(profileInputOf(profileFormOf(null)));
51
+ }
52
+ static { this.styles = [
53
+ FormFieldStyles,
54
+ ButtonStyles,
55
+ DocumentPageStyles,
56
+ css `
57
+ label {
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: var(--spacing-small, 4px);
61
+ min-width: 0;
62
+ font-size: var(--ui-t-small, 11.5px);
63
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
64
+ }
65
+ label > :is(input, textarea) {
66
+ font-size: var(--ui-t-body, 13px);
67
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
68
+ }
69
+ input,
70
+ textarea {
71
+ box-sizing: border-box;
72
+ width: 100%;
73
+ font-family: inherit;
74
+ }
75
+ input:not([type='checkbox']) {
76
+ height: var(--form-element-height-medium, 32px);
77
+ }
78
+ textarea {
79
+ min-height: 72px;
80
+ resize: vertical;
81
+ line-height: 1.5;
82
+ }
83
+ .muted {
84
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
85
+ }
86
+
87
+ /* ── Who it is about, and how far along ─────────────────────────────── */
88
+ .hero {
89
+ display: grid;
90
+ grid-template-columns: minmax(0, 1fr) minmax(240px, 340px);
91
+ gap: var(--spacing-huge, 24px);
92
+ align-items: center;
93
+ }
94
+ .who {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: var(--spacing-large, 12px);
98
+ min-width: 0;
99
+ }
100
+ .avatar {
101
+ flex: none;
102
+ display: grid;
103
+ place-items: center;
104
+ width: 56px;
105
+ height: 56px;
106
+ border-radius: 50%;
107
+ background: var(--md-sys-color-primary-container);
108
+ color: var(--md-sys-color-on-primary-container);
109
+ font-size: 22px;
110
+ font-weight: 700;
111
+ }
112
+ .who h2 {
113
+ margin: 0;
114
+ font-size: var(--ui-t-headline, 20px);
115
+ font-weight: 700;
116
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
117
+ }
118
+ .who p {
119
+ margin: 2px 0 0;
120
+ font-size: var(--ui-t-body, 13px);
121
+ }
122
+ .progress {
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: var(--spacing-small, 4px);
126
+ }
127
+ .progress-head {
128
+ display: flex;
129
+ align-items: baseline;
130
+ justify-content: space-between;
131
+ font-size: var(--ui-t-small, 11.5px);
132
+ }
133
+ .progress-head strong {
134
+ font-size: var(--ui-t-title, 15px);
135
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
136
+ }
137
+ .bar {
138
+ height: 8px;
139
+ border-radius: 4px;
140
+ background: var(--md-sys-color-surface-container-high);
141
+ overflow: hidden;
142
+ }
143
+ .bar > span {
144
+ display: block;
145
+ height: 100%;
146
+ border-radius: 4px;
147
+ background: var(--md-sys-color-primary);
148
+ transition: width 0.3s ease;
149
+ }
150
+ .missing {
151
+ display: flex;
152
+ flex-wrap: wrap;
153
+ align-items: center;
154
+ gap: var(--spacing-small, 4px);
155
+ margin-top: var(--spacing-small, 4px);
156
+ font-size: var(--ui-t-small, 11.5px);
157
+ }
158
+ .hero-foot {
159
+ grid-column: 1 / -1;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: var(--spacing-medium, 8px);
163
+ padding-top: var(--spacing-large, 12px);
164
+ border-top: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
165
+ font-size: var(--ui-t-small, 11.5px);
166
+ }
167
+ .hero-foot .unsaved {
168
+ color: var(--md-sys-color-tertiary, var(--md-sys-color-primary));
169
+ font-weight: 600;
170
+ }
171
+ .hero-foot button {
172
+ margin-left: auto;
173
+ }
174
+
175
+ /* ── A section ─────────────────────────────────────────────────────── */
176
+ .page-section-header h3 {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: var(--spacing-medium, 8px);
180
+ }
181
+ .page-section-header h3 md-icon {
182
+ --md-icon-size: 20px;
183
+ color: var(--md-sys-color-primary);
184
+ }
185
+ .count {
186
+ min-width: 20px;
187
+ padding: 1px 7px;
188
+ border-radius: 10px;
189
+ background: var(--md-sys-color-surface-container-high);
190
+ font-size: var(--ui-t-small, 11.5px);
191
+ font-weight: 600;
192
+ text-align: center;
193
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
194
+ }
195
+ .counter {
196
+ margin-left: auto;
197
+ font-size: var(--ui-t-small, 11.5px);
198
+ }
199
+ .summary textarea {
200
+ min-height: 132px;
201
+ }
202
+
203
+ /* Skills as chips, the field to add more inside the same box. */
204
+ .chips {
205
+ display: flex;
206
+ flex-wrap: wrap;
207
+ align-items: center;
208
+ gap: var(--spacing-small, 4px);
209
+ min-height: 88px;
210
+ align-content: flex-start;
211
+ box-sizing: border-box;
212
+ padding: var(--spacing-medium, 8px);
213
+ border: 1px solid var(--md-sys-color-outline);
214
+ border-radius: var(--input-field-border-radius, var(--md-sys-shape-corner-small, 4px));
215
+ background: var(--md-sys-color-surface-container-highest);
216
+ }
217
+ .chips:focus-within {
218
+ border-color: var(--md-sys-color-primary);
219
+ }
220
+ .chip {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ gap: 2px;
224
+ height: 26px;
225
+ padding: 0 4px 0 10px;
226
+ border-radius: 13px;
227
+ background: var(--md-sys-color-secondary-container);
228
+ color: var(--md-sys-color-on-secondary-container);
229
+ font-size: var(--ui-t-body, 13px);
230
+ }
231
+ .chip button {
232
+ height: 20px;
233
+ width: 20px;
234
+ padding: 0;
235
+ border: 0;
236
+ background: transparent;
237
+ }
238
+ .chip button md-icon {
239
+ --md-icon-size: 14px;
240
+ color: inherit;
241
+ }
242
+ .chips input {
243
+ flex: 1 1 140px;
244
+ width: auto;
245
+ height: 26px;
246
+ border: 0;
247
+ padding: 0 4px;
248
+ background: transparent;
249
+ outline: none;
250
+ }
251
+
252
+ /* A row of a section is a card: its line as the document reads it, then its fields. */
253
+ .entries {
254
+ display: flex;
255
+ flex-direction: column;
256
+ gap: var(--spacing-medium, 8px);
257
+ }
258
+ .entry {
259
+ display: flex;
260
+ flex-direction: column;
261
+ gap: var(--spacing-large, 12px);
262
+ }
263
+ .entry-head {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: var(--spacing-medium, 8px);
267
+ min-width: 0;
268
+ }
269
+ .entry-head strong {
270
+ min-width: 0;
271
+ overflow: hidden;
272
+ text-overflow: ellipsis;
273
+ white-space: nowrap;
274
+ font-size: var(--ui-t-body, 13px);
275
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
276
+ }
277
+ .entry-head strong.untitled {
278
+ font-weight: 400;
279
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
280
+ }
281
+ .entry-head .period {
282
+ flex: none;
283
+ font-size: var(--ui-t-small, 11.5px);
284
+ }
285
+ .entry-head button {
286
+ margin-left: auto;
287
+ }
288
+ .fields {
289
+ display: grid;
290
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
291
+ gap: var(--spacing-medium, 8px) var(--spacing-large, 12px);
292
+ }
293
+ .fields .wide {
294
+ grid-column: 1 / -1;
295
+ }
296
+ .fields .start {
297
+ grid-column-start: 1;
298
+ }
299
+ .end {
300
+ display: grid;
301
+ grid-template-columns: minmax(0, 1fr) auto;
302
+ gap: var(--spacing-medium, 8px);
303
+ align-items: end;
304
+ min-width: 0;
305
+ }
306
+ .ongoing {
307
+ flex-direction: row;
308
+ align-items: center;
309
+ gap: var(--spacing-small, 4px);
310
+ height: var(--form-element-height-medium, 32px);
311
+ font-size: var(--ui-t-body, 13px);
312
+ white-space: nowrap;
313
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
314
+ }
315
+ .ongoing input {
316
+ width: auto;
317
+ }
318
+
319
+ /* A section with nothing yet: what it is for, and the way to begin. */
320
+ .empty-slot {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: var(--spacing-large, 12px);
324
+ padding: var(--spacing-large, 12px) 16px;
325
+ border: 1px dashed var(--md-sys-color-outline-variant);
326
+ border-radius: var(--ui-r, 10px);
327
+ font-size: var(--ui-t-body, 13px);
328
+ }
329
+ .empty-slot button {
330
+ margin-left: auto;
331
+ }
332
+
333
+ @media (max-width: 720px) {
334
+ .hero {
335
+ grid-template-columns: 1fr;
336
+ }
337
+ .empty-slot {
338
+ flex-direction: column;
339
+ align-items: flex-start;
340
+ }
341
+ .empty-slot button {
342
+ margin-left: 0;
343
+ }
344
+ }
345
+ @media (pointer: coarse) {
346
+ input,
347
+ textarea {
348
+ font-size: 16px;
349
+ }
350
+ }
351
+ `
352
+ ]; }
353
+ get dirty() {
354
+ return JSON.stringify(profileInputOf(this.form)) !== this.saved;
355
+ }
356
+ get context() {
357
+ return {
358
+ title: i18next.t('title.my-employee-profile'),
359
+ frame: 'document',
360
+ actions: this.linked ? [{ title: i18next.t('button.save'), icon: 'save', emphasis: { raised: true }, action: () => this.save() }] : []
361
+ };
362
+ }
363
+ async pageUpdated(changes) {
364
+ if (this.active && changes.active)
365
+ this.load();
366
+ }
367
+ async load() {
368
+ this.loading = true;
369
+ this.error = null;
370
+ try {
371
+ const response = await client.query({
372
+ query: gql `
373
+ query {
374
+ myEmployees { id name jobPosition jobResponsibility hiredOn department { name } }
375
+ myEmployeeProfile { ${PROFILE_FIELDS} }
376
+ }
377
+ `,
378
+ fetchPolicy: 'no-cache'
379
+ });
380
+ if (response.errors?.length)
381
+ throw response.errors[0];
382
+ this.me = response.data?.myEmployees?.[0] ?? null;
383
+ this.linked = !!this.me;
384
+ this.hold(response.data?.myEmployeeProfile);
385
+ }
386
+ catch (error) {
387
+ this.error = error;
388
+ }
389
+ finally {
390
+ this.loading = false;
391
+ this.updateContext();
392
+ }
393
+ }
394
+ hold(profile) {
395
+ this.form = profileFormOf(profile);
396
+ this.saved = JSON.stringify(profileInputOf(this.form));
397
+ this.updatedAt = profile?.updatedAt ?? null;
398
+ }
399
+ async save() {
400
+ if (this.busy)
401
+ return false;
402
+ this.busy = true;
403
+ try {
404
+ const response = await client.mutate({
405
+ mutation: gql `
406
+ mutation ($profile: EmployeeProfileInput!) {
407
+ saveMyEmployeeProfile(profile: $profile) { ${PROFILE_FIELDS} }
408
+ }
409
+ `,
410
+ variables: { profile: profileInputOf(this.form) }
411
+ });
412
+ if (response.errors?.length)
413
+ throw response.errors[0];
414
+ this.hold(response.data.saveMyEmployeeProfile);
415
+ notify({ message: i18next.t('text.saved') });
416
+ return true;
417
+ }
418
+ catch (error) {
419
+ notify({ level: 'error', message: error?.message ?? String(error) });
420
+ return false;
421
+ }
422
+ finally {
423
+ this.busy = false;
424
+ }
425
+ }
426
+ /** The document as sales will hand it in - what is written is saved first, so the preview shows it. */
427
+ async preview() {
428
+ if (this.dirty && !(await this.save()))
429
+ return;
430
+ const languages = ((await getLanguages()) ?? []);
431
+ openProfilePreview({ own: true }, languages, readerLanguageOf(languages));
432
+ }
433
+ goTo(key) {
434
+ const section = this.renderRoot.querySelector(`#${key}`);
435
+ section?.scrollIntoView({ behavior: 'smooth', block: 'start' });
436
+ section?.querySelector('textarea, input:not([type="checkbox"])')?.focus({ preventScroll: true });
437
+ }
438
+ render() {
439
+ return html `<ox-page-state .loading=${this.loading && this.linked === null} .error=${this.linked === null ? this.error : null} @retry=${() => this.load()}>
440
+ ${this.linked === false
441
+ ? html `<ox-empty-note
442
+ icon="badge"
443
+ title=${i18next.t('text.employee-profile-not-linked-title')}
444
+ description=${i18next.t('text.employee-profile-not-linked-description')}
445
+ ></ox-empty-note>`
446
+ : this.linked
447
+ ? html `<div class="page">
448
+ ${this.renderHero()}
449
+ <div class="page-section-grid">${this.renderSummary()} ${this.renderSkills()}</div>
450
+ ${PROFILE_SECTIONS.map(section => this.renderSection(section))}
451
+ </div>`
452
+ : nothing}
453
+ </ox-page-state>`;
454
+ }
455
+ renderHero() {
456
+ const me = this.me ?? {};
457
+ const { percent, missing } = completenessOf(this.form);
458
+ const role = [me.jobPosition, me.jobResponsibility].filter(Boolean).join(' / ');
459
+ const day = (value) => (value ? String(value).slice(0, 10).replace(/-/g, '.') : '');
460
+ return html `<section class="hero page-card">
461
+ <div class="who">
462
+ <div class="avatar" aria-hidden="true">${(me.name ?? '?').trim().slice(0, 1)}</div>
463
+ <div>
464
+ <h2>${me.name ?? ''}</h2>
465
+ <p class="muted">${[me.department?.name, role].filter(Boolean).join(' · ')}</p>
466
+ ${me.hiredOn ? html `<p class="muted">${i18next.t('label.hired-on')} ${day(me.hiredOn)}</p>` : nothing}
467
+ </div>
468
+ </div>
469
+
470
+ <div class="progress">
471
+ <div class="progress-head">
472
+ <span class="muted">${i18next.t('text.employee-profile-completeness')}</span>
473
+ <strong>${percent}%</strong>
474
+ </div>
475
+ <div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow=${percent}><span style="width:${percent}%"></span></div>
476
+ ${missing.length
477
+ ? html `<div class="missing">
478
+ <span class="muted">${i18next.t('text.employee-profile-to-fill')}</span>
479
+ ${missing.map(key => html `<button small quiet @click=${() => this.goTo(key)}>${i18next.t(`label.employee-profile-${key}`)}</button>`)}
480
+ </div>`
481
+ : html `<div class="missing muted">${i18next.t('text.employee-profile-ready')}</div>`}
482
+ </div>
483
+
484
+ <div class="hero-foot">
485
+ ${this.dirty
486
+ ? html `<span class="unsaved">${i18next.t('text.employee-profile-unsaved')}</span>`
487
+ : this.updatedAt
488
+ ? html `<span class="muted">${i18next.t('label.employee-profile-updated-at')} ${day(this.updatedAt)}</span>`
489
+ : nothing}
490
+ <button ?disabled=${this.busy} @click=${() => this.preview()}><md-icon>preview</md-icon>${i18next.t('button.employee-profile-preview')}</button>
491
+ </div>
492
+ </section>`;
493
+ }
494
+ renderSummary() {
495
+ const summary = this.form.summary;
496
+ return html `<section id="summary" class="page-section summary">
497
+ <header class="page-section-header">
498
+ <h3><md-icon>person</md-icon>${i18next.t('label.employee-profile-summary')}</h3>
499
+ <span class="counter muted">${summary.length} / ${SUMMARY_LIMIT}</span>
500
+ </header>
501
+ <textarea
502
+ aria-label=${i18next.t('label.employee-profile-summary')}
503
+ maxlength=${SUMMARY_LIMIT}
504
+ placeholder=${i18next.t('text.employee-profile-summary-example')}
505
+ .value=${summary}
506
+ @input=${(e) => (this.form = { ...this.form, summary: e.target.value })}
507
+ ></textarea>
508
+ </section>`;
509
+ }
510
+ renderSkills() {
511
+ const skills = this.form.skills;
512
+ const add = (input) => {
513
+ this.form = { ...this.form, skills: addSkills(skills, input.value) };
514
+ input.value = '';
515
+ };
516
+ return html `<section id="skills" class="page-section">
517
+ <header class="page-section-header">
518
+ <h3><md-icon>construction</md-icon>${i18next.t('label.employee-profile-skills')}</h3>
519
+ ${skills.length ? html `<span class="count">${skills.length}</span>` : nothing}
520
+ </header>
521
+ <div class="chips" @click=${(e) => e.currentTarget.querySelector('input')?.focus()}>
522
+ ${skills.map(skill => html `<span class="chip"
523
+ >${skill}
524
+ <button
525
+ unstyled
526
+ title=${i18next.t('button.delete')}
527
+ aria-label=${`${i18next.t('button.delete')} ${skill}`}
528
+ @click=${() => (this.form = { ...this.form, skills: skills.filter(one => one !== skill) })}
529
+ >
530
+ <md-icon>close</md-icon>
531
+ </button></span
532
+ >`)}
533
+ <input
534
+ aria-label=${i18next.t('label.employee-profile-skills')}
535
+ placeholder=${i18next.t('text.employee-profile-skill-placeholder')}
536
+ @keydown=${(e) => {
537
+ const input = e.target;
538
+ if ((e.key === 'Enter' || e.key === ',') && !e.isComposing) {
539
+ e.preventDefault();
540
+ add(input);
541
+ }
542
+ else if (e.key === 'Backspace' && !input.value && skills.length) {
543
+ this.form = { ...this.form, skills: skills.slice(0, -1) };
544
+ }
545
+ }}
546
+ @blur=${(e) => add(e.target)}
547
+ />
548
+ </div>
549
+ </section>`;
550
+ }
551
+ renderSection(section) {
552
+ const rows = this.form[section.key];
553
+ /* Each change reads the rows as they are now - two inputs before a render must not undo each other. */
554
+ const current = () => this.form[section.key];
555
+ const setRows = (next) => (this.form = { ...this.form, [section.key]: next });
556
+ const change = (index, patch) => setRows(current().map((row, at) => (at === index ? { ...row, ...patch } : row)));
557
+ const remove = (index) => setRows(current().filter((_, at) => at !== index));
558
+ /* The newest first, as the document is read; its first field takes the cursor. */
559
+ const add = async () => {
560
+ setRows([blankRow(section), ...current()]);
561
+ await this.updateComplete;
562
+ this.renderRoot.querySelector(`#${section.key} .entry input`)?.focus();
563
+ };
564
+ return html `<section id=${section.key} class="page-section">
565
+ <header class="page-section-header">
566
+ <h3><md-icon>${section.icon}</md-icon>${i18next.t(section.title)}</h3>
567
+ ${rows.length ? html `<span class="count">${rows.length}</span>` : nothing}
568
+ ${rows.length
569
+ ? html `<div class="page-section-actions">
570
+ <button @click=${add}><md-icon>add</md-icon>${i18next.t('button.add')}</button>
571
+ </div>`
572
+ : nothing}
573
+ </header>
574
+ ${rows.length
575
+ ? html `<div class="entries">${rows.map((row, index) => this.renderEntry(section, row, index, change, () => remove(index)))}</div>`
576
+ : html `<div class="empty-slot">
577
+ <span class="muted">${i18next.t(`text.employee-profile-why-${section.key}`)}</span>
578
+ <button @click=${add}><md-icon>add</md-icon>${i18next.t('button.add')}</button>
579
+ </div>`}
580
+ </section>`;
581
+ }
582
+ renderEntry(section, row, index, change, remove) {
583
+ const headline = headlineOf(section, row);
584
+ const period = periodLabelOf(row, i18next.t('text.employee-profile-doc-ongoing'));
585
+ const field = (one) => {
586
+ const label = i18next.t(one.label);
587
+ if (one.input === 'textarea') {
588
+ return html `<label class="wide"
589
+ >${label}<textarea .value=${row[one.name] ?? ''} @input=${(e) => change(index, { [one.name]: e.target.value })}></textarea
590
+ ></label>`;
591
+ }
592
+ const ended = one.name === 'endedOn' && section.ongoing;
593
+ const input = html `<input
594
+ type=${one.input ?? 'text'}
595
+ .value=${ended && row.ongoing ? '' : (row[one.name] ?? '')}
596
+ ?disabled=${ended && !!row.ongoing}
597
+ @input=${(e) => change(index, { [one.name]: e.target.value })}
598
+ />`;
599
+ /* A period begins a line of its own, its end beside it. */
600
+ if (!ended)
601
+ return html `<label class=${one.name === 'startedOn' ? 'start' : ''}>${label}${input}</label>`;
602
+ /* The end and 「going on」 share one cell - the period stays on one line. */
603
+ return html `<div class="end">
604
+ <label>${label}${input}</label>
605
+ <label class="ongoing"
606
+ ><input
607
+ type="checkbox"
608
+ .checked=${!!row.ongoing}
609
+ @change=${(e) => change(index, { ongoing: e.target.checked ? 'true' : '', ...(e.target.checked ? { endedOn: '' } : {}) })}
610
+ />${i18next.t('label.employee-profile-ongoing')}</label
611
+ >
612
+ </div>`;
613
+ };
614
+ return html `<div class="entry page-card">
615
+ <div class="entry-head">
616
+ <strong class=${headline ? '' : 'untitled'}>${headline || i18next.t('text.employee-profile-new-entry')}</strong>
617
+ ${period ? html `<span class="period muted">${period}</span>` : nothing}
618
+ <button quiet icon-only title=${i18next.t('button.delete')} aria-label=${i18next.t('button.delete')} @click=${remove}><md-icon>delete</md-icon></button>
619
+ </div>
620
+ <div class="fields">${section.fields.map(field)}</div>
621
+ </div>`;
622
+ }
623
+ };
624
+ __decorate([
625
+ state(),
626
+ __metadata("design:type", Object)
627
+ ], MyEmployeeProfilePage.prototype, "form", void 0);
628
+ __decorate([
629
+ state(),
630
+ __metadata("design:type", Object)
631
+ ], MyEmployeeProfilePage.prototype, "me", void 0);
632
+ __decorate([
633
+ state(),
634
+ __metadata("design:type", Object)
635
+ ], MyEmployeeProfilePage.prototype, "linked", void 0);
636
+ __decorate([
637
+ state(),
638
+ __metadata("design:type", Object)
639
+ ], MyEmployeeProfilePage.prototype, "loading", void 0);
640
+ __decorate([
641
+ state(),
642
+ __metadata("design:type", Object)
643
+ ], MyEmployeeProfilePage.prototype, "error", void 0);
644
+ __decorate([
645
+ state(),
646
+ __metadata("design:type", Object)
647
+ ], MyEmployeeProfilePage.prototype, "busy", void 0);
648
+ __decorate([
649
+ state(),
650
+ __metadata("design:type", Object)
651
+ ], MyEmployeeProfilePage.prototype, "updatedAt", void 0);
652
+ MyEmployeeProfilePage = __decorate([
653
+ customElement('my-employee-profile-page')
654
+ ], MyEmployeeProfilePage);
655
+ export { MyEmployeeProfilePage };
656
+ //# sourceMappingURL=my-employee-profile-page.js.map