@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.
Files changed (55) 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 +42 -10
  5. package/dist-client/pages/employee-profile/employee-profile-form.js +68 -22
  6. package/dist-client/pages/employee-profile/employee-profile-form.js.map +1 -1
  7. package/dist-client/pages/employee-profile/employee-profile-list-page.d.ts +19 -5
  8. package/dist-client/pages/employee-profile/employee-profile-list-page.js +88 -46
  9. package/dist-client/pages/employee-profile/employee-profile-list-page.js.map +1 -1
  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 +20 -2
  14. package/dist-client/pages/employee-profile/my-employee-profile-page.js +467 -93
  15. package/dist-client/pages/employee-profile/my-employee-profile-page.js.map +1 -1
  16. package/dist-client/tsconfig.tsbuildinfo +1 -1
  17. package/dist-server/ai-entry/employee-profile-entry.d.ts +10 -0
  18. package/dist-server/ai-entry/employee-profile-entry.js +88 -0
  19. package/dist-server/ai-entry/employee-profile-entry.js.map +1 -0
  20. package/dist-server/ai-entry/entry-target-shape.d.ts +11 -1
  21. package/dist-server/ai-entry/entry-target-shape.js.map +1 -1
  22. package/dist-server/ai-entry/organization-entry-targets.d.ts +8 -0
  23. package/dist-server/ai-entry/organization-entry-targets.js +26 -1
  24. package/dist-server/ai-entry/organization-entry-targets.js.map +1 -1
  25. package/dist-server/service/employee-profile/employee-profile-document.d.ts +122 -0
  26. package/dist-server/service/employee-profile/employee-profile-document.js +439 -0
  27. package/dist-server/service/employee-profile/employee-profile-document.js.map +1 -0
  28. package/dist-server/service/employee-profile/employee-profile-mutation.d.ts +1 -0
  29. package/dist-server/service/employee-profile/employee-profile-mutation.js +29 -0
  30. package/dist-server/service/employee-profile/employee-profile-mutation.js.map +1 -1
  31. package/dist-server/service/employee-profile/employee-profile-query.d.ts +9 -1
  32. package/dist-server/service/employee-profile/employee-profile-query.js +83 -4
  33. package/dist-server/service/employee-profile/employee-profile-query.js.map +1 -1
  34. package/dist-server/service/employee-profile/employee-profile-rules.d.ts +6 -0
  35. package/dist-server/service/employee-profile/employee-profile-rules.js +24 -0
  36. package/dist-server/service/employee-profile/employee-profile-rules.js.map +1 -1
  37. package/dist-server/service/employee-profile/employee-profile-type.d.ts +11 -0
  38. package/dist-server/service/employee-profile/employee-profile-type.js +30 -1
  39. package/dist-server/service/employee-profile/employee-profile-type.js.map +1 -1
  40. package/dist-server/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +8 -7
  42. package/tests/employee-profile-db.test.ts +65 -4
  43. package/tests/employee-profile-document.test.ts +219 -0
  44. package/tests/employee-profile-entry.test.ts +72 -0
  45. package/tests/employee-profile-rules.test.ts +33 -6
  46. package/translations/en.json +43 -3
  47. package/translations/es.json +43 -3
  48. package/translations/ja.json +43 -3
  49. package/translations/ko.json +43 -3
  50. package/translations/ms.json +43 -3
  51. package/translations/zh.json +43 -3
  52. package/dist-client/pages/employee-profile/employee-profile-workbook.d.ts +0 -33
  53. package/dist-client/pages/employee-profile/employee-profile-workbook.js +0 -87
  54. package/dist-client/pages/employee-profile/employee-profile-workbook.js.map +0 -1
  55. package/tests/employee-profile-workbook.test.ts +0 -89
@@ -132,15 +132,55 @@
132
132
  "label.employee-profile-role": "役割",
133
133
  "label.employee-profile-technologies": "使用技術",
134
134
  "label.employee-profile-updated-at": "プロフィール更新日",
135
- "text.employee-profile-skills-hint": "カンマで区切って入力します",
136
135
  "text.employee-profile-not-linked-title": "社員記録と連携していないアカウントです",
137
136
  "text.employee-profile-not-linked-description": "プロフィールは社員本人が書きます。$t(term.domain)管理者が社員一覧でこのアカウントを社員と連携できます。",
138
137
  "text.employee-profile-select-rows-first": "ダウンロードする社員を先に選んでください",
139
138
  "text.employee-profile-file": "人材プロフィール",
140
139
  "text.employee-profile-empty": "在職中の社員がいません",
141
- "text.employee-profile-no-rows": "まだ記入がありません",
142
140
  "task.organization.own-profile.name": "自分のプロフィール",
143
141
  "task.organization.own-profile.description": "自分のプロフィール(概要・保有スキル・学歴・資格・職歴・プロジェクト履歴)を読み書きする。",
144
142
  "task.organization.profiles-reading.name": "人材プロフィールの閲覧",
145
- "task.organization.profiles-reading.description": "在職中の全社員のプロフィールを読み、Excelでダウンロードする。連絡先・住所・口座・給与は含まない。"
143
+ "task.organization.profiles-reading.description": "在職中の全社員のプロフィールを読み、Excelでダウンロードする。連絡先・住所・口座・給与は含まない。",
144
+ "text.employee-profile-doc-title": "人材プロフィール",
145
+ "text.employee-profile-doc-roster": "人材一覧",
146
+ "text.employee-profile-doc-ongoing": "現在",
147
+ "text.employee-profile-doc-none": "該当なし",
148
+ "text.employee-profile-doc-written-on": "作成日",
149
+ "text.employee-profile-doc-number": "番号",
150
+ "text.employee-profile-doc-years": "{years}年{months}ヶ月",
151
+ "label.employee-profile-career-length": "総経歴",
152
+ "button.employee-profile-download-pdf": "PDFでダウンロード",
153
+ "text.employee-profile-completeness": "プロフィール完成度",
154
+ "text.employee-profile-to-fill": "未入力",
155
+ "text.employee-profile-ready": "見積に添付する準備ができました",
156
+ "text.employee-profile-unsaved": "保存していない変更があります",
157
+ "text.employee-profile-summary-example": "例: 製造MESの設計・構築10年。工程モデリングと設備連携を主に担当し、直近3年はPMとして従事。",
158
+ "text.employee-profile-skill-placeholder": "スキルを入力してEnter",
159
+ "text.employee-profile-why-educations": "最終学歴から記入します。",
160
+ "text.employee-profile-why-certificates": "入札で見られる資格を記入します。",
161
+ "text.employee-profile-why-careers": "現在の会社に入る前の経歴です。総経歴に加算されます。",
162
+ "text.employee-profile-why-projects": "見積に添付するプロフィールで最初に読まれる箇所です。担当した役割と内容を記入します。",
163
+ "label.employee-profile-ongoing": "進行中",
164
+ "text.employee-profile-new-entry": "新しい項目",
165
+ "text.employee-profile-doc-expertise": "コア・コンピテンシー",
166
+ "text.employee-profile-doc-projects-done": "遂行プロジェクト",
167
+ "text.employee-profile-doc-certificates-held": "保有資格",
168
+ "text.employee-profile-doc-project-count": "{n}件",
169
+ "text.employee-profile-doc-certificate-count": "{n}件",
170
+ "text.employee-profile-doc-people": "{n}名",
171
+ "text.employee-profile-doc-average-career": "平均経歴",
172
+ "label.employee-profile-document-language": "文書の言語",
173
+ "button.employee-profile-preview": "プレビュー",
174
+ "title.employee-profile-preview": "人材プロフィールのプレビュー",
175
+ "label.organization-entry-employee-profile": "マイプロフィール",
176
+ "label.organization-entry-profile-section": "区分",
177
+ "label.organization-entry-profile-text": "内容",
178
+ "label.organization-entry-profile-name": "名称",
179
+ "text.organization-entry-profile-section-summary": "概要",
180
+ "text.organization-entry-profile-section-skill": "スキル",
181
+ "text.organization-entry-profile-section-education": "学歴",
182
+ "text.organization-entry-profile-section-certificate": "資格",
183
+ "text.organization-entry-profile-section-career": "経歴",
184
+ "text.organization-entry-profile-section-project": "プロジェクト",
185
+ "text.organization-entry-profile-one-summary": "概要は一つだけです"
146
186
  }
@@ -132,15 +132,55 @@
132
132
  "label.employee-profile-role": "역할",
133
133
  "label.employee-profile-technologies": "사용 기술",
134
134
  "label.employee-profile-updated-at": "프로필 갱신일",
135
- "text.employee-profile-skills-hint": "쉼표로 나눠 적습니다",
136
135
  "text.employee-profile-not-linked-title": "직원 기록과 이어지지 않은 계정입니다",
137
136
  "text.employee-profile-not-linked-description": "프로필은 직원이 자기 것을 씁니다. $t(term.domain) 관리자가 직원 목록에서 이 계정을 직원과 이어 줄 수 있습니다.",
138
137
  "text.employee-profile-select-rows-first": "내려받을 직원을 먼저 고르십시오",
139
138
  "text.employee-profile-file": "인력프로필",
140
139
  "text.employee-profile-empty": "재직 중인 직원이 없습니다",
141
- "text.employee-profile-no-rows": "아직 적은 것이 없습니다",
142
140
  "task.organization.own-profile.name": "내 프로필",
143
141
  "task.organization.own-profile.description": "자기 프로필(요약 · 보유 기술 · 학력 · 자격 · 경력 · 프로젝트 이력)을 읽고 쓴다.",
144
142
  "task.organization.profiles-reading.name": "인력 프로필 열람",
145
- "task.organization.profiles-reading.description": "재직 중인 모든 직원의 프로필을 읽고 엑셀로 내려받는다. 연락처 · 주소 · 계좌 · 급여는 빠진다."
143
+ "task.organization.profiles-reading.description": "재직 중인 모든 직원의 프로필을 읽고 엑셀로 내려받는다. 연락처 · 주소 · 계좌 · 급여는 빠진다.",
144
+ "text.employee-profile-doc-title": "인력 프로필",
145
+ "text.employee-profile-doc-roster": "인력 현황",
146
+ "text.employee-profile-doc-ongoing": "현재",
147
+ "text.employee-profile-doc-none": "해당 없음",
148
+ "text.employee-profile-doc-written-on": "작성일",
149
+ "text.employee-profile-doc-number": "번호",
150
+ "text.employee-profile-doc-years": "{years}년 {months}개월",
151
+ "label.employee-profile-career-length": "총 경력",
152
+ "button.employee-profile-download-pdf": "PDF로 내려받기",
153
+ "text.employee-profile-completeness": "프로필 완성도",
154
+ "text.employee-profile-to-fill": "채울 곳",
155
+ "text.employee-profile-ready": "견적에 붙일 준비가 됐습니다",
156
+ "text.employee-profile-unsaved": "저장하지 않은 변경이 있습니다",
157
+ "text.employee-profile-summary-example": "예: 제조 MES 설계·구축 10년. 공정 모델링과 설비 연동을 주로 맡았고, 최근 3년은 PM으로 일했습니다.",
158
+ "text.employee-profile-skill-placeholder": "기술을 적고 Enter",
159
+ "text.employee-profile-why-educations": "최종 학력부터 적습니다.",
160
+ "text.employee-profile-why-certificates": "정보처리기사처럼 입찰에서 보는 자격을 적습니다.",
161
+ "text.employee-profile-why-careers": "지금 회사에 오기 전의 경력입니다. 총 경력에 더해집니다.",
162
+ "text.employee-profile-why-projects": "견적에 붙는 프로필에서 가장 먼저 읽히는 곳입니다. 맡은 역할과 한 일을 적습니다.",
163
+ "label.employee-profile-ongoing": "진행 중",
164
+ "text.employee-profile-new-entry": "새 항목",
165
+ "text.employee-profile-doc-expertise": "핵심 역량",
166
+ "text.employee-profile-doc-projects-done": "수행 프로젝트",
167
+ "text.employee-profile-doc-certificates-held": "보유 자격",
168
+ "text.employee-profile-doc-project-count": "{n}건",
169
+ "text.employee-profile-doc-certificate-count": "{n}개",
170
+ "text.employee-profile-doc-people": "{n}명",
171
+ "text.employee-profile-doc-average-career": "평균 경력",
172
+ "label.employee-profile-document-language": "문서 언어",
173
+ "button.employee-profile-preview": "미리 보기",
174
+ "title.employee-profile-preview": "인력 프로필 미리 보기",
175
+ "label.organization-entry-employee-profile": "내 프로필",
176
+ "label.organization-entry-profile-section": "구분",
177
+ "label.organization-entry-profile-text": "내용",
178
+ "label.organization-entry-profile-name": "이름",
179
+ "text.organization-entry-profile-section-summary": "요약",
180
+ "text.organization-entry-profile-section-skill": "보유 기술",
181
+ "text.organization-entry-profile-section-education": "학력",
182
+ "text.organization-entry-profile-section-certificate": "자격",
183
+ "text.organization-entry-profile-section-career": "경력",
184
+ "text.organization-entry-profile-section-project": "프로젝트",
185
+ "text.organization-entry-profile-one-summary": "요약은 하나만 들어갑니다"
146
186
  }
@@ -132,15 +132,55 @@
132
132
  "label.employee-profile-role": "peranan",
133
133
  "label.employee-profile-technologies": "teknologi",
134
134
  "label.employee-profile-updated-at": "profil dikemas kini",
135
- "text.employee-profile-skills-hint": "pisahkan dengan koma",
136
135
  "text.employee-profile-not-linked-title": "akaun ini tidak dipautkan kepada rekod pekerja",
137
136
  "text.employee-profile-not-linked-description": "setiap pekerja menulis profil sendiri. Pentadbir $t(term.domain) boleh memautkan akaun ini kepada pekerja dalam senarai pekerja.",
138
137
  "text.employee-profile-select-rows-first": "pilih pekerja yang hendak dimuat turun dahulu",
139
138
  "text.employee-profile-file": "profil-pekerja",
140
139
  "text.employee-profile-empty": "tiada pekerja aktif",
141
- "text.employee-profile-no-rows": "belum ada yang ditulis",
142
140
  "task.organization.own-profile.name": "Profil saya",
143
141
  "task.organization.own-profile.description": "Membaca dan menulis profil sendiri: ringkasan, kemahiran, pendidikan, sijil, kerjaya dan projek.",
144
142
  "task.organization.profiles-reading.name": "Membaca profil pekerja",
145
- "task.organization.profiles-reading.description": "Membaca profil setiap pekerja aktif dan memuat turunnya sebagai Excel. Hubungan, alamat, akaun dan gaji tidak termasuk."
143
+ "task.organization.profiles-reading.description": "Membaca profil setiap pekerja aktif dan memuat turunnya sebagai Excel. Hubungan, alamat, akaun dan gaji tidak termasuk.",
144
+ "text.employee-profile-doc-title": "Profil Kakitangan",
145
+ "text.employee-profile-doc-roster": "Senarai Kakitangan",
146
+ "text.employee-profile-doc-ongoing": "kini",
147
+ "text.employee-profile-doc-none": "Tiada",
148
+ "text.employee-profile-doc-written-on": "Tarikh",
149
+ "text.employee-profile-doc-number": "Bil.",
150
+ "text.employee-profile-doc-years": "{years} thn {months} bln",
151
+ "label.employee-profile-career-length": "jumlah pengalaman",
152
+ "button.employee-profile-download-pdf": "muat turun PDF",
153
+ "text.employee-profile-completeness": "Kelengkapan profil",
154
+ "text.employee-profile-to-fill": "Untuk diisi",
155
+ "text.employee-profile-ready": "Sedia dilampirkan pada sebut harga",
156
+ "text.employee-profile-unsaved": "Ada perubahan yang belum disimpan",
157
+ "text.employee-profile-summary-example": "cth. Sepuluh tahun mereka bentuk dan membina MES pembuatan; pemodelan proses dan integrasi peralatan, PM tiga tahun kebelakangan.",
158
+ "text.employee-profile-skill-placeholder": "Taip kemahiran dan tekan Enter",
159
+ "text.employee-profile-why-educations": "Mulakan dengan kelulusan terkini.",
160
+ "text.employee-profile-why-certificates": "Sijil yang dilihat dalam tender.",
161
+ "text.employee-profile-why-careers": "Pekerjaan sebelum syarikat ini - dikira dalam jumlah pengalaman.",
162
+ "text.employee-profile-why-projects": "Bahagian pertama yang dibaca pada profil yang dilampirkan - peranan dan kerja yang dibuat.",
163
+ "label.employee-profile-ongoing": "sedang berjalan",
164
+ "text.employee-profile-new-entry": "Entri baharu",
165
+ "text.employee-profile-doc-expertise": "Kepakaran Teras",
166
+ "text.employee-profile-doc-projects-done": "Projek disiapkan",
167
+ "text.employee-profile-doc-certificates-held": "Pensijilan",
168
+ "text.employee-profile-doc-project-count": "{n}",
169
+ "text.employee-profile-doc-certificate-count": "{n}",
170
+ "text.employee-profile-doc-people": "{n} orang",
171
+ "text.employee-profile-doc-average-career": "Purata pengalaman",
172
+ "label.employee-profile-document-language": "bahasa dokumen",
173
+ "button.employee-profile-preview": "pratonton",
174
+ "title.employee-profile-preview": "pratonton profil kakitangan",
175
+ "label.organization-entry-employee-profile": "profil saya",
176
+ "label.organization-entry-profile-section": "bahagian",
177
+ "label.organization-entry-profile-text": "teks",
178
+ "label.organization-entry-profile-name": "nama",
179
+ "text.organization-entry-profile-section-summary": "ringkasan",
180
+ "text.organization-entry-profile-section-skill": "kemahiran",
181
+ "text.organization-entry-profile-section-education": "pendidikan",
182
+ "text.organization-entry-profile-section-certificate": "pensijilan",
183
+ "text.organization-entry-profile-section-career": "kerjaya",
184
+ "text.organization-entry-profile-section-project": "projek",
185
+ "text.organization-entry-profile-one-summary": "Profil hanya ada satu ringkasan"
146
186
  }
@@ -132,15 +132,55 @@
132
132
  "label.employee-profile-role": "角色",
133
133
  "label.employee-profile-technologies": "使用技术",
134
134
  "label.employee-profile-updated-at": "简历更新日期",
135
- "text.employee-profile-skills-hint": "用逗号分隔",
136
135
  "text.employee-profile-not-linked-title": "此账户未与员工记录关联",
137
136
  "text.employee-profile-not-linked-description": "简历由员工本人填写。$t(term.domain)管理员可以在员工列表中将此账户与员工关联。",
138
137
  "text.employee-profile-select-rows-first": "请先选择要下载的员工",
139
138
  "text.employee-profile-file": "人员简历",
140
139
  "text.employee-profile-empty": "没有在职员工",
141
- "text.employee-profile-no-rows": "尚未填写",
142
140
  "task.organization.own-profile.name": "我的简历",
143
141
  "task.organization.own-profile.description": "读取并填写自己的简历(简介、掌握技能、学历、资格证书、工作经历、项目经历)。",
144
142
  "task.organization.profiles-reading.name": "查看人员简历",
145
- "task.organization.profiles-reading.description": "读取所有在职员工的简历并下载为 Excel。不含联系方式、地址、账户和工资。"
143
+ "task.organization.profiles-reading.description": "读取所有在职员工的简历并下载为 Excel。不含联系方式、地址、账户和工资。",
144
+ "text.employee-profile-doc-title": "人员简历",
145
+ "text.employee-profile-doc-roster": "人员一览",
146
+ "text.employee-profile-doc-ongoing": "至今",
147
+ "text.employee-profile-doc-none": "无",
148
+ "text.employee-profile-doc-written-on": "填写日期",
149
+ "text.employee-profile-doc-number": "序号",
150
+ "text.employee-profile-doc-years": "{years}年{months}个月",
151
+ "label.employee-profile-career-length": "总经历",
152
+ "button.employee-profile-download-pdf": "下载 PDF",
153
+ "text.employee-profile-completeness": "简历完成度",
154
+ "text.employee-profile-to-fill": "待填写",
155
+ "text.employee-profile-ready": "已可附在报价单上",
156
+ "text.employee-profile-unsaved": "有未保存的更改",
157
+ "text.employee-profile-summary-example": "例:制造 MES 设计与实施 10 年,主要负责工艺建模与设备集成,近 3 年担任 PM。",
158
+ "text.employee-profile-skill-placeholder": "输入技能后按 Enter",
159
+ "text.employee-profile-why-educations": "从最高学历开始填写。",
160
+ "text.employee-profile-why-certificates": "填写投标时会看的资格证书。",
161
+ "text.employee-profile-why-careers": "加入本公司之前的经历,计入总经历。",
162
+ "text.employee-profile-why-projects": "报价所附简历中最先被阅读的部分。填写担任的角色和所做的工作。",
163
+ "label.employee-profile-ongoing": "进行中",
164
+ "text.employee-profile-new-entry": "新条目",
165
+ "text.employee-profile-doc-expertise": "核心能力",
166
+ "text.employee-profile-doc-projects-done": "完成项目",
167
+ "text.employee-profile-doc-certificates-held": "持有资格",
168
+ "text.employee-profile-doc-project-count": "{n}个",
169
+ "text.employee-profile-doc-certificate-count": "{n}项",
170
+ "text.employee-profile-doc-people": "{n}人",
171
+ "text.employee-profile-doc-average-career": "平均经历",
172
+ "label.employee-profile-document-language": "文档语言",
173
+ "button.employee-profile-preview": "预览",
174
+ "title.employee-profile-preview": "人员简历预览",
175
+ "label.organization-entry-employee-profile": "我的简历",
176
+ "label.organization-entry-profile-section": "类别",
177
+ "label.organization-entry-profile-text": "内容",
178
+ "label.organization-entry-profile-name": "名称",
179
+ "text.organization-entry-profile-section-summary": "概要",
180
+ "text.organization-entry-profile-section-skill": "技能",
181
+ "text.organization-entry-profile-section-education": "学历",
182
+ "text.organization-entry-profile-section-certificate": "资格",
183
+ "text.organization-entry-profile-section-career": "经历",
184
+ "text.organization-entry-profile-section-project": "项目",
185
+ "text.organization-entry-profile-one-summary": "概要只有一条"
146
186
  }
@@ -1,33 +0,0 @@
1
- import * as XLSX from 'xlsx';
2
- type Row = Record<string, string | undefined>;
3
- export type ProfilePerson = {
4
- name?: string | null;
5
- departmentName?: string | null;
6
- jobPosition?: string | null;
7
- jobResponsibility?: string | null;
8
- hiredOn?: string | null;
9
- profile?: {
10
- summary?: string | null;
11
- skills?: string[] | null;
12
- educations?: Row[] | null;
13
- certificates?: Row[] | null;
14
- careers?: Row[] | null;
15
- projects?: Row[] | null;
16
- } | null;
17
- };
18
- /** The words of the sheet, in the reader's language - handed in so this stays free of i18next. */
19
- export type ProfileSheetWords = Record<'name' | 'department' | 'jobPosition' | 'jobResponsibility' | 'hiredOn' | 'summary' | 'skills' | 'educations' | 'certificates' | 'careers' | 'projects' | 'school' | 'major' | 'degree' | 'period' | 'certificate' | 'issuer' | 'acquiredOn' | 'company' | 'position' | 'duties' | 'project' | 'client' | 'role' | 'technologies', string>;
20
- /** A period as one cell - an open end is still going on. */
21
- export declare function periodOf(startedOn?: string, endedOn?: string): string;
22
- /** One person's sheet as rows of cells. */
23
- export declare function profileSheetRows(person: ProfilePerson, words: ProfileSheetWords): string[][];
24
- /**
25
- * A sheet name for each person. Excel takes at most 31 characters, none of \ / ? * [ ] :, and no two alike ignoring
26
- * case - two people of one name become 「홍길동」 and 「홍길동 (2)」.
27
- */
28
- export declare function sheetNamesOf(names: (string | null | undefined)[]): string[];
29
- /** The workbook of the people given, in their order. */
30
- export declare function profileWorkbook(people: ProfilePerson[], words: ProfileSheetWords): XLSX.WorkBook;
31
- /** 「인력프로필-20261027.xlsx」 - the word in the reader's language, the day the profiles were taken. */
32
- export declare function profileFileName(word: string, day?: Date): string;
33
- export {};
@@ -1,87 +0,0 @@
1
- import * as XLSX from 'xlsx';
2
- /** A period as one cell - an open end is still going on. */
3
- export function periodOf(startedOn, endedOn) {
4
- if (!startedOn && !endedOn)
5
- return '';
6
- return [startedOn ?? '', endedOn ?? ''].join(' ~ ').trim();
7
- }
8
- /** One person's sheet as rows of cells. */
9
- export function profileSheetRows(person, words) {
10
- const profile = person.profile ?? {};
11
- const value = (text) => text ?? '';
12
- const table = (title, header, rows, cells) => [
13
- [],
14
- [title],
15
- header,
16
- ...(rows ?? []).map(row => cells(row).map(cell => cell ?? ''))
17
- ];
18
- return [
19
- [value(person.name)],
20
- [],
21
- [words.name, value(person.name)],
22
- [words.department, value(person.departmentName)],
23
- [words.jobPosition, value(person.jobPosition)],
24
- [words.jobResponsibility, value(person.jobResponsibility)],
25
- [words.hiredOn, value(person.hiredOn)],
26
- [words.summary, value(profile.summary)],
27
- [],
28
- [words.skills],
29
- [(profile.skills ?? []).join(', ')],
30
- ...table(words.educations, [words.school, words.major, words.degree, words.period], profile.educations, row => [
31
- row.school,
32
- row.major,
33
- row.degree,
34
- periodOf(row.startedOn, row.endedOn)
35
- ]),
36
- ...table(words.certificates, [words.certificate, words.issuer, words.acquiredOn], profile.certificates, row => [row.name, row.issuer, row.acquiredOn]),
37
- ...table(words.careers, [words.company, words.department, words.position, words.period, words.duties], profile.careers, row => [
38
- row.company,
39
- row.department,
40
- row.position,
41
- periodOf(row.startedOn, row.endedOn),
42
- row.duties
43
- ]),
44
- ...table(words.projects, [words.project, words.client, words.period, words.role, words.duties, words.technologies], profile.projects, row => [
45
- row.name,
46
- row.client,
47
- periodOf(row.startedOn, row.endedOn),
48
- row.role,
49
- row.duties,
50
- row.technologies
51
- ])
52
- ];
53
- }
54
- /**
55
- * A sheet name for each person. Excel takes at most 31 characters, none of \ / ? * [ ] :, and no two alike ignoring
56
- * case - two people of one name become 「홍길동」 and 「홍길동 (2)」.
57
- */
58
- export function sheetNamesOf(names) {
59
- const taken = new Set();
60
- return names.map((name, index) => {
61
- const base = (name ?? '').replace(/[\\/?*[\]:]/g, ' ').replace(/^'+|'+$/g, '').trim() || `#${index + 1}`;
62
- let candidate = base.slice(0, 31);
63
- for (let n = 2; taken.has(candidate.toLowerCase()); n++) {
64
- const tail = ` (${n})`;
65
- candidate = base.slice(0, 31 - tail.length) + tail;
66
- }
67
- taken.add(candidate.toLowerCase());
68
- return candidate;
69
- });
70
- }
71
- /** The workbook of the people given, in their order. */
72
- export function profileWorkbook(people, words) {
73
- const book = XLSX.utils.book_new();
74
- const names = sheetNamesOf(people.map(person => person.name));
75
- people.forEach((person, index) => {
76
- const sheet = XLSX.utils.aoa_to_sheet(profileSheetRows(person, words));
77
- sheet['!cols'] = [{ wch: 20 }, { wch: 24 }, { wch: 18 }, { wch: 20 }, { wch: 36 }, { wch: 24 }];
78
- XLSX.utils.book_append_sheet(book, sheet, names[index]);
79
- });
80
- return book;
81
- }
82
- /** 「인력프로필-20261027.xlsx」 - the word in the reader's language, the day the profiles were taken. */
83
- export function profileFileName(word, day = new Date()) {
84
- const pad = (n) => String(n).padStart(2, '0');
85
- return `${word}-${day.getFullYear()}${pad(day.getMonth() + 1)}${pad(day.getDate())}.xlsx`;
86
- }
87
- //# sourceMappingURL=employee-profile-workbook.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"employee-profile-workbook.js","sourceRoot":"","sources":["../../../client/pages/employee-profile/employee-profile-workbook.ts"],"names":[],"mappings":"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","sourcesContent":["import * as XLSX from 'xlsx'\n\n/*\n * The employee profiles a quotation attaches, as one workbook - one sheet a person, named by the person: the basics,\n * then each section as a titled table (ADR-0169 decision 5). Pure, apart from SheetJS building the book; the screen\n * only writes it out.\n */\n\ntype Row = Record<string, string | undefined>\n\nexport type ProfilePerson = {\n name?: string | null\n departmentName?: string | null\n jobPosition?: string | null\n jobResponsibility?: string | null\n hiredOn?: string | null\n profile?: {\n summary?: string | null\n skills?: string[] | null\n educations?: Row[] | null\n certificates?: Row[] | null\n careers?: Row[] | null\n projects?: Row[] | null\n } | null\n}\n\n/** The words of the sheet, in the reader's language - handed in so this stays free of i18next. */\nexport type ProfileSheetWords = Record<\n | 'name'\n | 'department'\n | 'jobPosition'\n | 'jobResponsibility'\n | 'hiredOn'\n | 'summary'\n | 'skills'\n | 'educations'\n | 'certificates'\n | 'careers'\n | 'projects'\n | 'school'\n | 'major'\n | 'degree'\n | 'period'\n | 'certificate'\n | 'issuer'\n | 'acquiredOn'\n | 'company'\n | 'position'\n | 'duties'\n | 'project'\n | 'client'\n | 'role'\n | 'technologies',\n string\n>\n\n/** A period as one cell - an open end is still going on. */\nexport function periodOf(startedOn?: string, endedOn?: string): string {\n if (!startedOn && !endedOn) return ''\n return [startedOn ?? '', endedOn ?? ''].join(' ~ ').trim()\n}\n\n/** One person's sheet as rows of cells. */\nexport function profileSheetRows(person: ProfilePerson, words: ProfileSheetWords): string[][] {\n const profile = person.profile ?? {}\n const value = (text?: string | null) => text ?? ''\n const table = (title: string, header: string[], rows: Row[] | null | undefined, cells: (row: Row) => (string | undefined)[]) => [\n [],\n [title],\n header,\n ...(rows ?? []).map(row => cells(row).map(cell => cell ?? ''))\n ]\n\n return [\n [value(person.name)],\n [],\n [words.name, value(person.name)],\n [words.department, value(person.departmentName)],\n [words.jobPosition, value(person.jobPosition)],\n [words.jobResponsibility, value(person.jobResponsibility)],\n [words.hiredOn, value(person.hiredOn)],\n [words.summary, value(profile.summary)],\n [],\n [words.skills],\n [(profile.skills ?? []).join(', ')],\n ...table(words.educations, [words.school, words.major, words.degree, words.period], profile.educations, row => [\n row.school,\n row.major,\n row.degree,\n periodOf(row.startedOn, row.endedOn)\n ]),\n ...table(words.certificates, [words.certificate, words.issuer, words.acquiredOn], profile.certificates, row => [row.name, row.issuer, row.acquiredOn]),\n ...table(words.careers, [words.company, words.department, words.position, words.period, words.duties], profile.careers, row => [\n row.company,\n row.department,\n row.position,\n periodOf(row.startedOn, row.endedOn),\n row.duties\n ]),\n ...table(words.projects, [words.project, words.client, words.period, words.role, words.duties, words.technologies], profile.projects, row => [\n row.name,\n row.client,\n periodOf(row.startedOn, row.endedOn),\n row.role,\n row.duties,\n row.technologies\n ])\n ]\n}\n\n/**\n * A sheet name for each person. Excel takes at most 31 characters, none of \\ / ? * [ ] :, and no two alike ignoring\n * case - two people of one name become 「홍길동」 and 「홍길동 (2)」.\n */\nexport function sheetNamesOf(names: (string | null | undefined)[]): string[] {\n const taken = new Set<string>()\n return names.map((name, index) => {\n const base = (name ?? '').replace(/[\\\\/?*[\\]:]/g, ' ').replace(/^'+|'+$/g, '').trim() || `#${index + 1}`\n let candidate = base.slice(0, 31)\n for (let n = 2; taken.has(candidate.toLowerCase()); n++) {\n const tail = ` (${n})`\n candidate = base.slice(0, 31 - tail.length) + tail\n }\n taken.add(candidate.toLowerCase())\n return candidate\n })\n}\n\n/** The workbook of the people given, in their order. */\nexport function profileWorkbook(people: ProfilePerson[], words: ProfileSheetWords): XLSX.WorkBook {\n const book = XLSX.utils.book_new()\n const names = sheetNamesOf(people.map(person => person.name))\n people.forEach((person, index) => {\n const sheet = XLSX.utils.aoa_to_sheet(profileSheetRows(person, words))\n sheet['!cols'] = [{ wch: 20 }, { wch: 24 }, { wch: 18 }, { wch: 20 }, { wch: 36 }, { wch: 24 }]\n XLSX.utils.book_append_sheet(book, sheet, names[index])\n })\n return book\n}\n\n/** 「인력프로필-20261027.xlsx」 - the word in the reader's language, the day the profiles were taken. */\nexport function profileFileName(word: string, day = new Date()): string {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${word}-${day.getFullYear()}${pad(day.getMonth() + 1)}${pad(day.getDate())}.xlsx`\n}\n"]}
@@ -1,89 +0,0 @@
1
- import * as XLSX from 'xlsx'
2
-
3
- import { periodOf, profileFileName, profileSheetRows, profileWorkbook, sheetNamesOf, type ProfileSheetWords } from '../client/pages/employee-profile/employee-profile-workbook'
4
-
5
- /** The profiles a quotation attaches: one workbook, a sheet a person (ADR-0169 decision 5). */
6
- const WORDS = new Proxy({}, { get: (_target, key) => `<${String(key)}>` }) as ProfileSheetWords
7
-
8
- const KIM = {
9
- name: '김하늘',
10
- departmentName: '개발팀',
11
- jobPosition: '과장',
12
- jobResponsibility: '팀장',
13
- hiredOn: '2020-03-02',
14
- profile: {
15
- summary: 'MES 10년',
16
- skills: ['TypeScript', 'PostgreSQL'],
17
- educations: [{ school: '서울대', major: '컴퓨터공학', degree: '학사', startedOn: '2005-03', endedOn: '2009-02' }],
18
- certificates: [{ name: '정보처리기사', issuer: '한국산업인력공단', acquiredOn: '2010-05-01' }],
19
- careers: [{ company: 'A사', department: '연구소', position: '대리', startedOn: '2009-03', endedOn: '2019-12', duties: 'ERP' }],
20
- projects: [{ name: '미라텍 MES', client: '미라텍', startedOn: '2024-01', role: 'PM', duties: '설계', technologies: 'Node' }]
21
- }
22
- }
23
-
24
- describe('the employee profile workbook', () => {
25
- it('★ a sheet is the basics, then each section as a titled table', () => {
26
- const rows = profileSheetRows(KIM, WORDS)
27
- expect(rows.slice(0, 11)).toEqual([
28
- ['김하늘'],
29
- [],
30
- ['<name>', '김하늘'],
31
- ['<department>', '개발팀'],
32
- ['<jobPosition>', '과장'],
33
- ['<jobResponsibility>', '팀장'],
34
- ['<hiredOn>', '2020-03-02'],
35
- ['<summary>', 'MES 10년'],
36
- [],
37
- ['<skills>'],
38
- ['TypeScript, PostgreSQL']
39
- ])
40
- const after = (title: string) => rows.slice(rows.findIndex(row => row[0] === title))
41
- expect(after('<educations>').slice(0, 3)).toEqual([['<educations>'], ['<school>', '<major>', '<degree>', '<period>'], ['서울대', '컴퓨터공학', '학사', '2005-03 ~ 2009-02']])
42
- expect(after('<certificates>').slice(0, 3)).toEqual([['<certificates>'], ['<certificate>', '<issuer>', '<acquiredOn>'], ['정보처리기사', '한국산업인력공단', '2010-05-01']])
43
- expect(after('<careers>').slice(0, 3)).toEqual([
44
- ['<careers>'],
45
- ['<company>', '<department>', '<position>', '<period>', '<duties>'],
46
- ['A사', '연구소', '대리', '2009-03 ~ 2019-12', 'ERP']
47
- ])
48
- expect(after('<projects>').slice(0, 3)).toEqual([
49
- ['<projects>'],
50
- ['<project>', '<client>', '<period>', '<role>', '<duties>', '<technologies>'],
51
- ['미라텍 MES', '미라텍', '2024-01 ~', 'PM', '설계', 'Node']
52
- ])
53
- })
54
-
55
- it('an employee with no profile still has a sheet - the basics and the empty tables', () => {
56
- const rows = profileSheetRows({ name: '박지민', profile: null }, WORDS)
57
- expect(rows[2]).toEqual(['<name>', '박지민'])
58
- expect(rows.filter(row => row[0] === '<projects>')).toHaveLength(1)
59
- expect(rows[rows.length - 1]).toEqual(['<project>', '<client>', '<period>', '<role>', '<duties>', '<technologies>'])
60
- })
61
-
62
- it('a period with one end open', () => {
63
- expect(periodOf('2024-01', '')).toBe('2024-01 ~')
64
- expect(periodOf('', '2024-01')).toBe('~ 2024-01')
65
- expect(periodOf()).toBe('')
66
- })
67
-
68
- it('★ one sheet a person, named by the person, in the order picked - read back from the written file', () => {
69
- const book = profileWorkbook([KIM, { name: '박지민' }], WORDS)
70
- const read = XLSX.read(XLSX.write(book, { type: 'buffer', bookType: 'xlsx' }), { type: 'buffer' })
71
- expect(read.SheetNames).toEqual(['김하늘', '박지민'])
72
- const kim = XLSX.utils.sheet_to_json<string[]>(read.Sheets['김하늘'], { header: 1, blankrows: true, defval: '' })
73
- expect(kim[0][0]).toBe('김하늘')
74
- expect(kim.some(row => row[0] === '미라텍 MES' && row[2] === '2024-01 ~')).toBe(true)
75
- })
76
-
77
- it('★ sheet names Excel takes: at most 31 characters, none of \\ / ? * [ ] :, no two alike', () => {
78
- const names = sheetNamesOf(['홍길동', '홍길동', 'HONG', 'hong', 'a/b:c*d?[e]', '', null, 'x'.repeat(40), 'x'.repeat(40)])
79
- expect(names.slice(0, 7)).toEqual(['홍길동', '홍길동 (2)', 'HONG', 'hong (2)', 'a b c d e', '#6', '#7'])
80
- expect(names[7]).toBe('x'.repeat(31))
81
- expect(names[8]).toBe('x'.repeat(27) + ' (2)')
82
- expect(new Set(names.map(name => name.toLowerCase())).size).toBe(names.length)
83
- expect(names.every(name => name.length <= 31 && !/[\\/?*[\]:]/.test(name))).toBe(true)
84
- })
85
-
86
- it('the file is named by the day it was taken', () => {
87
- expect(profileFileName('인력프로필', new Date(2026, 9, 7))).toBe('인력프로필-20261007.xlsx')
88
- })
89
- })