@things-factory/organization 10.1.222 → 10.1.226

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 (54) hide show
  1. package/dist-client/pages/employee-profile/employee-profile-form.d.ts +31 -0
  2. package/dist-client/pages/employee-profile/employee-profile-form.js +86 -0
  3. package/dist-client/pages/employee-profile/employee-profile-form.js.map +1 -0
  4. package/dist-client/pages/employee-profile/employee-profile-list-page.d.ts +38 -0
  5. package/dist-client/pages/employee-profile/employee-profile-list-page.js +164 -0
  6. package/dist-client/pages/employee-profile/employee-profile-list-page.js.map +1 -0
  7. package/dist-client/pages/employee-profile/employee-profile-workbook.d.ts +33 -0
  8. package/dist-client/pages/employee-profile/employee-profile-workbook.js +87 -0
  9. package/dist-client/pages/employee-profile/employee-profile-workbook.js.map +1 -0
  10. package/dist-client/pages/employee-profile/my-employee-profile-page.d.ts +38 -0
  11. package/dist-client/pages/employee-profile/my-employee-profile-page.js +282 -0
  12. package/dist-client/pages/employee-profile/my-employee-profile-page.js.map +1 -0
  13. package/dist-client/route.d.ts +1 -1
  14. package/dist-client/route.js +7 -0
  15. package/dist-client/route.js.map +1 -1
  16. package/dist-client/tsconfig.tsbuildinfo +1 -1
  17. package/dist-server/role-tasks.js +4 -1
  18. package/dist-server/role-tasks.js.map +1 -1
  19. package/dist-server/service/employee-profile/employee-profile-mutation.d.ts +5 -0
  20. package/dist-server/service/employee-profile/employee-profile-mutation.js +45 -0
  21. package/dist-server/service/employee-profile/employee-profile-mutation.js.map +1 -0
  22. package/dist-server/service/employee-profile/employee-profile-query.d.ts +6 -0
  23. package/dist-server/service/employee-profile/employee-profile-query.js +58 -0
  24. package/dist-server/service/employee-profile/employee-profile-query.js.map +1 -0
  25. package/dist-server/service/employee-profile/employee-profile-rows.d.ts +29 -0
  26. package/dist-server/service/employee-profile/employee-profile-rows.js +122 -0
  27. package/dist-server/service/employee-profile/employee-profile-rows.js.map +1 -0
  28. package/dist-server/service/employee-profile/employee-profile-rules.d.ts +55 -0
  29. package/dist-server/service/employee-profile/employee-profile-rules.js +80 -0
  30. package/dist-server/service/employee-profile/employee-profile-rules.js.map +1 -0
  31. package/dist-server/service/employee-profile/employee-profile-type.d.ts +24 -0
  32. package/dist-server/service/employee-profile/employee-profile-type.js +81 -0
  33. package/dist-server/service/employee-profile/employee-profile-type.js.map +1 -0
  34. package/dist-server/service/employee-profile/employee-profile.d.ts +24 -0
  35. package/dist-server/service/employee-profile/employee-profile.js +112 -0
  36. package/dist-server/service/employee-profile/employee-profile.js.map +1 -0
  37. package/dist-server/service/employee-profile/index.d.ts +5 -0
  38. package/dist-server/service/employee-profile/index.js +9 -0
  39. package/dist-server/service/employee-profile/index.js.map +1 -0
  40. package/dist-server/service/index.d.ts +3 -2
  41. package/dist-server/service/index.js +6 -2
  42. package/dist-server/service/index.js.map +1 -1
  43. package/dist-server/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +7 -6
  45. package/tests/employee-profile-db.test.ts +186 -0
  46. package/tests/employee-profile-rules.test.ts +76 -0
  47. package/tests/employee-profile-workbook.test.ts +89 -0
  48. package/things-factory.config.js +3 -0
  49. package/translations/en.json +38 -1
  50. package/translations/es.json +38 -1
  51. package/translations/ja.json +38 -1
  52. package/translations/ko.json +38 -1
  53. package/translations/ms.json +38 -1
  54. package/translations/zh.json +38 -1
@@ -0,0 +1,282 @@
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 { PROFILE_SECTIONS, profileFormOf, profileInputOf } from './employee-profile-form';
14
+ const PROFILE_FIELDS = `
15
+ id
16
+ summary
17
+ skills
18
+ educations { school major degree startedOn endedOn }
19
+ certificates { name issuer acquiredOn }
20
+ careers { company department position startedOn endedOn duties }
21
+ projects { name client startedOn endedOn role duties technologies }
22
+ updatedAt
23
+ `;
24
+ /**
25
+ * 내 프로필 — the employee writes their own career profile: a summary, skills, and the rows of education,
26
+ * certificates, past jobs and projects; sales attaches it to quotations (ADR-0169 decisions 2 · 5). Saved whole.
27
+ * An account not linked to an employee record has no profile to write, and is told so before anything is typed.
28
+ */
29
+ let MyEmployeeProfilePage = class MyEmployeeProfilePage extends localize(i18next)(PageView) {
30
+ constructor() {
31
+ super(...arguments);
32
+ this.form = profileFormOf(null);
33
+ /** Whether the account is linked to an employee record - unknown until read. */
34
+ this.linked = null;
35
+ this.loading = false;
36
+ this.error = null;
37
+ this.busy = false;
38
+ }
39
+ static { this.styles = [
40
+ FormFieldStyles,
41
+ ButtonStyles,
42
+ DocumentPageStyles,
43
+ css `
44
+ label {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: var(--spacing-small, 4px);
48
+ font-size: var(--ui-t-micro, 11px);
49
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
50
+ }
51
+ label > :is(input, textarea) {
52
+ font-size: var(--ui-t-body, 13px);
53
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
54
+ }
55
+ input,
56
+ textarea {
57
+ box-sizing: border-box;
58
+ width: 100%;
59
+ font-family: inherit;
60
+ }
61
+ input {
62
+ height: var(--form-element-height-medium, 32px);
63
+ }
64
+ textarea {
65
+ min-height: 72px;
66
+ resize: vertical;
67
+ }
68
+ textarea.summary {
69
+ min-height: 120px;
70
+ }
71
+ .hint {
72
+ font-size: var(--ui-t-small, 11.5px);
73
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
74
+ }
75
+ /* A row of a section: its fields in a grid, the button that takes it away at its end. */
76
+ .row {
77
+ display: grid;
78
+ grid-template-columns: 1fr auto;
79
+ gap: var(--spacing-large, 12px);
80
+ align-items: start;
81
+ padding: var(--spacing-large, 12px) 0;
82
+ border-top: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
83
+ }
84
+ .row:first-of-type {
85
+ border-top: 0;
86
+ padding-top: 0;
87
+ }
88
+ .fields {
89
+ display: grid;
90
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
91
+ gap: var(--spacing-medium, 8px) var(--spacing-large, 12px);
92
+ }
93
+ .fields .wide {
94
+ grid-column: 1 / -1;
95
+ }
96
+ .row > button {
97
+ margin-top: 18px;
98
+ }
99
+ .empty {
100
+ margin: 0;
101
+ font-size: var(--ui-t-body, 13px);
102
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
103
+ }
104
+ @media (pointer: coarse) {
105
+ input,
106
+ textarea {
107
+ font-size: 16px;
108
+ }
109
+ }
110
+ `
111
+ ]; }
112
+ get context() {
113
+ return {
114
+ title: i18next.t('title.my-employee-profile'),
115
+ frame: 'document',
116
+ actions: this.linked && !this.busy ? [{ title: i18next.t('button.save'), icon: 'save', emphasis: { raised: true }, action: () => this.save() }] : []
117
+ };
118
+ }
119
+ async pageUpdated(changes) {
120
+ if (this.active && changes.active)
121
+ this.load();
122
+ }
123
+ async load() {
124
+ this.loading = true;
125
+ this.error = null;
126
+ try {
127
+ const response = await client.query({
128
+ query: gql `
129
+ query {
130
+ myEmployees { id }
131
+ myEmployeeProfile { ${PROFILE_FIELDS} }
132
+ }
133
+ `,
134
+ fetchPolicy: 'no-cache'
135
+ });
136
+ if (response.errors?.length)
137
+ throw response.errors[0];
138
+ this.linked = (response.data?.myEmployees ?? []).length > 0;
139
+ this.form = profileFormOf(response.data?.myEmployeeProfile);
140
+ }
141
+ catch (error) {
142
+ this.error = error;
143
+ }
144
+ finally {
145
+ this.loading = false;
146
+ this.updateContext();
147
+ }
148
+ }
149
+ async save() {
150
+ this.busy = true;
151
+ this.updateContext();
152
+ try {
153
+ const response = await client.mutate({
154
+ mutation: gql `
155
+ mutation ($profile: EmployeeProfileInput!) {
156
+ saveMyEmployeeProfile(profile: $profile) { ${PROFILE_FIELDS} }
157
+ }
158
+ `,
159
+ variables: { profile: profileInputOf(this.form) }
160
+ });
161
+ if (response.errors?.length)
162
+ throw response.errors[0];
163
+ this.form = profileFormOf(response.data.saveMyEmployeeProfile);
164
+ notify({ message: i18next.t('text.saved') });
165
+ }
166
+ catch (error) {
167
+ notify({ level: 'error', message: error?.message ?? String(error) });
168
+ }
169
+ finally {
170
+ this.busy = false;
171
+ this.updateContext();
172
+ }
173
+ }
174
+ render() {
175
+ const form = this.form;
176
+ const set = (patch) => (this.form = { ...form, ...patch });
177
+ return html `<ox-page-state .loading=${this.loading && this.linked === null} .error=${this.linked === null ? this.error : null} @retry=${() => this.load()}>
178
+ ${this.linked === false
179
+ ? html `<ox-empty-note
180
+ icon="badge"
181
+ title=${i18next.t('text.employee-profile-not-linked-title')}
182
+ description=${i18next.t('text.employee-profile-not-linked-description')}
183
+ ></ox-empty-note>`
184
+ : this.linked
185
+ ? html `<div class="page">
186
+ <section class="page-section">
187
+ <header class="page-section-header"><h3>${i18next.t('label.employee-profile-summary')}</h3></header>
188
+ <textarea
189
+ class="summary"
190
+ aria-label=${i18next.t('label.employee-profile-summary')}
191
+ .value=${form.summary}
192
+ ?disabled=${this.busy}
193
+ @input=${(e) => set({ summary: e.target.value })}
194
+ ></textarea>
195
+ </section>
196
+
197
+ <section class="page-section">
198
+ <header class="page-section-header"><h3>${i18next.t('label.employee-profile-skills')}</h3></header>
199
+ <input
200
+ aria-label=${i18next.t('label.employee-profile-skills')}
201
+ .value=${form.skills}
202
+ ?disabled=${this.busy}
203
+ @input=${(e) => set({ skills: e.target.value })}
204
+ />
205
+ <span class="hint">${i18next.t('text.employee-profile-skills-hint')}</span>
206
+ </section>
207
+
208
+ ${PROFILE_SECTIONS.map(section => this.renderSection(section.key, section.title, section.fields))}
209
+ </div>`
210
+ : nothing}
211
+ </ox-page-state>`;
212
+ }
213
+ renderSection(key, title, fields) {
214
+ const rows = this.form[key];
215
+ const setRows = (next) => (this.form = { ...this.form, [key]: next });
216
+ const change = (index, name, value) => setRows(rows.map((row, at) => (at === index ? { ...row, [name]: value } : row)));
217
+ const blank = () => Object.fromEntries(fields.map(field => [field.name, '']));
218
+ return html `<section class="page-section">
219
+ <header class="page-section-header">
220
+ <h3>${i18next.t(title)}</h3>
221
+ <div class="page-section-actions">
222
+ <button ?disabled=${this.busy} @click=${() => setRows([...rows, blank()])}><md-icon>add</md-icon>${i18next.t('button.add')}</button>
223
+ </div>
224
+ </header>
225
+ ${rows.length
226
+ ? rows.map((row, index) => html `<div class="row">
227
+ <div class="fields">
228
+ ${fields.map(field => field.input === 'textarea'
229
+ ? html `<label class="wide"
230
+ >${i18next.t(field.label)}
231
+ <textarea .value=${row[field.name] ?? ''} ?disabled=${this.busy} @input=${(e) => change(index, field.name, e.target.value)}></textarea>
232
+ </label>`
233
+ : html `<label
234
+ >${i18next.t(field.label)}
235
+ <input
236
+ type=${field.input ?? 'text'}
237
+ .value=${row[field.name] ?? ''}
238
+ ?disabled=${this.busy}
239
+ @input=${(e) => change(index, field.name, e.target.value)}
240
+ />
241
+ </label>`)}
242
+ </div>
243
+ <button
244
+ quiet
245
+ icon-only
246
+ title=${i18next.t('button.delete')}
247
+ aria-label=${i18next.t('button.delete')}
248
+ ?disabled=${this.busy}
249
+ @click=${() => setRows(rows.filter((_, at) => at !== index))}
250
+ >
251
+ <md-icon>close</md-icon>
252
+ </button>
253
+ </div>`)
254
+ : html `<p class="empty">${i18next.t('text.employee-profile-no-rows')}</p>`}
255
+ </section>`;
256
+ }
257
+ };
258
+ __decorate([
259
+ state(),
260
+ __metadata("design:type", Object)
261
+ ], MyEmployeeProfilePage.prototype, "form", void 0);
262
+ __decorate([
263
+ state(),
264
+ __metadata("design:type", Object)
265
+ ], MyEmployeeProfilePage.prototype, "linked", void 0);
266
+ __decorate([
267
+ state(),
268
+ __metadata("design:type", Object)
269
+ ], MyEmployeeProfilePage.prototype, "loading", void 0);
270
+ __decorate([
271
+ state(),
272
+ __metadata("design:type", Object)
273
+ ], MyEmployeeProfilePage.prototype, "error", void 0);
274
+ __decorate([
275
+ state(),
276
+ __metadata("design:type", Object)
277
+ ], MyEmployeeProfilePage.prototype, "busy", void 0);
278
+ MyEmployeeProfilePage = __decorate([
279
+ customElement('my-employee-profile-page')
280
+ ], MyEmployeeProfilePage);
281
+ export { MyEmployeeProfilePage };
282
+ //# sourceMappingURL=my-employee-profile-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"my-employee-profile-page.js","sourceRoot":"","sources":["../../../client/pages/employee-profile/my-employee-profile-page.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,sCAAsC,CAAA;AAC7C,OAAO,sCAAsC,CAAA;AAE7C,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEnF,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAA+D,MAAM,yBAAyB,CAAA;AAEtJ,MAAM,cAAc,GAAG;;;;;;;;;CAStB,CAAA;AAED;;;;GAIG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAsB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;IAA/D;;QA2EI,SAAI,GAAgB,aAAa,CAAC,IAAI,CAAC,CAAA;QAChD,gFAAgF;QACvE,WAAM,GAAmB,IAAI,CAAA;QAC7B,YAAO,GAAG,KAAK,CAAA;QACf,UAAK,GAAY,IAAI,CAAA;QACrB,SAAI,GAAG,KAAK,CAAA;IAuJvB,CAAC;aAtOQ,WAAM,GAAG;QACd,eAAe;QACf,YAAY;QACZ,kBAAkB;QAClB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAmEF;KACF,AAxEY,CAwEZ;IASD,IAAI,OAAO;QACT,OAAO;YACL,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;YAC7C,KAAK,EAAE,UAAU;YACjB,OAAO,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;SACrJ,CAAA;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAY;QAC5B,IAAK,IAAY,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM;YAAE,IAAI,CAAC,IAAI,EAAE,CAAA;IACzD,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;gBAClC,KAAK,EAAE,GAAG,CAAA;;;kCAGgB,cAAc;;SAEvC;gBACD,WAAW,EAAE,UAAU;aACxB,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YACrD,IAAI,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;YAC3D,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAA;QAC7D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QACpB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA;YACpB,IAAI,CAAC,aAAa,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,aAAa,EAAE,CAAA;QACpB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,GAAG,CAAA;;yDAEoC,cAAc;;SAE9D;gBACD,SAAS,EAAE,EAAE,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aAClD,CAAC,CAAA;YACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YACrD,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;YAC9D,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;QAC9C,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QACtE,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,KAAK,CAAA;YACjB,IAAI,CAAC,aAAa,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAED,MAAM;QACJ,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAA;QACtB,MAAM,GAAG,GAAG,CAAC,KAA2B,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;QAEhF,OAAO,IAAI,CAAA,2BAA2B,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,WAAW,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;QACrJ,IAAI,CAAC,MAAM,KAAK,KAAK;YACrB,CAAC,CAAC,IAAI,CAAA;;oBAEM,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC;0BAC7C,OAAO,CAAC,CAAC,CAAC,8CAA8C,CAAC;4BACvD;YACpB,CAAC,CAAC,IAAI,CAAC,MAAM;gBACX,CAAC,CAAC,IAAI,CAAA;;0DAE0C,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC;;;+BAGtE,OAAO,CAAC,CAAC,CAAC,gCAAgC,CAAC;2BAC/C,IAAI,CAAC,OAAO;8BACT,IAAI,CAAC,IAAI;2BACZ,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;;;;;0DAKb,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;;+BAErE,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;2BAC9C,IAAI,CAAC,MAAM;8BACR,IAAI,CAAC,IAAI;2BACZ,CAAC,CAAM,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;;qCAEjC,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC;;;gBAGnE,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;mBAC5F;gBACT,CAAC,CAAC,OAAO;qBACE,CAAA;IACnB,CAAC;IAEO,aAAa,CAAC,GAAsB,EAAE,KAAa,EAAE,MAAsB;QACjF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,OAAO,GAAG,CAAC,IAAiB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;QAClF,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,IAAY,EAAE,KAAa,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC/I,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;QAE7E,OAAO,IAAI,CAAA;;cAED,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;;8BAEA,IAAI,CAAC,IAAI,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,0BAA0B,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;;;QAG5H,IAAI,CAAC,MAAM;YACX,CAAC,CAAC,IAAI,CAAC,GAAG,CACN,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAA;;kBAEd,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CACnB,KAAK,CAAC,KAAK,KAAK,UAAU;gBACxB,CAAC,CAAC,IAAI,CAAA;2BACC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;2CACN,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,IAAI,CAAC,IAAI,WAAW,CAAC,CAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;+BACxH;gBACX,CAAC,CAAC,IAAI,CAAA;2BACC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;;iCAEhB,KAAK,CAAC,KAAK,IAAI,MAAM;mCACnB,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;sCAClB,IAAI,CAAC,IAAI;mCACZ,CAAC,CAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;+BAEzD,CACd;;;;;wBAKO,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;6BACrB,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;4BAC3B,IAAI,CAAC,IAAI;yBACZ,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC;;;;mBAIzD,CACR;YACH,CAAC,CAAC,IAAI,CAAA,oBAAoB,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC,MAAM;eACnE,CAAA;IACb,CAAC;;AA3JQ;IAAR,KAAK,EAAE;;mDAAwC;AAEvC;IAAR,KAAK,EAAE;;qDAA8B;AAC7B;IAAR,KAAK,EAAE;;sDAAgB;AACf;IAAR,KAAK,EAAE;;oDAAsB;AACrB;IAAR,KAAK,EAAE;;mDAAa;AAhFV,qBAAqB;IADjC,aAAa,CAAC,0BAA0B,CAAC;GAC7B,qBAAqB,CAuOjC","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/list-state/ox-empty-note.js'\nimport '@operato/list-state/ox-page-state.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, nothing } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { PageView } from '@operato/shell'\nimport { ButtonStyles, DocumentPageStyles, FormFieldStyles } from '@operato/styles'\n\nimport { PROFILE_SECTIONS, profileFormOf, profileInputOf, type ProfileField, type ProfileForm, type ProfileSectionKey } from './employee-profile-form'\n\nconst PROFILE_FIELDS = `\n id\n summary\n skills\n educations { school major degree startedOn endedOn }\n certificates { name issuer acquiredOn }\n careers { company department position startedOn endedOn duties }\n projects { name client startedOn endedOn role duties technologies }\n updatedAt\n`\n\n/**\n * 내 프로필 — the employee writes their own career profile: a summary, skills, and the rows of education,\n * certificates, past jobs and projects; sales attaches it to quotations (ADR-0169 decisions 2 · 5). Saved whole.\n * An account not linked to an employee record has no profile to write, and is told so before anything is typed.\n */\n@customElement('my-employee-profile-page')\nexport class MyEmployeeProfilePage extends localize(i18next)(PageView) {\n static styles = [\n FormFieldStyles,\n ButtonStyles,\n DocumentPageStyles,\n css`\n label {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-small, 4px);\n font-size: var(--ui-t-micro, 11px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n label > :is(input, textarea) {\n font-size: var(--ui-t-body, 13px);\n color: var(--ui-ink, var(--md-sys-color-on-surface));\n }\n input,\n textarea {\n box-sizing: border-box;\n width: 100%;\n font-family: inherit;\n }\n input {\n height: var(--form-element-height-medium, 32px);\n }\n textarea {\n min-height: 72px;\n resize: vertical;\n }\n textarea.summary {\n min-height: 120px;\n }\n .hint {\n font-size: var(--ui-t-small, 11.5px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n /* A row of a section: its fields in a grid, the button that takes it away at its end. */\n .row {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: var(--spacing-large, 12px);\n align-items: start;\n padding: var(--spacing-large, 12px) 0;\n border-top: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));\n }\n .row:first-of-type {\n border-top: 0;\n padding-top: 0;\n }\n .fields {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n gap: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n }\n .fields .wide {\n grid-column: 1 / -1;\n }\n .row > button {\n margin-top: 18px;\n }\n .empty {\n margin: 0;\n font-size: var(--ui-t-body, 13px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n @media (pointer: coarse) {\n input,\n textarea {\n font-size: 16px;\n }\n }\n `\n ]\n\n @state() form: ProfileForm = profileFormOf(null)\n /** Whether the account is linked to an employee record - unknown until read. */\n @state() linked: boolean | null = null\n @state() loading = false\n @state() error: unknown = null\n @state() busy = false\n\n get context() {\n return {\n title: i18next.t('title.my-employee-profile'),\n frame: 'document',\n actions: this.linked && !this.busy ? [{ title: i18next.t('button.save'), icon: 'save', emphasis: { raised: true }, action: () => this.save() }] : []\n }\n }\n\n async pageUpdated(changes: any) {\n if ((this as any).active && changes.active) this.load()\n }\n\n private async load() {\n this.loading = true\n this.error = null\n try {\n const response = await client.query({\n query: gql`\n query {\n myEmployees { id }\n myEmployeeProfile { ${PROFILE_FIELDS} }\n }\n `,\n fetchPolicy: 'no-cache'\n })\n if (response.errors?.length) throw response.errors[0]\n this.linked = (response.data?.myEmployees ?? []).length > 0\n this.form = profileFormOf(response.data?.myEmployeeProfile)\n } catch (error) {\n this.error = error\n } finally {\n this.loading = false\n this.updateContext()\n }\n }\n\n private async save() {\n this.busy = true\n this.updateContext()\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($profile: EmployeeProfileInput!) {\n saveMyEmployeeProfile(profile: $profile) { ${PROFILE_FIELDS} }\n }\n `,\n variables: { profile: profileInputOf(this.form) }\n })\n if (response.errors?.length) throw response.errors[0]\n this.form = profileFormOf(response.data.saveMyEmployeeProfile)\n notify({ message: i18next.t('text.saved') })\n } catch (error: any) {\n notify({ level: 'error', message: error?.message ?? String(error) })\n } finally {\n this.busy = false\n this.updateContext()\n }\n }\n\n render() {\n const form = this.form\n const set = (patch: Partial<ProfileForm>) => (this.form = { ...form, ...patch })\n\n return html`<ox-page-state .loading=${this.loading && this.linked === null} .error=${this.linked === null ? this.error : null} @retry=${() => this.load()}>\n ${this.linked === false\n ? html`<ox-empty-note\n icon=\"badge\"\n title=${i18next.t('text.employee-profile-not-linked-title')}\n description=${i18next.t('text.employee-profile-not-linked-description')}\n ></ox-empty-note>`\n : this.linked\n ? html`<div class=\"page\">\n <section class=\"page-section\">\n <header class=\"page-section-header\"><h3>${i18next.t('label.employee-profile-summary')}</h3></header>\n <textarea\n class=\"summary\"\n aria-label=${i18next.t('label.employee-profile-summary')}\n .value=${form.summary}\n ?disabled=${this.busy}\n @input=${(e: any) => set({ summary: e.target.value })}\n ></textarea>\n </section>\n\n <section class=\"page-section\">\n <header class=\"page-section-header\"><h3>${i18next.t('label.employee-profile-skills')}</h3></header>\n <input\n aria-label=${i18next.t('label.employee-profile-skills')}\n .value=${form.skills}\n ?disabled=${this.busy}\n @input=${(e: any) => set({ skills: e.target.value })}\n />\n <span class=\"hint\">${i18next.t('text.employee-profile-skills-hint')}</span>\n </section>\n\n ${PROFILE_SECTIONS.map(section => this.renderSection(section.key, section.title, section.fields))}\n </div>`\n : nothing}\n </ox-page-state>`\n }\n\n private renderSection(key: ProfileSectionKey, title: string, fields: ProfileField[]) {\n const rows = this.form[key]\n const setRows = (next: typeof rows) => (this.form = { ...this.form, [key]: next })\n const change = (index: number, name: string, value: string) => setRows(rows.map((row, at) => (at === index ? { ...row, [name]: value } : row)))\n const blank = () => Object.fromEntries(fields.map(field => [field.name, '']))\n\n return html`<section class=\"page-section\">\n <header class=\"page-section-header\">\n <h3>${i18next.t(title)}</h3>\n <div class=\"page-section-actions\">\n <button ?disabled=${this.busy} @click=${() => setRows([...rows, blank()])}><md-icon>add</md-icon>${i18next.t('button.add')}</button>\n </div>\n </header>\n ${rows.length\n ? rows.map(\n (row, index) => html`<div class=\"row\">\n <div class=\"fields\">\n ${fields.map(field =>\n field.input === 'textarea'\n ? html`<label class=\"wide\"\n >${i18next.t(field.label)}\n <textarea .value=${row[field.name] ?? ''} ?disabled=${this.busy} @input=${(e: any) => change(index, field.name, e.target.value)}></textarea>\n </label>`\n : html`<label\n >${i18next.t(field.label)}\n <input\n type=${field.input ?? 'text'}\n .value=${row[field.name] ?? ''}\n ?disabled=${this.busy}\n @input=${(e: any) => change(index, field.name, e.target.value)}\n />\n </label>`\n )}\n </div>\n <button\n quiet\n icon-only\n title=${i18next.t('button.delete')}\n aria-label=${i18next.t('button.delete')}\n ?disabled=${this.busy}\n @click=${() => setRows(rows.filter((_, at) => at !== index))}\n >\n <md-icon>close</md-icon>\n </button>\n </div>`\n )\n : html`<p class=\"empty\">${i18next.t('text.employee-profile-no-rows')}</p>`}\n </section>`\n }\n}\n"]}
@@ -1 +1 @@
1
- export default function route(page: string): "approval-line-list" | "common-approval-line-templates-page" | "my-approval-line-templates-page" | "employee-list" | "department-list" | undefined;
1
+ export default function route(page: string): "approval-line-list" | "common-approval-line-templates-page" | "my-approval-line-templates-page" | "employee-list" | "department-list" | "my-employee-profile" | "employee-profile-list" | undefined;
@@ -6,6 +6,13 @@ export default function route(page) {
6
6
  case 'department-list':
7
7
  import('./pages/department/department-list-page');
8
8
  return page;
9
+ /* The employee writes their own profile; sales downloads the chosen ones for a quotation (ADR-0169). */
10
+ case 'my-employee-profile':
11
+ import('./pages/employee-profile/my-employee-profile-page');
12
+ return page;
13
+ case 'employee-profile-list':
14
+ import('./pages/employee-profile/employee-profile-list-page');
15
+ return page;
9
16
  /* 공용과 내 것을 한 목록에 — 아래 둘을 대신한다. */
10
17
  case 'approval-line-list':
11
18
  import('./pages/approval-line/approval-line-list-page');
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../client/route.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,IAAY;IACxC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,eAAe;YAClB,MAAM,CAAC,qCAAqC,CAAC,CAAA;YAC7C,OAAO,IAAI,CAAA;QAEb,KAAK,iBAAiB;YACpB,MAAM,CAAC,yCAAyC,CAAC,CAAA;YACjD,OAAO,IAAI,CAAA;QAEb,kCAAkC;QAClC,KAAK,oBAAoB;YACvB,MAAM,CAAC,+CAA+C,CAAC,CAAA;YACvD,OAAO,IAAI,CAAA;QAEb;;;;WAIG;QACH,KAAK,qCAAqC;YACxC,MAAM,CAAC,2DAA2D,CAAC,CAAA;YACnE,OAAO,IAAI,CAAA;QAEb,KAAK,iCAAiC;YACpC,MAAM,CAAC,uDAAuD,CAAC,CAAA;YAC/D,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["export default function route(page: string) {\n switch (page) {\n case 'employee-list':\n import('./pages/employee/employee-list-page')\n return page\n\n case 'department-list':\n import('./pages/department/department-list-page')\n return page\n\n /* 공용과 내 것을 한 목록에 — 아래 둘을 대신한다. */\n case 'approval-line-list':\n import('./pages/approval-line/approval-line-list-page')\n return page\n\n /*\n * 아래 두 화면은 아직 살아 있다. operato-dataset · operato-gangsters · operato-twin 의 메뉴가 이 두\n * 주소를 가리킨다. 지금 지우면 그 세 앱의 메뉴가 아무 데도 안 닿는다. operato-plant 만 새 화면으로\n * 돌렸고, 나머지가 옮겨 온 뒤에 지운다.\n */\n case 'common-approval-line-templates-page':\n import('./pages/approval-line/common-approval-line-templates-page')\n return page\n\n case 'my-approval-line-templates-page':\n import('./pages/approval-line/my-approval-line-templates-page')\n return page\n }\n}\n"]}
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../client/route.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,IAAY;IACxC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,eAAe;YAClB,MAAM,CAAC,qCAAqC,CAAC,CAAA;YAC7C,OAAO,IAAI,CAAA;QAEb,KAAK,iBAAiB;YACpB,MAAM,CAAC,yCAAyC,CAAC,CAAA;YACjD,OAAO,IAAI,CAAA;QAEb,wGAAwG;QACxG,KAAK,qBAAqB;YACxB,MAAM,CAAC,mDAAmD,CAAC,CAAA;YAC3D,OAAO,IAAI,CAAA;QAEb,KAAK,uBAAuB;YAC1B,MAAM,CAAC,qDAAqD,CAAC,CAAA;YAC7D,OAAO,IAAI,CAAA;QAEb,kCAAkC;QAClC,KAAK,oBAAoB;YACvB,MAAM,CAAC,+CAA+C,CAAC,CAAA;YACvD,OAAO,IAAI,CAAA;QAEb;;;;WAIG;QACH,KAAK,qCAAqC;YACxC,MAAM,CAAC,2DAA2D,CAAC,CAAA;YACnE,OAAO,IAAI,CAAA;QAEb,KAAK,iCAAiC;YACpC,MAAM,CAAC,uDAAuD,CAAC,CAAA;YAC/D,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["export default function route(page: string) {\n switch (page) {\n case 'employee-list':\n import('./pages/employee/employee-list-page')\n return page\n\n case 'department-list':\n import('./pages/department/department-list-page')\n return page\n\n /* The employee writes their own profile; sales downloads the chosen ones for a quotation (ADR-0169). */\n case 'my-employee-profile':\n import('./pages/employee-profile/my-employee-profile-page')\n return page\n\n case 'employee-profile-list':\n import('./pages/employee-profile/employee-profile-list-page')\n return page\n\n /* 공용과 내 것을 한 목록에 — 아래 둘을 대신한다. */\n case 'approval-line-list':\n import('./pages/approval-line/approval-line-list-page')\n return page\n\n /*\n * 아래 두 화면은 아직 살아 있다. operato-dataset · operato-gangsters · operato-twin 의 메뉴가 이 두\n * 주소를 가리킨다. 지금 지우면 그 세 앱의 메뉴가 아무 데도 안 닿는다. operato-plant 만 새 화면으로\n * 돌렸고, 나머지가 옮겨 온 뒤에 지운다.\n */\n case 'common-approval-line-templates-page':\n import('./pages/approval-line/common-approval-line-templates-page')\n return page\n\n case 'my-approval-line-templates-page':\n import('./pages/approval-line/my-approval-line-templates-page')\n return page\n }\n}\n"]}