@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,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"]}
@@ -0,0 +1,63 @@
1
+ /** A row as the screen edits it - the fields, and `ongoing` for a job or project still going on (never sent). */
2
+ export type ProfileRow = Record<string, string>;
3
+ export type ProfileSectionKey = 'educations' | 'certificates' | 'careers' | 'projects';
4
+ export type ProfileField = {
5
+ name: string;
6
+ /** The translation key of its label. */
7
+ label: string;
8
+ /** `month` for a period's ends (YYYY-MM), `date` for a day, `textarea` for what was done, text otherwise. */
9
+ input?: 'month' | 'date' | 'textarea';
10
+ };
11
+ export type ProfileSection = {
12
+ key: ProfileSectionKey;
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;
20
+ fields: ProfileField[];
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[];
25
+ export type ProfileForm = {
26
+ summary: string;
27
+ skills: string[];
28
+ } & Record<ProfileSectionKey, ProfileRow[]>;
29
+ /** An empty row of a section, to add. */
30
+ export declare function blankRow(section: ProfileSection): ProfileRow;
31
+ /** The held profile as the form edits it - a profile not yet written is an empty form. */
32
+ export declare function profileFormOf(profile: any): ProfileForm;
33
+ /** The form as `saveMyEmployeeProfile` takes it - the whole profile, the section's fields only; a row going on has no end. */
34
+ export declare function profileInputOf(form: ProfileForm): {
35
+ summary: string;
36
+ skills: string[];
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[];
63
+ };
@@ -0,0 +1,132 @@
1
+ /*
2
+ * What the profile screen edits (ADR-0169 decision 2): the four sections of rows, each with its fields and the input a
3
+ * field takes. Pure, so the shape the screen sends is read by the tests against what the server keeps.
4
+ */
5
+ const label = (name) => `label.employee-profile-${name}`;
6
+ export const PROFILE_SECTIONS = [
7
+ {
8
+ key: 'educations',
9
+ title: label('educations'),
10
+ icon: 'school',
11
+ headline: ['school', 'major', 'degree'],
12
+ fields: [
13
+ { name: 'school', label: label('school') },
14
+ { name: 'major', label: label('major') },
15
+ { name: 'degree', label: label('degree') },
16
+ { name: 'startedOn', label: label('started-on'), input: 'month' },
17
+ { name: 'endedOn', label: label('ended-on'), input: 'month' }
18
+ ]
19
+ },
20
+ {
21
+ key: 'certificates',
22
+ title: label('certificates'),
23
+ icon: 'workspace_premium',
24
+ headline: ['name', 'issuer'],
25
+ fields: [
26
+ { name: 'name', label: label('certificate') },
27
+ { name: 'issuer', label: label('issuer') },
28
+ { name: 'acquiredOn', label: label('acquired-on'), input: 'date' }
29
+ ]
30
+ },
31
+ {
32
+ key: 'careers',
33
+ title: label('careers'),
34
+ icon: 'work_history',
35
+ headline: ['company', 'department', 'position'],
36
+ ongoing: true,
37
+ fields: [
38
+ { name: 'company', label: label('company') },
39
+ { name: 'department', label: 'label.department' },
40
+ { name: 'position', label: label('position') },
41
+ { name: 'startedOn', label: label('started-on'), input: 'month' },
42
+ { name: 'endedOn', label: label('ended-on'), input: 'month' },
43
+ { name: 'duties', label: label('duties'), input: 'textarea' }
44
+ ]
45
+ },
46
+ {
47
+ key: 'projects',
48
+ title: label('projects'),
49
+ icon: 'rocket_launch',
50
+ headline: ['name', 'client', 'role'],
51
+ ongoing: true,
52
+ fields: [
53
+ { name: 'name', label: label('project') },
54
+ { name: 'client', label: label('client') },
55
+ { name: 'role', label: label('role') },
56
+ { name: 'technologies', label: label('technologies') },
57
+ { name: 'startedOn', label: label('started-on'), input: 'month' },
58
+ { name: 'endedOn', label: label('ended-on'), input: 'month' },
59
+ { name: 'duties', label: label('duties'), input: 'textarea' }
60
+ ]
61
+ }
62
+ ];
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', '']] : [])]);
75
+ }
76
+ /** The held profile as the form edits it - a profile not yet written is an empty form. */
77
+ export function profileFormOf(profile) {
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);
84
+ return {
85
+ summary: profile?.summary ?? '',
86
+ skills: [...(profile?.skills ?? [])],
87
+ educations: rows(sectionOf('educations')),
88
+ certificates: rows(sectionOf('certificates')),
89
+ careers: rows(sectionOf('careers')),
90
+ projects: rows(sectionOf('projects'))
91
+ };
92
+ }
93
+ /** The form as `saveMyEmployeeProfile` takes it - the whole profile, the section's fields only; a row going on has no end. */
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);
100
+ return {
101
+ summary: form.summary,
102
+ skills: form.skills,
103
+ educations: rows(sectionOf('educations')),
104
+ certificates: rows(sectionOf('certificates')),
105
+ careers: rows(sectionOf('careers')),
106
+ projects: rows(sectionOf('projects'))
107
+ };
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
+ }
132
+ //# sourceMappingURL=employee-profile-form.js.map
@@ -0,0 +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\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"]}
@@ -0,0 +1,52 @@
1
+ import '@operato/data-grist/ox-grist.js';
2
+ import '@operato/input/ox-select.js';
3
+ import '@operato/popup/ox-popup-list.js';
4
+ import { FetchOption } from '@operato/data-grist';
5
+ import { PageView } from '@operato/shell';
6
+ import { type Language } from './employee-profile-files';
7
+ declare const EmployeeProfileListPage_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
8
+ /**
9
+ * 인력 프로필 — every active employee and when their profile was last written; the rows picked are downloaded as one
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.
12
+ */
13
+ export declare class EmployeeProfileListPage extends EmployeeProfileListPage_base {
14
+ static styles: import("lit").CSSResult[];
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;
19
+ private grist;
20
+ get context(): {
21
+ title: string;
22
+ search: {
23
+ handler: (search: string) => void;
24
+ value: string;
25
+ };
26
+ actions: ({
27
+ title: string;
28
+ icon: string;
29
+ action: () => void;
30
+ emphasis?: undefined;
31
+ } | {
32
+ title: string;
33
+ icon: string;
34
+ emphasis: {
35
+ raised: boolean;
36
+ };
37
+ action: () => Promise<void>;
38
+ })[];
39
+ };
40
+ render(): import("lit-html").TemplateResult<1>;
41
+ pageInitialized(): Promise<void>;
42
+ /** Every active employee at once - a company's people, not a ledger - narrowed here by the search words. */
43
+ fetchHandler({ search }: FetchOption): Promise<{
44
+ total: number;
45
+ records: any[];
46
+ }>;
47
+ /** The ids of the rows picked - or nothing, having said so. */
48
+ private picked;
49
+ private preview;
50
+ private download;
51
+ }
52
+ export {};
@@ -0,0 +1,206 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@operato/data-grist/ox-grist.js';
3
+ import '@operato/input/ox-select.js';
4
+ import '@operato/popup/ox-popup-list.js';
5
+ import gql from 'graphql-tag';
6
+ import { css, html } from 'lit';
7
+ import { customElement, query, state } from 'lit/decorators.js';
8
+ import { DataGrist } from '@operato/data-grist';
9
+ import { client } from '@operato/graphql';
10
+ import { i18next, localize } from '@operato/i18n';
11
+ import { notify } from '@operato/layout';
12
+ import { PageView } from '@operato/shell';
13
+ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
14
+ import { isMobileDevice } from '@operato/utils';
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';
18
+ const dayOf = (value) => {
19
+ if (!value)
20
+ return '';
21
+ const day = new Date(value);
22
+ const pad = (n) => String(n).padStart(2, '0');
23
+ return `${day.getFullYear()}-${pad(day.getMonth() + 1)}-${pad(day.getDate())}`;
24
+ };
25
+ /**
26
+ * 인력 프로필 — every active employee and when their profile was last written; the rows picked are downloaded as one
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.
29
+ */
30
+ let EmployeeProfileListPage = class EmployeeProfileListPage extends localize(i18next)(PageView) {
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
+ ]; }
56
+ get context() {
57
+ return {
58
+ title: i18next.t('title.employee-profile-list'),
59
+ search: {
60
+ handler: (search) => {
61
+ this.grist.searchText = search;
62
+ },
63
+ value: this.grist?.searchText || ''
64
+ },
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
+ ]
70
+ };
71
+ }
72
+ render() {
73
+ const shown = this.languages.find(one => one.code === this.language)?.display ?? this.language;
74
+ return html `<ox-grist
75
+ class="page-fill"
76
+ .mode=${isMobileDevice() ? 'CARD' : 'GRID'}
77
+ .config=${this.gristConfig}
78
+ .fetchHandler=${this.fetchHandler.bind(this)}
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>`;
96
+ }
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);
101
+ const text = (name, header, width, extra = {}) => ({
102
+ type: 'string',
103
+ name,
104
+ header,
105
+ width,
106
+ sortable: true,
107
+ ...extra,
108
+ record: { editable: false, ...extra.record }
109
+ });
110
+ this.gristConfig = {
111
+ rows: { appendable: false, selectable: { multiple: true } },
112
+ pagination: { infinite: true },
113
+ emptyNote: { title: i18next.t('text.employee-profile-empty') },
114
+ list: { fields: ['name', 'departmentName'], details: ['jobPosition', 'profileUpdatedAt'] },
115
+ columns: [
116
+ { type: 'gutter', gutterName: 'sequence' },
117
+ { type: 'gutter', gutterName: 'row-selector', multiple: true },
118
+ text('name', i18next.t('field.name'), 120),
119
+ text('departmentName', i18next.t('label.department'), 160),
120
+ text('jobPosition', i18next.t('label.job-position'), 100),
121
+ text('jobResponsibility', i18next.t('label.job-responsibility'), 100),
122
+ text('profileUpdatedAt', i18next.t('label.employee-profile-updated-at'), 120)
123
+ ]
124
+ };
125
+ }
126
+ /** Every active employee at once - a company's people, not a ledger - narrowed here by the search words. */
127
+ async fetchHandler({ search }) {
128
+ const response = await client.query({
129
+ query: gql `
130
+ query {
131
+ employeeProfiles {
132
+ employeeId
133
+ controlNo
134
+ name
135
+ departmentName
136
+ jobPosition
137
+ jobResponsibility
138
+ hiredOn
139
+ profile {
140
+ summary
141
+ skills
142
+ educations { school major degree startedOn endedOn }
143
+ certificates { name issuer acquiredOn }
144
+ careers { company department position startedOn endedOn duties }
145
+ projects { name client startedOn endedOn role duties technologies }
146
+ updatedAt
147
+ }
148
+ }
149
+ }
150
+ `,
151
+ fetchPolicy: 'network-only'
152
+ });
153
+ if (response.errors?.length)
154
+ throw response;
155
+ const words = (search ?? '').trim().toLowerCase();
156
+ const records = response.data.employeeProfiles
157
+ .map(one => ({ ...one, id: one.employeeId, profileUpdatedAt: dayOf(one.profile?.updatedAt) }))
158
+ .filter(one => !words || [one.name, one.departmentName, one.jobPosition, one.jobResponsibility].some(value => value?.toLowerCase().includes(words)));
159
+ return { total: records.length, records };
160
+ }
161
+ /** The ids of the rows picked - or nothing, having said so. */
162
+ picked() {
163
+ const picked = this.grist?.selected ?? [];
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)
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) });
183
+ }
184
+ }
185
+ };
186
+ __decorate([
187
+ state(),
188
+ __metadata("design:type", Object)
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);
198
+ __decorate([
199
+ query('ox-grist'),
200
+ __metadata("design:type", DataGrist)
201
+ ], EmployeeProfileListPage.prototype, "grist", void 0);
202
+ EmployeeProfileListPage = __decorate([
203
+ customElement('employee-profile-list-page')
204
+ ], EmployeeProfileListPage);
205
+ export { EmployeeProfileListPage };
206
+ //# sourceMappingURL=employee-profile-list-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"employee-profile-list-page.js","sourceRoot":"","sources":["../../../client/pages/employee-profile/employee-profile-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist/ox-grist.js'\nimport '@operato/input/ox-select.js'\nimport '@operato/popup/ox-popup-list.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, query, state } from 'lit/decorators.js'\n\nimport { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { PageView } from '@operato/shell'\nimport { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { getLanguages } from '@things-factory/auth-base/dist-client'\n\nimport { profileFile, readerLanguageOf, saveProfileFile, type Language, type ProfileFormat } from './employee-profile-files'\nimport { openProfilePreview } from './employee-profile-preview'\n\nconst dayOf = (value?: string | null) => {\n if (!value) return ''\n const day = new Date(value)\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${day.getFullYear()}-${pad(day.getMonth() + 1)}-${pad(day.getDate())}`\n}\n\n/**\n * 인력 프로필 — every active employee and when their profile was last written; the rows picked are downloaded as one\n * document to attach to a quotation - Excel or PDF, laid out once on the server (ADR-0169 decision 5). Read under\n * `employee-profile`: the basics and the profile, nothing else of the employee record.\n */\n@customElement('employee-profile-list-page')\nexport class EmployeeProfileListPage extends localize(i18next)(PageView) {\n static styles = [\n WorkSurfacePageStyles,\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n .filters {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n font-size: var(--ui-t-small, 11.5px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n ox-select {\n min-width: 140px;\n }\n `\n ]\n\n @state() gristConfig: any\n /** The languages the application runs, and the one the document's own words are printed in - the reader's at first. */\n @state() languages: Language[] = []\n @state() language = ''\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('title.employee-profile-list'),\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [\n { title: i18next.t('button.employee-profile-preview'), icon: 'preview', action: () => this.preview() },\n { title: i18next.t('button.employee-profile-download-pdf'), icon: 'picture_as_pdf', action: () => this.download('PDF') },\n { title: i18next.t('button.employee-profile-download'), icon: 'download', emphasis: { raised: true }, action: () => this.download('XLSX') }\n ]\n }\n }\n\n render() {\n const shown = this.languages.find(one => one.code === this.language)?.display ?? this.language\n return html`<ox-grist\n class=\"page-fill\"\n .mode=${isMobileDevice() ? 'CARD' : 'GRID'}\n .config=${this.gristConfig}\n .fetchHandler=${this.fetchHandler.bind(this)}\n >\n ${this.languages.length > 1\n ? html`<div slot=\"headroom\" class=\"header\">\n <div class=\"filters\">\n <span>${i18next.t('label.employee-profile-document-language')}</span>\n <ox-select\n .value=${this.language}\n aria-label=${i18next.t('label.employee-profile-document-language')}\n @change=${(e: CustomEvent) => (this.language = String(e.detail || this.language))}\n >\n <span slot=\"label\">${shown}</span>\n <ox-popup-list align-left nowrap with-search>${this.languages.map(one => html`<div option value=${one.code}>${one.display}</div>`)}</ox-popup-list>\n </ox-select>\n </div>\n </div>`\n : ''}\n </ox-grist>`\n }\n\n async pageInitialized() {\n /* The reader's language first - matched to a code the application runs, by its language part when the region differs. */\n this.languages = ((await getLanguages()) ?? []) as Language[]\n this.language = readerLanguageOf(this.languages)\n\n const text = (name: string, header: string, width: number, extra: any = {}) => ({\n type: 'string',\n name,\n header,\n width,\n sortable: true,\n ...extra,\n record: { editable: false, ...extra.record }\n })\n this.gristConfig = {\n rows: { appendable: false, selectable: { multiple: true } },\n pagination: { infinite: true },\n emptyNote: { title: i18next.t('text.employee-profile-empty') },\n list: { fields: ['name', 'departmentName'], details: ['jobPosition', 'profileUpdatedAt'] },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n { type: 'gutter', gutterName: 'row-selector', multiple: true },\n text('name', i18next.t('field.name'), 120),\n text('departmentName', i18next.t('label.department'), 160),\n text('jobPosition', i18next.t('label.job-position'), 100),\n text('jobResponsibility', i18next.t('label.job-responsibility'), 100),\n text('profileUpdatedAt', i18next.t('label.employee-profile-updated-at'), 120)\n ]\n }\n }\n\n /** Every active employee at once - a company's people, not a ledger - narrowed here by the search words. */\n async fetchHandler({ search }: FetchOption) {\n const response = await client.query({\n query: gql`\n query {\n employeeProfiles {\n employeeId\n controlNo\n name\n departmentName\n jobPosition\n jobResponsibility\n hiredOn\n profile {\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 `,\n fetchPolicy: 'network-only'\n })\n if (response.errors?.length) throw response\n\n const words = (search ?? '').trim().toLowerCase()\n const records = (response.data.employeeProfiles as any[])\n .map(one => ({ ...one, id: one.employeeId, profileUpdatedAt: dayOf(one.profile?.updatedAt) }))\n .filter(one => !words || [one.name, one.departmentName, one.jobPosition, one.jobResponsibility].some(value => value?.toLowerCase().includes(words)))\n return { total: records.length, records }\n }\n\n /** The ids of the rows picked - or nothing, having said so. */\n private picked(): string[] | null {\n const picked = this.grist?.selected ?? []\n if (picked.length) return picked.map((one: any) => one.employeeId)\n notify({ level: 'warn', message: i18next.t('text.employee-profile-select-rows-first') })\n return null\n }\n\n private preview() {\n const employeeIds = this.picked()\n if (employeeIds) openProfilePreview({ employeeIds }, this.languages, this.language)\n }\n\n private async download(format: ProfileFormat) {\n const employeeIds = this.picked()\n if (!employeeIds) return\n try {\n saveProfileFile(await profileFile({ employeeIds }, format, this.language))\n } catch (error: any) {\n notify({ level: 'error', message: error?.message ?? String(error) })\n }\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '@operato/input/ox-select.js';
3
+ import '@operato/popup/ox-popup-list.js';
4
+ import { LitElement } from 'lit';
5
+ import { type Language, type ProfileSubject } from './employee-profile-files';
6
+ declare const EmployeeProfilePreview_base: (new (...args: any[]) => LitElement) & typeof LitElement;
7
+ /**
8
+ * The profiles as they will be handed in, before they are (ADR-0169 decision 9): the server's pages of A4 in a frame,
9
+ * the language of the document's own words to change, and the two downloads in the same language. Opened from the list
10
+ * for the people chosen and from one's own page for oneself.
11
+ */
12
+ export declare class EmployeeProfilePreview extends EmployeeProfilePreview_base {
13
+ static styles: import("lit").CSSResult[];
14
+ subject: ProfileSubject;
15
+ languages: Language[];
16
+ language: string;
17
+ private page;
18
+ private busy;
19
+ firstUpdated(): void;
20
+ private show;
21
+ private download;
22
+ /** As tall as its pages - read from its document, which is ours (same origin, no script). */
23
+ private fit;
24
+ render(): import("lit-html").TemplateResult<1>;
25
+ }
26
+ /** Open the preview of `subject`'s profiles in `language`. */
27
+ export declare function openProfilePreview(subject: ProfileSubject, languages: Language[], language: string): import("@operato/layout").PopupHandle<unknown>;
28
+ export {};