@things-factory/organization 10.1.226 → 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.
- package/dist-client/pages/employee-profile/employee-profile-files.d.ts +23 -0
- package/dist-client/pages/employee-profile/employee-profile-files.js +50 -0
- package/dist-client/pages/employee-profile/employee-profile-files.js.map +1 -0
- package/dist-client/pages/employee-profile/employee-profile-form.d.ts +42 -10
- package/dist-client/pages/employee-profile/employee-profile-form.js +68 -22
- package/dist-client/pages/employee-profile/employee-profile-form.js.map +1 -1
- package/dist-client/pages/employee-profile/employee-profile-list-page.d.ts +19 -5
- package/dist-client/pages/employee-profile/employee-profile-list-page.js +88 -46
- package/dist-client/pages/employee-profile/employee-profile-list-page.js.map +1 -1
- package/dist-client/pages/employee-profile/employee-profile-preview.d.ts +28 -0
- package/dist-client/pages/employee-profile/employee-profile-preview.js +179 -0
- package/dist-client/pages/employee-profile/employee-profile-preview.js.map +1 -0
- package/dist-client/pages/employee-profile/my-employee-profile-page.d.ts +20 -2
- package/dist-client/pages/employee-profile/my-employee-profile-page.js +467 -93
- package/dist-client/pages/employee-profile/my-employee-profile-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/ai-entry/employee-profile-entry.d.ts +10 -0
- package/dist-server/ai-entry/employee-profile-entry.js +88 -0
- package/dist-server/ai-entry/employee-profile-entry.js.map +1 -0
- package/dist-server/ai-entry/entry-target-shape.d.ts +11 -1
- package/dist-server/ai-entry/entry-target-shape.js.map +1 -1
- package/dist-server/ai-entry/organization-entry-targets.d.ts +8 -0
- package/dist-server/ai-entry/organization-entry-targets.js +26 -1
- package/dist-server/ai-entry/organization-entry-targets.js.map +1 -1
- package/dist-server/service/employee-profile/employee-profile-document.d.ts +122 -0
- package/dist-server/service/employee-profile/employee-profile-document.js +439 -0
- package/dist-server/service/employee-profile/employee-profile-document.js.map +1 -0
- package/dist-server/service/employee-profile/employee-profile-mutation.d.ts +1 -0
- package/dist-server/service/employee-profile/employee-profile-mutation.js +29 -0
- package/dist-server/service/employee-profile/employee-profile-mutation.js.map +1 -1
- package/dist-server/service/employee-profile/employee-profile-query.d.ts +9 -1
- package/dist-server/service/employee-profile/employee-profile-query.js +83 -4
- package/dist-server/service/employee-profile/employee-profile-query.js.map +1 -1
- package/dist-server/service/employee-profile/employee-profile-rules.d.ts +6 -0
- package/dist-server/service/employee-profile/employee-profile-rules.js +24 -0
- package/dist-server/service/employee-profile/employee-profile-rules.js.map +1 -1
- package/dist-server/service/employee-profile/employee-profile-type.d.ts +11 -0
- package/dist-server/service/employee-profile/employee-profile-type.js +30 -1
- package/dist-server/service/employee-profile/employee-profile-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/tests/employee-profile-db.test.ts +65 -4
- package/tests/employee-profile-document.test.ts +219 -0
- package/tests/employee-profile-entry.test.ts +72 -0
- package/tests/employee-profile-rules.test.ts +33 -6
- package/translations/en.json +43 -3
- package/translations/es.json +43 -3
- package/translations/ja.json +43 -3
- package/translations/ko.json +43 -3
- package/translations/ms.json +43 -3
- package/translations/zh.json +43 -3
- package/dist-client/pages/employee-profile/employee-profile-workbook.d.ts +0 -33
- package/dist-client/pages/employee-profile/employee-profile-workbook.js +0 -87
- package/dist-client/pages/employee-profile/employee-profile-workbook.js.map +0 -1
- package/tests/employee-profile-workbook.test.ts +0 -89
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ProfileFormat = 'XLSX' | 'PDF' | 'HTML';
|
|
2
|
+
export type ProfileFile = {
|
|
3
|
+
fileName: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
/** Whose profiles: the chosen employees, or the caller's own. */
|
|
8
|
+
export type ProfileSubject = {
|
|
9
|
+
employeeIds: string[];
|
|
10
|
+
} | {
|
|
11
|
+
own: true;
|
|
12
|
+
};
|
|
13
|
+
export declare function profileFile(subject: ProfileSubject, format: ProfileFormat, language?: string): Promise<ProfileFile>;
|
|
14
|
+
/** Hand a file the server made to the browser to save. */
|
|
15
|
+
export declare function saveProfileFile({ fileName, mimeType, content }: ProfileFile): void;
|
|
16
|
+
/** The HTML page of a preview, as text. */
|
|
17
|
+
export declare function textOf({ content }: ProfileFile): string;
|
|
18
|
+
export type Language = {
|
|
19
|
+
code: string;
|
|
20
|
+
display: string;
|
|
21
|
+
};
|
|
22
|
+
/** The reader's language among those the application runs - by its language part when the region differs. */
|
|
23
|
+
export declare function readerLanguageOf(languages: Language[], reader?: string): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import gql from 'graphql-tag';
|
|
2
|
+
import { client } from '@operato/graphql';
|
|
3
|
+
import { i18next } from '@operato/i18n';
|
|
4
|
+
const CHOSEN = gql `
|
|
5
|
+
query ($employeeIds: [String!]!, $format: EmployeeProfileFormat!, $language: String) {
|
|
6
|
+
employeeProfileDocument(employeeIds: $employeeIds, format: $format, language: $language) {
|
|
7
|
+
fileName
|
|
8
|
+
mimeType
|
|
9
|
+
content
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
const OWN = gql `
|
|
14
|
+
query ($format: EmployeeProfileFormat!, $language: String) {
|
|
15
|
+
myEmployeeProfileDocument(format: $format, language: $language) {
|
|
16
|
+
fileName
|
|
17
|
+
mimeType
|
|
18
|
+
content
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
export async function profileFile(subject, format, language) {
|
|
23
|
+
const own = 'own' in subject;
|
|
24
|
+
const response = await client.query({
|
|
25
|
+
query: own ? OWN : CHOSEN,
|
|
26
|
+
variables: { ...(own ? {} : { employeeIds: subject.employeeIds }), format, language: language || null },
|
|
27
|
+
fetchPolicy: 'no-cache'
|
|
28
|
+
});
|
|
29
|
+
const file = own ? response.data?.myEmployeeProfileDocument : response.data?.employeeProfileDocument;
|
|
30
|
+
if (!file)
|
|
31
|
+
throw response.errors?.[0] ?? new Error('no document');
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
const bytesOf = (content) => Uint8Array.from(atob(content), char => char.charCodeAt(0));
|
|
35
|
+
/** Hand a file the server made to the browser to save. */
|
|
36
|
+
export function saveProfileFile({ fileName, mimeType, content }) {
|
|
37
|
+
const url = URL.createObjectURL(new Blob([bytesOf(content)], { type: mimeType }));
|
|
38
|
+
const link = Object.assign(document.createElement('a'), { href: url, download: fileName });
|
|
39
|
+
link.click();
|
|
40
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
41
|
+
}
|
|
42
|
+
/** The HTML page of a preview, as text. */
|
|
43
|
+
export function textOf({ content }) {
|
|
44
|
+
return new TextDecoder().decode(bytesOf(content));
|
|
45
|
+
}
|
|
46
|
+
/** The reader's language among those the application runs - by its language part when the region differs. */
|
|
47
|
+
export function readerLanguageOf(languages, reader = i18next.language ?? '') {
|
|
48
|
+
return languages.find(one => one.code === reader)?.code ?? languages.find(one => one.code.split('-')[0] === reader.split('-')[0])?.code ?? languages[0]?.code ?? '';
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=employee-profile-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"employee-profile-files.js","sourceRoot":"","sources":["../../../client/pages/employee-profile/employee-profile-files.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\n\n/*\n * The profile documents the screens ask the server for (ADR-0169 decisions 6 · 9): the chosen people's - read under\n * `employee-profile` - or one's own, as Excel, PDF or the HTML page the preview shows. One way for the list, the\n * preview and one's own page.\n */\n\nexport type ProfileFormat = 'XLSX' | 'PDF' | 'HTML'\n\nexport type ProfileFile = { fileName: string; mimeType: string; content: string }\n\n/** Whose profiles: the chosen employees, or the caller's own. */\nexport type ProfileSubject = { employeeIds: string[] } | { own: true }\n\nconst CHOSEN = gql`\n query ($employeeIds: [String!]!, $format: EmployeeProfileFormat!, $language: String) {\n employeeProfileDocument(employeeIds: $employeeIds, format: $format, language: $language) {\n fileName\n mimeType\n content\n }\n }\n`\n\nconst OWN = gql`\n query ($format: EmployeeProfileFormat!, $language: String) {\n myEmployeeProfileDocument(format: $format, language: $language) {\n fileName\n mimeType\n content\n }\n }\n`\n\nexport async function profileFile(subject: ProfileSubject, format: ProfileFormat, language?: string): Promise<ProfileFile> {\n const own = 'own' in subject\n const response = await client.query({\n query: own ? OWN : CHOSEN,\n variables: { ...(own ? {} : { employeeIds: subject.employeeIds }), format, language: language || null },\n fetchPolicy: 'no-cache'\n })\n const file = own ? response.data?.myEmployeeProfileDocument : response.data?.employeeProfileDocument\n if (!file) throw response.errors?.[0] ?? new Error('no document')\n return file\n}\n\nconst bytesOf = (content: string) => Uint8Array.from(atob(content), char => char.charCodeAt(0))\n\n/** Hand a file the server made to the browser to save. */\nexport function saveProfileFile({ fileName, mimeType, content }: ProfileFile) {\n const url = URL.createObjectURL(new Blob([bytesOf(content)], { type: mimeType }))\n const link = Object.assign(document.createElement('a'), { href: url, download: fileName })\n link.click()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n}\n\n/** The HTML page of a preview, as text. */\nexport function textOf({ content }: ProfileFile): string {\n return new TextDecoder().decode(bytesOf(content))\n}\n\nexport type Language = { code: string; display: string }\n\n/** The reader's language among those the application runs - by its language part when the region differs. */\nexport function readerLanguageOf(languages: Language[], reader = i18next.language ?? ''): string {\n return languages.find(one => one.code === reader)?.code ?? languages.find(one => one.code.split('-')[0] === reader.split('-')[0])?.code ?? languages[0]?.code ?? ''\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** A row as the screen edits it - the fields, and `ongoing` for a job or project still going on (never sent). */
|
|
1
2
|
export type ProfileRow = Record<string, string>;
|
|
2
3
|
export type ProfileSectionKey = 'educations' | 'certificates' | 'careers' | 'projects';
|
|
3
4
|
export type ProfileField = {
|
|
@@ -7,25 +8,56 @@ export type ProfileField = {
|
|
|
7
8
|
/** `month` for a period's ends (YYYY-MM), `date` for a day, `textarea` for what was done, text otherwise. */
|
|
8
9
|
input?: 'month' | 'date' | 'textarea';
|
|
9
10
|
};
|
|
10
|
-
export
|
|
11
|
+
export type ProfileSection = {
|
|
11
12
|
key: ProfileSectionKey;
|
|
12
13
|
title: string;
|
|
14
|
+
/** The icon the section is shown with. */
|
|
15
|
+
icon: string;
|
|
16
|
+
/** The fields a row's one-line headline is made of, in order. */
|
|
17
|
+
headline: string[];
|
|
18
|
+
/** Whether a row may still be going on - a job held now, a project under way. */
|
|
19
|
+
ongoing?: boolean;
|
|
13
20
|
fields: ProfileField[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
};
|
|
22
|
+
export declare const PROFILE_SECTIONS: ProfileSection[];
|
|
23
|
+
/** Skills as typed - split at commas and line ends, blanks and repeats dropped, added after those already held. */
|
|
24
|
+
export declare function addSkills(held: string[], text: string): string[];
|
|
17
25
|
export type ProfileForm = {
|
|
18
26
|
summary: string;
|
|
19
|
-
skills: string;
|
|
27
|
+
skills: string[];
|
|
20
28
|
} & Record<ProfileSectionKey, ProfileRow[]>;
|
|
29
|
+
/** An empty row of a section, to add. */
|
|
30
|
+
export declare function blankRow(section: ProfileSection): ProfileRow;
|
|
21
31
|
/** The held profile as the form edits it - a profile not yet written is an empty form. */
|
|
22
32
|
export declare function profileFormOf(profile: any): ProfileForm;
|
|
23
|
-
/** The form as `saveMyEmployeeProfile` takes it - the whole profile. */
|
|
33
|
+
/** The form as `saveMyEmployeeProfile` takes it - the whole profile, the section's fields only; a row going on has no end. */
|
|
24
34
|
export declare function profileInputOf(form: ProfileForm): {
|
|
25
35
|
summary: string;
|
|
26
36
|
skills: string[];
|
|
27
|
-
educations:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
educations: {
|
|
38
|
+
[k: string]: string;
|
|
39
|
+
}[];
|
|
40
|
+
certificates: {
|
|
41
|
+
[k: string]: string;
|
|
42
|
+
}[];
|
|
43
|
+
careers: {
|
|
44
|
+
[k: string]: string;
|
|
45
|
+
}[];
|
|
46
|
+
projects: {
|
|
47
|
+
[k: string]: string;
|
|
48
|
+
}[];
|
|
49
|
+
};
|
|
50
|
+
/** A row in one line: the headline fields that are written, joined. */
|
|
51
|
+
export declare function headlineOf(section: ProfileSection, row: ProfileRow): string;
|
|
52
|
+
/** 「2023.03 ~ 2024.08」, 「2024.01 ~ 현재」 - a row's period as the document prints it. */
|
|
53
|
+
export declare function periodLabelOf(row: ProfileRow, ongoing: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* What a profile a quotation can stand on has: a summary, skills, education and projects. Past jobs and certificates
|
|
56
|
+
* count when written but are not asked of everyone - a first job has none.
|
|
57
|
+
*/
|
|
58
|
+
export declare const PROFILE_ESSENTIALS: readonly ["summary", "skills", "educations", "projects"];
|
|
59
|
+
export type ProfileEssential = (typeof PROFILE_ESSENTIALS)[number];
|
|
60
|
+
export declare function completenessOf(form: ProfileForm): {
|
|
61
|
+
percent: number;
|
|
62
|
+
missing: ProfileEssential[];
|
|
31
63
|
};
|
|
@@ -7,6 +7,8 @@ export const PROFILE_SECTIONS = [
|
|
|
7
7
|
{
|
|
8
8
|
key: 'educations',
|
|
9
9
|
title: label('educations'),
|
|
10
|
+
icon: 'school',
|
|
11
|
+
headline: ['school', 'major', 'degree'],
|
|
10
12
|
fields: [
|
|
11
13
|
{ name: 'school', label: label('school') },
|
|
12
14
|
{ name: 'major', label: label('major') },
|
|
@@ -18,6 +20,8 @@ export const PROFILE_SECTIONS = [
|
|
|
18
20
|
{
|
|
19
21
|
key: 'certificates',
|
|
20
22
|
title: label('certificates'),
|
|
23
|
+
icon: 'workspace_premium',
|
|
24
|
+
headline: ['name', 'issuer'],
|
|
21
25
|
fields: [
|
|
22
26
|
{ name: 'name', label: label('certificate') },
|
|
23
27
|
{ name: 'issuer', label: label('issuer') },
|
|
@@ -27,6 +31,9 @@ export const PROFILE_SECTIONS = [
|
|
|
27
31
|
{
|
|
28
32
|
key: 'careers',
|
|
29
33
|
title: label('careers'),
|
|
34
|
+
icon: 'work_history',
|
|
35
|
+
headline: ['company', 'department', 'position'],
|
|
36
|
+
ongoing: true,
|
|
30
37
|
fields: [
|
|
31
38
|
{ name: 'company', label: label('company') },
|
|
32
39
|
{ name: 'department', label: 'label.department' },
|
|
@@ -39,48 +46,87 @@ export const PROFILE_SECTIONS = [
|
|
|
39
46
|
{
|
|
40
47
|
key: 'projects',
|
|
41
48
|
title: label('projects'),
|
|
49
|
+
icon: 'rocket_launch',
|
|
50
|
+
headline: ['name', 'client', 'role'],
|
|
51
|
+
ongoing: true,
|
|
42
52
|
fields: [
|
|
43
53
|
{ name: 'name', label: label('project') },
|
|
44
54
|
{ name: 'client', label: label('client') },
|
|
45
55
|
{ name: 'role', label: label('role') },
|
|
56
|
+
{ name: 'technologies', label: label('technologies') },
|
|
46
57
|
{ name: 'startedOn', label: label('started-on'), input: 'month' },
|
|
47
58
|
{ name: 'endedOn', label: label('ended-on'), input: 'month' },
|
|
48
|
-
{ name: 'technologies', label: label('technologies') },
|
|
49
59
|
{ name: 'duties', label: label('duties'), input: 'textarea' }
|
|
50
60
|
]
|
|
51
61
|
}
|
|
52
62
|
];
|
|
53
|
-
/**
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
/** Skills as typed - split at commas and line ends, blanks and repeats dropped, added after those already held. */
|
|
64
|
+
export function addSkills(held, text) {
|
|
65
|
+
const next = [...held];
|
|
66
|
+
for (const one of text.split(/[,\n]/).map(word => word.trim())) {
|
|
67
|
+
if (one && !next.some(skill => skill.toLowerCase() === one.toLowerCase()))
|
|
68
|
+
next.push(one);
|
|
69
|
+
}
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
/** An empty row of a section, to add. */
|
|
73
|
+
export function blankRow(section) {
|
|
74
|
+
return Object.fromEntries([...section.fields.map(field => [field.name, '']), ...(section.ongoing ? [['ongoing', '']] : [])]);
|
|
59
75
|
}
|
|
60
76
|
/** The held profile as the form edits it - a profile not yet written is an empty form. */
|
|
61
77
|
export function profileFormOf(profile) {
|
|
62
|
-
const rows = (key) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
const rows = (section) => (profile?.[section.key] ?? []).map(row => ({
|
|
79
|
+
...Object.fromEntries(section.fields.map(field => [field.name, row?.[field.name] ?? ''])),
|
|
80
|
+
/* Begun and not ended: still going on. */
|
|
81
|
+
...(section.ongoing ? { ongoing: row?.startedOn && !row?.endedOn ? 'true' : '' } : {})
|
|
82
|
+
}));
|
|
83
|
+
const sectionOf = (key) => PROFILE_SECTIONS.find(section => section.key === key);
|
|
66
84
|
return {
|
|
67
85
|
summary: profile?.summary ?? '',
|
|
68
|
-
skills: (profile?.skills ?? [])
|
|
69
|
-
educations: rows('educations'),
|
|
70
|
-
certificates: rows('certificates'),
|
|
71
|
-
careers: rows('careers'),
|
|
72
|
-
projects: rows('projects')
|
|
86
|
+
skills: [...(profile?.skills ?? [])],
|
|
87
|
+
educations: rows(sectionOf('educations')),
|
|
88
|
+
certificates: rows(sectionOf('certificates')),
|
|
89
|
+
careers: rows(sectionOf('careers')),
|
|
90
|
+
projects: rows(sectionOf('projects'))
|
|
73
91
|
};
|
|
74
92
|
}
|
|
75
|
-
/** The form as `saveMyEmployeeProfile` takes it - the whole profile. */
|
|
93
|
+
/** The form as `saveMyEmployeeProfile` takes it - the whole profile, the section's fields only; a row going on has no end. */
|
|
76
94
|
export function profileInputOf(form) {
|
|
95
|
+
const rows = (section) => form[section.key].map(row => {
|
|
96
|
+
const kept = Object.fromEntries(section.fields.map(field => [field.name, row[field.name] ?? '']));
|
|
97
|
+
return section.ongoing && row.ongoing ? { ...kept, endedOn: '' } : kept;
|
|
98
|
+
});
|
|
99
|
+
const sectionOf = (key) => PROFILE_SECTIONS.find(section => section.key === key);
|
|
77
100
|
return {
|
|
78
101
|
summary: form.summary,
|
|
79
|
-
skills:
|
|
80
|
-
educations:
|
|
81
|
-
certificates:
|
|
82
|
-
careers:
|
|
83
|
-
projects:
|
|
102
|
+
skills: form.skills,
|
|
103
|
+
educations: rows(sectionOf('educations')),
|
|
104
|
+
certificates: rows(sectionOf('certificates')),
|
|
105
|
+
careers: rows(sectionOf('careers')),
|
|
106
|
+
projects: rows(sectionOf('projects'))
|
|
84
107
|
};
|
|
85
108
|
}
|
|
109
|
+
/** A row in one line: the headline fields that are written, joined. */
|
|
110
|
+
export function headlineOf(section, row) {
|
|
111
|
+
return section.headline.map(name => (row[name] ?? '').trim()).filter(Boolean).join(' · ');
|
|
112
|
+
}
|
|
113
|
+
/** 「2023.03 ~ 2024.08」, 「2024.01 ~ 현재」 - a row's period as the document prints it. */
|
|
114
|
+
export function periodLabelOf(row, ongoing) {
|
|
115
|
+
const dot = (day) => (day ?? '').replace(/-/g, '.');
|
|
116
|
+
if (row.acquiredOn !== undefined && row.startedOn === undefined)
|
|
117
|
+
return dot(row.acquiredOn);
|
|
118
|
+
if (!row.startedOn && !row.endedOn && !row.ongoing)
|
|
119
|
+
return '';
|
|
120
|
+
return `${dot(row.startedOn)} ~ ${row.ongoing ? ongoing : dot(row.endedOn)}`.trim();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* What a profile a quotation can stand on has: a summary, skills, education and projects. Past jobs and certificates
|
|
124
|
+
* count when written but are not asked of everyone - a first job has none.
|
|
125
|
+
*/
|
|
126
|
+
export const PROFILE_ESSENTIALS = ['summary', 'skills', 'educations', 'projects'];
|
|
127
|
+
export function completenessOf(form) {
|
|
128
|
+
const has = (key) => (key === 'summary' ? !!form.summary.trim() : form[key].length > 0);
|
|
129
|
+
const missing = PROFILE_ESSENTIALS.filter(key => !has(key));
|
|
130
|
+
return { percent: Math.round(((PROFILE_ESSENTIALS.length - missing.length) / PROFILE_ESSENTIALS.length) * 100), missing };
|
|
131
|
+
}
|
|
86
132
|
//# sourceMappingURL=employee-profile-form.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"employee-profile-form.js","sourceRoot":"","sources":["../../../client/pages/employee-profile/employee-profile-form.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the profile screen edits (ADR-0169 decision 2): the four sections of rows, each with its fields and the input a\n * field takes. Pure, so the shape the screen sends is read by the tests against what the server keeps.\n */\n\nexport type ProfileRow = Record<string, string>\n\nexport type ProfileSectionKey = 'educations' | 'certificates' | 'careers' | 'projects'\n\nexport type ProfileField = {\n name: string\n /** The translation key of its label. */\n label: string\n /** `month` for a period's ends (YYYY-MM), `date` for a day, `textarea` for what was done, text otherwise. */\n input?: 'month' | 'date' | 'textarea'\n}\n\nconst label = (name: string) => `label.employee-profile-${name}`\n\nexport const PROFILE_SECTIONS: { key: ProfileSectionKey; title: string; fields: ProfileField[] }[] = [\n {\n key: 'educations',\n title: label('educations'),\n fields: [\n { name: 'school', label: label('school') },\n { name: 'major', label: label('major') },\n { name: 'degree', label: label('degree') },\n { name: 'startedOn', label: label('started-on'), input: 'month' },\n { name: 'endedOn', label: label('ended-on'), input: 'month' }\n ]\n },\n {\n key: 'certificates',\n title: label('certificates'),\n fields: [\n { name: 'name', label: label('certificate') },\n { name: 'issuer', label: label('issuer') },\n { name: 'acquiredOn', label: label('acquired-on'), input: 'date' }\n ]\n },\n {\n key: 'careers',\n title: label('careers'),\n fields: [\n { name: 'company', label: label('company') },\n { name: 'department', label: 'label.department' },\n { name: 'position', label: label('position') },\n { name: 'startedOn', label: label('started-on'), input: 'month' },\n { name: 'endedOn', label: label('ended-on'), input: 'month' },\n { name: 'duties', label: label('duties'), input: 'textarea' }\n ]\n },\n {\n key: 'projects',\n title: label('projects'),\n fields: [\n { name: 'name', label: label('project') },\n { name: 'client', label: label('client') },\n { name: 'role', label: label('role') },\n { name: 'startedOn', label: label('started-on'), input: 'month' },\n { name: 'endedOn', label: label('ended-on'), input: 'month' },\n { name: 'technologies', label: label('technologies') },\n { name: 'duties', label: label('duties'), input: 'textarea' }\n ]\n }\n]\n\n/** The skills as written in one line - split at commas and line ends, blanks dropped. */\nexport function skillsOf(text: string): string[] {\n return text\n .split(/[,\\n]/)\n .map(one => one.trim())\n .filter(Boolean)\n}\n\nexport type ProfileForm = { summary: string; skills: string } & Record<ProfileSectionKey, ProfileRow[]>\n\n/** The held profile as the form edits it - a profile not yet written is an empty form. */\nexport function profileFormOf(profile: any): ProfileForm {\n const rows = (key: ProfileSectionKey) => {\n const fields = PROFILE_SECTIONS.find(section => section.key === key)!.fields\n return ((profile?.[key] ?? []) as any[]).map(row => Object.fromEntries(fields.map(field => [field.name, row?.[field.name] ?? ''])))\n }\n return {\n summary: profile?.summary ?? '',\n skills: (profile?.skills ?? []).join(', '),\n educations: rows('educations'),\n certificates: rows('certificates'),\n careers: rows('careers'),\n projects: rows('projects')\n }\n}\n\n/** The form as `saveMyEmployeeProfile` takes it - the whole profile. */\nexport function profileInputOf(form: ProfileForm) {\n return {\n summary: form.summary,\n skills: skillsOf(form.skills),\n educations: form.educations,\n certificates: form.certificates,\n careers: form.careers,\n projects: form.projects\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"employee-profile-form.js","sourceRoot":"","sources":["../../../client/pages/employee-profile/employee-profile-form.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the profile screen edits (ADR-0169 decision 2): the four sections of rows, each with its fields and the input a\n * field takes. Pure, so the shape the screen sends is read by the tests against what the server keeps.\n */\n\n/** A row as the screen edits it - the fields, and `ongoing` for a job or project still going on (never sent). */\nexport type ProfileRow = Record<string, string>\n\nexport type ProfileSectionKey = 'educations' | 'certificates' | 'careers' | 'projects'\n\nexport type ProfileField = {\n name: string\n /** The translation key of its label. */\n label: string\n /** `month` for a period's ends (YYYY-MM), `date` for a day, `textarea` for what was done, text otherwise. */\n input?: 'month' | 'date' | 'textarea'\n}\n\nconst label = (name: string) => `label.employee-profile-${name}`\n\nexport type ProfileSection = {\n key: ProfileSectionKey\n title: string\n /** The icon the section is shown with. */\n icon: string\n /** The fields a row's one-line headline is made of, in order. */\n headline: string[]\n /** Whether a row may still be going on - a job held now, a project under way. */\n ongoing?: boolean\n fields: ProfileField[]\n}\n\nexport const PROFILE_SECTIONS: ProfileSection[] = [\n {\n key: 'educations',\n title: label('educations'),\n icon: 'school',\n headline: ['school', 'major', 'degree'],\n fields: [\n { name: 'school', label: label('school') },\n { name: 'major', label: label('major') },\n { name: 'degree', label: label('degree') },\n { name: 'startedOn', label: label('started-on'), input: 'month' },\n { name: 'endedOn', label: label('ended-on'), input: 'month' }\n ]\n },\n {\n key: 'certificates',\n title: label('certificates'),\n icon: 'workspace_premium',\n headline: ['name', 'issuer'],\n fields: [\n { name: 'name', label: label('certificate') },\n { name: 'issuer', label: label('issuer') },\n { name: 'acquiredOn', label: label('acquired-on'), input: 'date' }\n ]\n },\n {\n key: 'careers',\n title: label('careers'),\n icon: 'work_history',\n headline: ['company', 'department', 'position'],\n ongoing: true,\n fields: [\n { name: 'company', label: label('company') },\n { name: 'department', label: 'label.department' },\n { name: 'position', label: label('position') },\n { name: 'startedOn', label: label('started-on'), input: 'month' },\n { name: 'endedOn', label: label('ended-on'), input: 'month' },\n { name: 'duties', label: label('duties'), input: 'textarea' }\n ]\n },\n {\n key: 'projects',\n title: label('projects'),\n icon: 'rocket_launch',\n headline: ['name', 'client', 'role'],\n ongoing: true,\n fields: [\n { name: 'name', label: label('project') },\n { name: 'client', label: label('client') },\n { name: 'role', label: label('role') },\n { name: 'technologies', label: label('technologies') },\n { name: 'startedOn', label: label('started-on'), input: 'month' },\n { name: 'endedOn', label: label('ended-on'), input: 'month' },\n { name: 'duties', label: label('duties'), input: 'textarea' }\n ]\n }\n]\n\n/** Skills as typed - split at commas and line ends, blanks and repeats dropped, added after those already held. */\nexport function addSkills(held: string[], text: string): string[] {\n const next = [...held]\n for (const one of text.split(/[,\\n]/).map(word => word.trim())) {\n if (one && !next.some(skill => skill.toLowerCase() === one.toLowerCase())) next.push(one)\n }\n return next\n}\n\nexport type ProfileForm = { summary: string; skills: string[] } & Record<ProfileSectionKey, ProfileRow[]>\n\n/** An empty row of a section, to add. */\nexport function blankRow(section: ProfileSection): ProfileRow {\n return Object.fromEntries([...section.fields.map(field => [field.name, '']), ...(section.ongoing ? [['ongoing', '']] : [])])\n}\n\n/** The held profile as the form edits it - a profile not yet written is an empty form. */\nexport function profileFormOf(profile: any): ProfileForm {\n const rows = (section: ProfileSection) =>\n ((profile?.[section.key] ?? []) as any[]).map(row => ({\n ...Object.fromEntries(section.fields.map(field => [field.name, row?.[field.name] ?? ''])),\n /* Begun and not ended: still going on. */\n ...(section.ongoing ? { ongoing: row?.startedOn && !row?.endedOn ? 'true' : '' } : {})\n }))\n const sectionOf = (key: ProfileSectionKey) => PROFILE_SECTIONS.find(section => section.key === key)!\n return {\n summary: profile?.summary ?? '',\n skills: [...(profile?.skills ?? [])],\n educations: rows(sectionOf('educations')),\n certificates: rows(sectionOf('certificates')),\n careers: rows(sectionOf('careers')),\n projects: rows(sectionOf('projects'))\n }\n}\n\n/** The form as `saveMyEmployeeProfile` takes it - the whole profile, the section's fields only; a row going on has no end. */\nexport function profileInputOf(form: ProfileForm) {\n const rows = (section: ProfileSection) =>\n form[section.key].map(row => {\n const kept = Object.fromEntries(section.fields.map(field => [field.name, row[field.name] ?? '']))\n return section.ongoing && row.ongoing ? { ...kept, endedOn: '' } : kept\n })\n const sectionOf = (key: ProfileSectionKey) => PROFILE_SECTIONS.find(section => section.key === key)!\n return {\n summary: form.summary,\n skills: form.skills,\n educations: rows(sectionOf('educations')),\n certificates: rows(sectionOf('certificates')),\n careers: rows(sectionOf('careers')),\n projects: rows(sectionOf('projects'))\n }\n}\n\n/** A row in one line: the headline fields that are written, joined. */\nexport function headlineOf(section: ProfileSection, row: ProfileRow): string {\n return section.headline.map(name => (row[name] ?? '').trim()).filter(Boolean).join(' · ')\n}\n\n/** 「2023.03 ~ 2024.08」, 「2024.01 ~ 현재」 - a row's period as the document prints it. */\nexport function periodLabelOf(row: ProfileRow, ongoing: string): string {\n const dot = (day?: string) => (day ?? '').replace(/-/g, '.')\n if (row.acquiredOn !== undefined && row.startedOn === undefined) return dot(row.acquiredOn)\n if (!row.startedOn && !row.endedOn && !row.ongoing) return ''\n return `${dot(row.startedOn)} ~ ${row.ongoing ? ongoing : dot(row.endedOn)}`.trim()\n}\n\n/**\n * What a profile a quotation can stand on has: a summary, skills, education and projects. Past jobs and certificates\n * count when written but are not asked of everyone - a first job has none.\n */\nexport const PROFILE_ESSENTIALS = ['summary', 'skills', 'educations', 'projects'] as const\n\nexport type ProfileEssential = (typeof PROFILE_ESSENTIALS)[number]\n\nexport function completenessOf(form: ProfileForm): { percent: number; missing: ProfileEssential[] } {\n const has = (key: ProfileEssential) => (key === 'summary' ? !!form.summary.trim() : form[key].length > 0)\n const missing = PROFILE_ESSENTIALS.filter(key => !has(key))\n return { percent: Math.round(((PROFILE_ESSENTIALS.length - missing.length) / PROFILE_ESSENTIALS.length) * 100), missing }\n}\n"]}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import '@operato/data-grist/ox-grist.js';
|
|
2
|
+
import '@operato/input/ox-select.js';
|
|
3
|
+
import '@operato/popup/ox-popup-list.js';
|
|
2
4
|
import { FetchOption } from '@operato/data-grist';
|
|
3
5
|
import { PageView } from '@operato/shell';
|
|
6
|
+
import { type Language } from './employee-profile-files';
|
|
4
7
|
declare const EmployeeProfileListPage_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
|
|
5
8
|
/**
|
|
6
9
|
* 인력 프로필 — every active employee and when their profile was last written; the rows picked are downloaded as one
|
|
7
|
-
*
|
|
8
|
-
* basics and the profile, nothing else of the employee record.
|
|
10
|
+
* document to attach to a quotation - Excel or PDF, laid out once on the server (ADR-0169 decision 5). Read under
|
|
11
|
+
* `employee-profile`: the basics and the profile, nothing else of the employee record.
|
|
9
12
|
*/
|
|
10
13
|
export declare class EmployeeProfileListPage extends EmployeeProfileListPage_base {
|
|
11
14
|
static styles: import("lit").CSSResult[];
|
|
12
15
|
gristConfig: any;
|
|
16
|
+
/** The languages the application runs, and the one the document's own words are printed in - the reader's at first. */
|
|
17
|
+
languages: Language[];
|
|
18
|
+
language: string;
|
|
13
19
|
private grist;
|
|
14
20
|
get context(): {
|
|
15
21
|
title: string;
|
|
@@ -17,14 +23,19 @@ export declare class EmployeeProfileListPage extends EmployeeProfileListPage_bas
|
|
|
17
23
|
handler: (search: string) => void;
|
|
18
24
|
value: string;
|
|
19
25
|
};
|
|
20
|
-
actions: {
|
|
26
|
+
actions: ({
|
|
27
|
+
title: string;
|
|
28
|
+
icon: string;
|
|
29
|
+
action: () => void;
|
|
30
|
+
emphasis?: undefined;
|
|
31
|
+
} | {
|
|
21
32
|
title: string;
|
|
22
33
|
icon: string;
|
|
23
34
|
emphasis: {
|
|
24
35
|
raised: boolean;
|
|
25
36
|
};
|
|
26
|
-
action: () => void
|
|
27
|
-
}[];
|
|
37
|
+
action: () => Promise<void>;
|
|
38
|
+
})[];
|
|
28
39
|
};
|
|
29
40
|
render(): import("lit-html").TemplateResult<1>;
|
|
30
41
|
pageInitialized(): Promise<void>;
|
|
@@ -33,6 +44,9 @@ export declare class EmployeeProfileListPage extends EmployeeProfileListPage_bas
|
|
|
33
44
|
total: number;
|
|
34
45
|
records: any[];
|
|
35
46
|
}>;
|
|
47
|
+
/** The ids of the rows picked - or nothing, having said so. */
|
|
48
|
+
private picked;
|
|
49
|
+
private preview;
|
|
36
50
|
private download;
|
|
37
51
|
}
|
|
38
52
|
export {};
|
|
@@ -1,50 +1,20 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import '@operato/data-grist/ox-grist.js';
|
|
3
|
+
import '@operato/input/ox-select.js';
|
|
4
|
+
import '@operato/popup/ox-popup-list.js';
|
|
3
5
|
import gql from 'graphql-tag';
|
|
4
|
-
import { html } from 'lit';
|
|
6
|
+
import { css, html } from 'lit';
|
|
5
7
|
import { customElement, query, state } from 'lit/decorators.js';
|
|
6
|
-
import * as XLSX from 'xlsx';
|
|
7
8
|
import { DataGrist } from '@operato/data-grist';
|
|
8
9
|
import { client } from '@operato/graphql';
|
|
9
10
|
import { i18next, localize } from '@operato/i18n';
|
|
10
11
|
import { notify } from '@operato/layout';
|
|
11
12
|
import { PageView } from '@operato/shell';
|
|
12
|
-
import { CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
|
|
13
|
+
import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
|
|
13
14
|
import { isMobileDevice } from '@operato/utils';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const t = (key) => i18next.t(key);
|
|
18
|
-
const p = (name) => t(`label.employee-profile-${name}`);
|
|
19
|
-
return {
|
|
20
|
-
name: t('field.name'),
|
|
21
|
-
department: t('label.department'),
|
|
22
|
-
jobPosition: t('label.job-position'),
|
|
23
|
-
jobResponsibility: t('label.job-responsibility'),
|
|
24
|
-
hiredOn: t('label.hired-on'),
|
|
25
|
-
summary: p('summary'),
|
|
26
|
-
skills: p('skills'),
|
|
27
|
-
educations: p('educations'),
|
|
28
|
-
certificates: p('certificates'),
|
|
29
|
-
careers: p('careers'),
|
|
30
|
-
projects: p('projects'),
|
|
31
|
-
school: p('school'),
|
|
32
|
-
major: p('major'),
|
|
33
|
-
degree: p('degree'),
|
|
34
|
-
period: p('period'),
|
|
35
|
-
certificate: p('certificate'),
|
|
36
|
-
issuer: p('issuer'),
|
|
37
|
-
acquiredOn: p('acquired-on'),
|
|
38
|
-
company: p('company'),
|
|
39
|
-
position: p('position'),
|
|
40
|
-
duties: p('duties'),
|
|
41
|
-
project: p('project'),
|
|
42
|
-
client: p('client'),
|
|
43
|
-
role: p('role'),
|
|
44
|
-
technologies: p('technologies')
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/** A profile's last change as a day of the reader's calendar. */
|
|
15
|
+
import { getLanguages } from '@things-factory/auth-base/dist-client';
|
|
16
|
+
import { profileFile, readerLanguageOf, saveProfileFile } from './employee-profile-files';
|
|
17
|
+
import { openProfilePreview } from './employee-profile-preview';
|
|
48
18
|
const dayOf = (value) => {
|
|
49
19
|
if (!value)
|
|
50
20
|
return '';
|
|
@@ -54,11 +24,35 @@ const dayOf = (value) => {
|
|
|
54
24
|
};
|
|
55
25
|
/**
|
|
56
26
|
* 인력 프로필 — every active employee and when their profile was last written; the rows picked are downloaded as one
|
|
57
|
-
*
|
|
58
|
-
* basics and the profile, nothing else of the employee record.
|
|
27
|
+
* document to attach to a quotation - Excel or PDF, laid out once on the server (ADR-0169 decision 5). Read under
|
|
28
|
+
* `employee-profile`: the basics and the profile, nothing else of the employee record.
|
|
59
29
|
*/
|
|
60
30
|
let EmployeeProfileListPage = class EmployeeProfileListPage extends localize(i18next)(PageView) {
|
|
61
|
-
|
|
31
|
+
constructor() {
|
|
32
|
+
super(...arguments);
|
|
33
|
+
/** The languages the application runs, and the one the document's own words are printed in - the reader's at first. */
|
|
34
|
+
this.languages = [];
|
|
35
|
+
this.language = '';
|
|
36
|
+
}
|
|
37
|
+
static { this.styles = [
|
|
38
|
+
WorkSurfacePageStyles,
|
|
39
|
+
CommonGristStyles,
|
|
40
|
+
CommonHeaderStyles,
|
|
41
|
+
ScrollbarStyles,
|
|
42
|
+
css `
|
|
43
|
+
.filters {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--spacing-medium, 8px);
|
|
48
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
49
|
+
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
50
|
+
}
|
|
51
|
+
ox-select {
|
|
52
|
+
min-width: 140px;
|
|
53
|
+
}
|
|
54
|
+
`
|
|
55
|
+
]; }
|
|
62
56
|
get context() {
|
|
63
57
|
return {
|
|
64
58
|
title: i18next.t('title.employee-profile-list'),
|
|
@@ -68,18 +62,42 @@ let EmployeeProfileListPage = class EmployeeProfileListPage extends localize(i18
|
|
|
68
62
|
},
|
|
69
63
|
value: this.grist?.searchText || ''
|
|
70
64
|
},
|
|
71
|
-
actions: [
|
|
65
|
+
actions: [
|
|
66
|
+
{ title: i18next.t('button.employee-profile-preview'), icon: 'preview', action: () => this.preview() },
|
|
67
|
+
{ title: i18next.t('button.employee-profile-download-pdf'), icon: 'picture_as_pdf', action: () => this.download('PDF') },
|
|
68
|
+
{ title: i18next.t('button.employee-profile-download'), icon: 'download', emphasis: { raised: true }, action: () => this.download('XLSX') }
|
|
69
|
+
]
|
|
72
70
|
};
|
|
73
71
|
}
|
|
74
72
|
render() {
|
|
73
|
+
const shown = this.languages.find(one => one.code === this.language)?.display ?? this.language;
|
|
75
74
|
return html `<ox-grist
|
|
76
75
|
class="page-fill"
|
|
77
76
|
.mode=${isMobileDevice() ? 'CARD' : 'GRID'}
|
|
78
77
|
.config=${this.gristConfig}
|
|
79
78
|
.fetchHandler=${this.fetchHandler.bind(this)}
|
|
80
|
-
|
|
79
|
+
>
|
|
80
|
+
${this.languages.length > 1
|
|
81
|
+
? html `<div slot="headroom" class="header">
|
|
82
|
+
<div class="filters">
|
|
83
|
+
<span>${i18next.t('label.employee-profile-document-language')}</span>
|
|
84
|
+
<ox-select
|
|
85
|
+
.value=${this.language}
|
|
86
|
+
aria-label=${i18next.t('label.employee-profile-document-language')}
|
|
87
|
+
@change=${(e) => (this.language = String(e.detail || this.language))}
|
|
88
|
+
>
|
|
89
|
+
<span slot="label">${shown}</span>
|
|
90
|
+
<ox-popup-list align-left nowrap with-search>${this.languages.map(one => html `<div option value=${one.code}>${one.display}</div>`)}</ox-popup-list>
|
|
91
|
+
</ox-select>
|
|
92
|
+
</div>
|
|
93
|
+
</div>`
|
|
94
|
+
: ''}
|
|
95
|
+
</ox-grist>`;
|
|
81
96
|
}
|
|
82
97
|
async pageInitialized() {
|
|
98
|
+
/* The reader's language first - matched to a code the application runs, by its language part when the region differs. */
|
|
99
|
+
this.languages = ((await getLanguages()) ?? []);
|
|
100
|
+
this.language = readerLanguageOf(this.languages);
|
|
83
101
|
const text = (name, header, width, extra = {}) => ({
|
|
84
102
|
type: 'string',
|
|
85
103
|
name,
|
|
@@ -140,19 +158,43 @@ let EmployeeProfileListPage = class EmployeeProfileListPage extends localize(i18
|
|
|
140
158
|
.filter(one => !words || [one.name, one.departmentName, one.jobPosition, one.jobResponsibility].some(value => value?.toLowerCase().includes(words)));
|
|
141
159
|
return { total: records.length, records };
|
|
142
160
|
}
|
|
143
|
-
|
|
161
|
+
/** The ids of the rows picked - or nothing, having said so. */
|
|
162
|
+
picked() {
|
|
144
163
|
const picked = this.grist?.selected ?? [];
|
|
145
|
-
if (
|
|
146
|
-
|
|
164
|
+
if (picked.length)
|
|
165
|
+
return picked.map((one) => one.employeeId);
|
|
166
|
+
notify({ level: 'warn', message: i18next.t('text.employee-profile-select-rows-first') });
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
preview() {
|
|
170
|
+
const employeeIds = this.picked();
|
|
171
|
+
if (employeeIds)
|
|
172
|
+
openProfilePreview({ employeeIds }, this.languages, this.language);
|
|
173
|
+
}
|
|
174
|
+
async download(format) {
|
|
175
|
+
const employeeIds = this.picked();
|
|
176
|
+
if (!employeeIds)
|
|
147
177
|
return;
|
|
178
|
+
try {
|
|
179
|
+
saveProfileFile(await profileFile({ employeeIds }, format, this.language));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
notify({ level: 'error', message: error?.message ?? String(error) });
|
|
148
183
|
}
|
|
149
|
-
XLSX.writeFile(profileWorkbook(picked, sheetWords()), profileFileName(i18next.t('text.employee-profile-file')));
|
|
150
184
|
}
|
|
151
185
|
};
|
|
152
186
|
__decorate([
|
|
153
187
|
state(),
|
|
154
188
|
__metadata("design:type", Object)
|
|
155
189
|
], EmployeeProfileListPage.prototype, "gristConfig", void 0);
|
|
190
|
+
__decorate([
|
|
191
|
+
state(),
|
|
192
|
+
__metadata("design:type", Array)
|
|
193
|
+
], EmployeeProfileListPage.prototype, "languages", void 0);
|
|
194
|
+
__decorate([
|
|
195
|
+
state(),
|
|
196
|
+
__metadata("design:type", Object)
|
|
197
|
+
], EmployeeProfileListPage.prototype, "language", void 0);
|
|
156
198
|
__decorate([
|
|
157
199
|
query('ox-grist'),
|
|
158
200
|
__metadata("design:type", DataGrist)
|