@codihaus/odp-app-hr 0.1.0

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 (192) hide show
  1. package/app/components/hr/contact/tab.vue +238 -0
  2. package/app/components/hr/department/card.vue +141 -0
  3. package/app/components/hr/department/form-modal.vue +90 -0
  4. package/app/components/hr/employees/assets/tab.vue +432 -0
  5. package/app/components/hr/employees/compensation.vue +136 -0
  6. package/app/components/hr/employees/contract.vue +77 -0
  7. package/app/components/hr/employees/insurance.vue +164 -0
  8. package/app/components/hr/employees/leave/tab.vue +180 -0
  9. package/app/components/hr/employees/provisioning/form-modal.vue +219 -0
  10. package/app/components/hr/employees/provisioning/tab.vue +187 -0
  11. package/app/components/hr/employees/tab.vue +38 -0
  12. package/app/components/hr/leave/calendar-tab.vue +649 -0
  13. package/app/components/hr/leave/override-modal.vue +62 -0
  14. package/app/components/hr/leave/request-modal.vue +185 -0
  15. package/app/components/hr/leave/requests-tab.vue +289 -0
  16. package/app/components/hr/leave/timeline-tab.vue +259 -0
  17. package/app/components/hr/offboarding/tab.vue +303 -0
  18. package/app/components/hr/person/activity/tab.vue +65 -0
  19. package/app/components/hr/person/activity/timeline.vue +119 -0
  20. package/app/components/hr/person/detail.vue +303 -0
  21. package/app/components/hr/person/document/tab-documents.vue +120 -0
  22. package/app/components/hr/person/document/template-edit-drawer.vue +215 -0
  23. package/app/components/hr/person/document/template-preview-card.vue +39 -0
  24. package/app/components/hr/person/document/trigger-modal.vue +121 -0
  25. package/app/components/hr/person/employee-form-modal.vue +78 -0
  26. package/app/components/hr/person/form-modal.vue +78 -0
  27. package/app/components/hr/person/list-row.vue +40 -0
  28. package/app/components/hr/person/profile/tab.vue +231 -0
  29. package/app/components/hr/settings/automation.vue +113 -0
  30. package/app/components/hr/settings/documents.vue +200 -0
  31. package/app/components/hr/settings/general.vue +87 -0
  32. package/app/components/hr/settings/holidays.vue +171 -0
  33. package/app/components/hr/settings/integrations.vue +185 -0
  34. package/app/components/hr/settings/policies.vue +83 -0
  35. package/app/components/hr/settings/policy/benefit-override-modal.vue +59 -0
  36. package/app/components/hr/settings/policy/editor-eligibility.vue +27 -0
  37. package/app/components/hr/settings/policy/editor-leave-base.vue +37 -0
  38. package/app/components/hr/settings/policy/editor-tenure-bonus.vue +61 -0
  39. package/app/components/hr/settings/recruitment.vue +128 -0
  40. package/app/components/hr/settings/taxonomies.vue +170 -0
  41. package/app/components/hr/shared/row.vue +21 -0
  42. package/app/components/hr/shared/section.vue +20 -0
  43. package/app/components/hr/shared/source-badge.vue +42 -0
  44. package/app/components/hr/shared/stage-badge.vue +24 -0
  45. package/app/components/hr/shared/workflow-timeline.vue +27 -0
  46. package/app/components/hr/talents/app-sidebar.vue +54 -0
  47. package/app/components/hr/talents/application-form-modal.vue +114 -0
  48. package/app/components/hr/talents/pipeline-picker.vue +56 -0
  49. package/app/components/hr/talents/step-detail.vue +133 -0
  50. package/app/components/hr/talents/step-stepper.vue +85 -0
  51. package/app/components/hr/talents/tab.vue +263 -0
  52. package/app/composables/use-departments.ts +59 -0
  53. package/app/composables/use-employee-detail.ts +24 -0
  54. package/app/composables/use-holidays.ts +48 -0
  55. package/app/composables/use-hr-api.ts +210 -0
  56. package/app/composables/use-hr-field-registry.ts +76 -0
  57. package/app/composables/use-hr-policies.ts +66 -0
  58. package/app/composables/use-hr-settings.ts +118 -0
  59. package/app/composables/use-leave.ts +71 -0
  60. package/app/composables/use-offboarding.ts +49 -0
  61. package/app/composables/use-people.ts +149 -0
  62. package/app/composables/use-providers.ts +44 -0
  63. package/app/composables/use-recruitment-workflow.ts +173 -0
  64. package/app/composables/use-templates.ts +44 -0
  65. package/app/composables/use-triggers.ts +26 -0
  66. package/app/config/column-renderers.ts +4 -0
  67. package/app/config/form-layouts.ts +193 -0
  68. package/app/data/hr-schema.ts +2608 -0
  69. package/app/lib/policy-engine.ts +116 -0
  70. package/app/pages/hr/departments.vue +114 -0
  71. package/app/pages/hr/employees/[id]/activity.vue +10 -0
  72. package/app/pages/hr/employees/[id]/assets.vue +14 -0
  73. package/app/pages/hr/employees/[id]/employment.vue +14 -0
  74. package/app/pages/hr/employees/[id]/index.vue +9 -0
  75. package/app/pages/hr/employees/[id]/offboarding.vue +7 -0
  76. package/app/pages/hr/employees/[id]/profile.vue +11 -0
  77. package/app/pages/hr/employees/[id]/provisioning.vue +17 -0
  78. package/app/pages/hr/employees/[id].vue +313 -0
  79. package/app/pages/hr/employees/index.vue +291 -0
  80. package/app/pages/hr/index.vue +3 -0
  81. package/app/pages/hr/leave.vue +79 -0
  82. package/app/pages/hr/settings.vue +43 -0
  83. package/app/pages/hr/setup.vue +3 -0
  84. package/app/pages/hr/talents/[id]/interview/[stepId].vue +231 -0
  85. package/app/pages/hr/talents/[id].vue +52 -0
  86. package/app/pages/hr/talents/index.vue +224 -0
  87. package/app/pages/hr.vue +129 -0
  88. package/app/plugins/hr-contacts-sync.client.ts +3 -0
  89. package/app/plugins/hr-extensions.ts +36 -0
  90. package/app/plugins/hr-setup.ts +5 -0
  91. package/app/plugins/navigations.ts +22 -0
  92. package/app/utils/hr-permissions.ts +27 -0
  93. package/app/utils/hr-policy-seed-step.ts +110 -0
  94. package/i18n/locales/en.json +726 -0
  95. package/i18n/locales/vi.json +688 -0
  96. package/nuxt.config.ts +19 -0
  97. package/package.json +27 -0
  98. package/server/api/hr/departments/[id].delete.ts +12 -0
  99. package/server/api/hr/departments/[id].patch.ts +14 -0
  100. package/server/api/hr/departments/index.get.ts +11 -0
  101. package/server/api/hr/departments/index.post.ts +13 -0
  102. package/server/api/hr/documents/templates/[id]/preview.post.ts +16 -0
  103. package/server/api/hr/documents/templates/[id].delete.ts +14 -0
  104. package/server/api/hr/documents/templates/[id].patch.ts +16 -0
  105. package/server/api/hr/documents/templates/index.get.ts +15 -0
  106. package/server/api/hr/documents/templates/index.post.ts +15 -0
  107. package/server/api/hr/documents/triggers/[id].patch.ts +16 -0
  108. package/server/api/hr/documents/triggers/index.get.ts +13 -0
  109. package/server/api/hr/fields/[collection].get.ts +14 -0
  110. package/server/api/hr/holidays/[id].delete.ts +14 -0
  111. package/server/api/hr/holidays/[id].patch.ts +16 -0
  112. package/server/api/hr/holidays/copy.post.ts +15 -0
  113. package/server/api/hr/holidays/index.get.ts +15 -0
  114. package/server/api/hr/holidays/index.post.ts +15 -0
  115. package/server/api/hr/leave/requests/[id].patch.ts +22 -0
  116. package/server/api/hr/leave/requests.get.ts +15 -0
  117. package/server/api/hr/leave/types.get.ts +13 -0
  118. package/server/api/hr/offboarding/[id]/cancel.post.ts +8 -0
  119. package/server/api/hr/offboarding/[id]/deprovision.post.ts +8 -0
  120. package/server/api/hr/offboarding/[id]/finalize.post.ts +8 -0
  121. package/server/api/hr/offboarding/[id]/return-assets.post.ts +8 -0
  122. package/server/api/hr/offboarding/[id]/settlement.get.ts +8 -0
  123. package/server/api/hr/offboarding/[id]/tasks/[taskId].patch.ts +10 -0
  124. package/server/api/hr/offboarding/[id].get.ts +8 -0
  125. package/server/api/hr/offboarding/[id].patch.ts +9 -0
  126. package/server/api/hr/offboarding/index.get.ts +7 -0
  127. package/server/api/hr/people/[id]/applications/[appId]/interviews/[iid].delete.ts +16 -0
  128. package/server/api/hr/people/[id]/applications/[appId]/interviews/[iid].patch.ts +18 -0
  129. package/server/api/hr/people/[id]/applications/[appId]/interviews/index.get.ts +15 -0
  130. package/server/api/hr/people/[id]/applications/[appId]/interviews/index.post.ts +17 -0
  131. package/server/api/hr/people/[id]/applications/[appId].patch.ts +17 -0
  132. package/server/api/hr/people/[id]/applications/index.get.ts +14 -0
  133. package/server/api/hr/people/[id]/applications/index.post.ts +16 -0
  134. package/server/api/hr/people/[id]/assets/[aid].delete.ts +13 -0
  135. package/server/api/hr/people/[id]/assets/[aid].patch.ts +15 -0
  136. package/server/api/hr/people/[id]/assets/index.get.ts +12 -0
  137. package/server/api/hr/people/[id]/assets/index.post.ts +14 -0
  138. package/server/api/hr/people/[id]/compensations.get.ts +14 -0
  139. package/server/api/hr/people/[id]/compensations.patch.ts +16 -0
  140. package/server/api/hr/people/[id]/contracts.get.ts +14 -0
  141. package/server/api/hr/people/[id]/contracts.patch.ts +16 -0
  142. package/server/api/hr/people/[id]/documents/[did].delete.ts +15 -0
  143. package/server/api/hr/people/[id]/documents/index.get.ts +14 -0
  144. package/server/api/hr/people/[id]/documents/index.post.ts +16 -0
  145. package/server/api/hr/people/[id]/insurances.get.ts +14 -0
  146. package/server/api/hr/people/[id]/insurances.patch.ts +16 -0
  147. package/server/api/hr/people/[id]/leave-balances/[bid].patch.ts +17 -0
  148. package/server/api/hr/people/[id]/leave-balances/index.get.ts +14 -0
  149. package/server/api/hr/people/[id]/leave-requests/index.get.ts +14 -0
  150. package/server/api/hr/people/[id]/leave-requests/index.post.ts +16 -0
  151. package/server/api/hr/people/[id]/link-user.post.ts +16 -0
  152. package/server/api/hr/people/[id]/notes/[nid].delete.ts +15 -0
  153. package/server/api/hr/people/[id]/notes/index.get.ts +14 -0
  154. package/server/api/hr/people/[id]/notes/index.post.ts +16 -0
  155. package/server/api/hr/people/[id]/offboarding/cases.get.ts +12 -0
  156. package/server/api/hr/people/[id]/offboarding.get.ts +12 -0
  157. package/server/api/hr/people/[id]/offboarding.post.ts +14 -0
  158. package/server/api/hr/people/[id]/provisioning/[logId]/retry.post.ts +7 -0
  159. package/server/api/hr/people/[id]/provisioning/index.get.ts +6 -0
  160. package/server/api/hr/people/[id]/provisioning/index.post.ts +7 -0
  161. package/server/api/hr/people/[id]/transition.post.ts +19 -0
  162. package/server/api/hr/people/[id]/transitions.get.ts +14 -0
  163. package/server/api/hr/people/[id].delete.ts +15 -0
  164. package/server/api/hr/people/[id].get.ts +14 -0
  165. package/server/api/hr/people/[id].patch.ts +17 -0
  166. package/server/api/hr/people/index.get.ts +15 -0
  167. package/server/api/hr/people/index.post.ts +19 -0
  168. package/server/api/hr/policies/[id].patch.ts +16 -0
  169. package/server/api/hr/policies/index.get.ts +13 -0
  170. package/server/api/hr/providers/[id]/test.post.ts +6 -0
  171. package/server/api/hr/providers/[id].delete.ts +6 -0
  172. package/server/api/hr/providers/[id].patch.ts +7 -0
  173. package/server/api/hr/providers/index.get.ts +5 -0
  174. package/server/api/hr/providers/index.post.ts +6 -0
  175. package/server/api/hr/settings/employment-types/[id].delete.ts +14 -0
  176. package/server/api/hr/settings/employment-types/[id].patch.ts +16 -0
  177. package/server/api/hr/settings/employment-types/index.get.ts +13 -0
  178. package/server/api/hr/settings/employment-types/index.post.ts +15 -0
  179. package/server/api/hr/settings/index.get.ts +13 -0
  180. package/server/api/hr/settings/index.patch.ts +15 -0
  181. package/server/api/hr/settings/leave-types/[id].delete.ts +14 -0
  182. package/server/api/hr/settings/leave-types/[id].patch.ts +16 -0
  183. package/server/api/hr/settings/leave-types/index.get.ts +13 -0
  184. package/server/api/hr/settings/leave-types/index.post.ts +15 -0
  185. package/shared/types/form-layout.ts +30 -0
  186. package/shared/types/index.ts +2 -0
  187. package/shared/types/integration.ts +41 -0
  188. package/shared/types/leave.ts +53 -0
  189. package/shared/types/offboarding.ts +46 -0
  190. package/shared/types/person.ts +54 -0
  191. package/shared/types/settings.ts +16 -0
  192. package/shared/utils/template-render.ts +155 -0
@@ -0,0 +1,291 @@
1
+ <script lang="ts" setup>
2
+ const { t } = useI18n();
3
+ const { employees, fetchPeople } = usePeople();
4
+ const { departments } = useDepartments();
5
+ const { can } = usePermissionRegistry();
6
+ const canCreate = computed(() => can('hr', 'create'));
7
+
8
+ await fetchPeople();
9
+
10
+ const showAddModal = ref(false);
11
+
12
+ function onEmployeeCreated(person: Person) {
13
+ navigateTo(`/hr/employees/${person.id}`);
14
+ }
15
+
16
+ const EMPLOYEE_STAGES: LifecycleStage[] = ["probation", "active", "resigned"];
17
+
18
+ const stageFilter = ref<LifecycleStage | "All">("All");
19
+ const departmentFilter = ref<string | "All">("All");
20
+ const search = ref("");
21
+
22
+ const filtered = computed(() => {
23
+ let list = employees.value;
24
+ if (stageFilter.value !== "All")
25
+ list = list.filter((p) => p.stage === stageFilter.value);
26
+ if (departmentFilter.value !== "All")
27
+ list = list.filter((p) => p.department === departmentFilter.value);
28
+ if (search.value.trim()) {
29
+ const q = search.value.trim().toLowerCase();
30
+ list = list.filter(
31
+ (p) =>
32
+ (p.display_name ?? "").toLowerCase().includes(q) ||
33
+ (p.email ?? "").toLowerCase().includes(q) ||
34
+ (p.employee_id ?? "").toLowerCase().includes(q) ||
35
+ (p.job_title ?? "").toLowerCase().includes(q),
36
+ );
37
+ }
38
+ return list;
39
+ });
40
+
41
+ const stageTabs = computed<
42
+ { value: LifecycleStage | "All"; label: string; count: number }[]
43
+ >(() => [
44
+ {
45
+ value: "All",
46
+ label: t("hr.employees.tabs.all"),
47
+ count: employees.value.length,
48
+ },
49
+ ...EMPLOYEE_STAGES.map((s) => ({
50
+ value: s,
51
+ label: t(`hr.stages.${s}`),
52
+ count: employees.value.filter((p) => p.stage === s).length,
53
+ })),
54
+ ]);
55
+
56
+ const hasActiveFilter = computed(
57
+ () =>
58
+ stageFilter.value !== "All" ||
59
+ departmentFilter.value !== "All" ||
60
+ !!search.value.trim(),
61
+ );
62
+
63
+ function clearFilters() {
64
+ stageFilter.value = "All";
65
+ departmentFilter.value = "All";
66
+ search.value = "";
67
+ }
68
+ </script>
69
+
70
+ <template>
71
+ <ModulePage
72
+ :title="$t('hr.employees.title')"
73
+ icon="i-ph-users-light"
74
+ padding="none"
75
+ max-width="full"
76
+ >
77
+ <template #actions>
78
+ <UButton
79
+ v-if="canCreate"
80
+ icon="i-ph-plus"
81
+ :label="$t('hr.employees.actions.add')"
82
+ color="primary"
83
+ size="sm"
84
+ @click="showAddModal = true"
85
+ />
86
+ </template>
87
+
88
+ <div class="flex flex-col h-full">
89
+ <!-- Toolbar -->
90
+ <div class="flex items-center gap-3 py-3 border-b border-default">
91
+ <div
92
+ class="flex items-center gap-1 bg-elevated rounded-lg p-0.5"
93
+ >
94
+ <button
95
+ v-for="tab in stageTabs"
96
+ :key="tab.value"
97
+ class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors"
98
+ :class="
99
+ stageFilter === tab.value
100
+ ? 'bg-default text-default shadow-sm'
101
+ : 'text-muted hover:text-default'
102
+ "
103
+ @click="stageFilter = tab.value"
104
+ >
105
+ {{ tab.label }}
106
+ <UBadge
107
+ :label="String(tab.count)"
108
+ variant="subtle"
109
+ size="md"
110
+ class="ml-1.5"
111
+ />
112
+ </button>
113
+ </div>
114
+
115
+ <USelectMenu
116
+ v-model="departmentFilter"
117
+ :items="[
118
+ $t('hr.employees.all_departments'),
119
+ ...departments.map((d: any) => d.name),
120
+ ]"
121
+ size="sm"
122
+ class="w-44"
123
+ />
124
+
125
+ <div class="flex-1" />
126
+
127
+ <UInput
128
+ v-model="search"
129
+ icon="i-ph-magnifying-glass"
130
+ :placeholder="$t('hr.employees.search_placeholder')"
131
+ size="sm"
132
+ class="w-64"
133
+ :ui="{ base: 'bg-elevated' }"
134
+ />
135
+ </div>
136
+
137
+ <!-- Table -->
138
+ <div class="flex-1 overflow-auto">
139
+ <table class="w-full text-sm">
140
+ <thead class="bg-elevated/50 sticky top-0 z-10">
141
+ <tr
142
+ class="text-left text-xs text-muted uppercase tracking-wider"
143
+ >
144
+ <th class="px-4 py-2.5 font-medium w-[280px]">
145
+ {{ $t("hr.employees.columns.name") }}
146
+ </th>
147
+ <th class="px-4 py-2.5 font-medium">
148
+ {{ $t("hr.employees.columns.id") }}
149
+ </th>
150
+ <th class="px-4 py-2.5 font-medium">
151
+ {{ $t("hr.employees.columns.department") }}
152
+ </th>
153
+ <th class="px-4 py-2.5 font-medium">
154
+ {{ $t("hr.employees.columns.job_title") }}
155
+ </th>
156
+ <th class="px-4 py-2.5 font-medium">
157
+ {{ $t("hr.employees.columns.type") }}
158
+ </th>
159
+ <th class="px-4 py-2.5 font-medium">
160
+ {{ $t("hr.employees.columns.start_date") }}
161
+ </th>
162
+ <th class="px-4 py-2.5 font-medium w-[120px]">
163
+ {{ $t("hr.employees.columns.stage") }}
164
+ </th>
165
+ </tr>
166
+ </thead>
167
+ <tbody class="divide-y divide-default">
168
+ <tr
169
+ v-for="p in filtered"
170
+ :key="p.id"
171
+ class="hover:bg-elevated/50 cursor-pointer transition-colors"
172
+ @click="navigateTo(`/hr/employees/${p.id}`)"
173
+ >
174
+ <td class="px-4 py-3">
175
+ <div class="flex items-center gap-3">
176
+ <UAvatar
177
+ icon="i-ph-user-light"
178
+ size="sm"
179
+ class="bg-primary/10 text-primary shrink-0"
180
+ />
181
+ <div class="min-w-0">
182
+ <div
183
+ class="font-medium text-default truncate"
184
+ >
185
+ {{ p.display_name }}
186
+ </div>
187
+ <div
188
+ class="text-xs text-muted truncate"
189
+ >
190
+ {{ p.email }}
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </td>
195
+ <td class="px-4 py-3 text-muted text-xs">
196
+ {{ p.employee_id ?? "—" }}
197
+ </td>
198
+ <td class="px-4 py-3 text-muted">
199
+ {{ p.department ?? "—" }}
200
+ </td>
201
+ <td class="px-4 py-3 text-muted">
202
+ {{ p.job_title ?? "—" }}
203
+ </td>
204
+ <td class="px-4 py-3 text-muted">
205
+ {{ p.employment_type ?? "—" }}
206
+ </td>
207
+ <td class="px-4 py-3 text-muted">
208
+ {{ p.start_date ?? "—" }}
209
+ </td>
210
+ <td class="px-4 py-3">
211
+ <HrSharedStageBadge :stage="p.stage" />
212
+ </td>
213
+ </tr>
214
+
215
+ <tr v-if="filtered.length === 0">
216
+ <td
217
+ colspan="7"
218
+ class="px-4 py-16 text-center text-muted"
219
+ >
220
+ <UIcon
221
+ :name="
222
+ hasActiveFilter
223
+ ? 'i-ph-funnel-x-light'
224
+ : 'i-ph-users-light'
225
+ "
226
+ class="size-10 mx-auto mb-3 opacity-40"
227
+ />
228
+ <p
229
+ class="text-sm font-medium text-default mb-1"
230
+ >
231
+ {{
232
+ hasActiveFilter
233
+ ? $t("hr.employees.no_match")
234
+ : $t("hr.employees.empty")
235
+ }}
236
+ </p>
237
+ <p class="text-xs mb-4">
238
+ {{
239
+ hasActiveFilter
240
+ ? $t("hr.employees.clear_hint")
241
+ : $t("hr.employees.empty_hint")
242
+ }}
243
+ </p>
244
+ <div
245
+ class="flex items-center justify-center gap-2"
246
+ >
247
+ <UButton
248
+ v-if="hasActiveFilter"
249
+ :label="
250
+ $t('hr.employees.clear_filters')
251
+ "
252
+ icon="i-ph-arrow-counter-clockwise-light"
253
+ variant="outline"
254
+ size="sm"
255
+ @click="clearFilters"
256
+ />
257
+ <UButton
258
+ v-if="canCreate"
259
+ :label="$t('hr.employees.actions.add')"
260
+ icon="i-ph-plus"
261
+ color="primary"
262
+ size="sm"
263
+ @click="showAddModal = true"
264
+ />
265
+ </div>
266
+ </td>
267
+ </tr>
268
+ </tbody>
269
+ </table>
270
+ </div>
271
+
272
+ <!-- Footer -->
273
+ <div
274
+ class="flex items-center justify-between px-4 py-2.5 border-t border-default text-xs text-muted"
275
+ >
276
+ <span
277
+ >{{ filtered.length }}
278
+ {{
279
+ filtered.length === 1
280
+ ? $t("hr.employees.count_singular")
281
+ : $t("hr.employees.count_plural")
282
+ }}</span
283
+ >
284
+ </div>
285
+ </div>
286
+ <HrPersonEmployeeFormModal
287
+ v-model:open="showAddModal"
288
+ @created="onEmployeeCreated"
289
+ />
290
+ </ModulePage>
291
+ </template>
@@ -0,0 +1,3 @@
1
+ <script lang="ts" setup>
2
+ navigateTo('/hr/employees', { replace: true })
3
+ </script>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <ModulePage
3
+ :title="$t('hr.leave.title')"
4
+ icon="i-ph-calendar-check-light"
5
+ padding="none"
6
+ max-width="full"
7
+ >
8
+ <div class="flex flex-col h-full">
9
+ <!-- View switcher -->
10
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-default">
11
+ <div class="flex items-center gap-1 bg-elevated rounded-lg p-0.5">
12
+ <button
13
+ v-for="view in views"
14
+ :key="view.id"
15
+ class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors cursor-pointer flex items-center gap-1.5"
16
+ :class="activeView === view.id
17
+ ? 'bg-default text-default shadow-sm'
18
+ : 'text-muted hover:text-default'"
19
+ @click="activeView = view.id"
20
+ >
21
+ <UIcon
22
+ :name="view.icon"
23
+ class="size-3.5"
24
+ />
25
+ {{ view.label }}
26
+ <UBadge
27
+ v-if="view.id === 'list' && pendingCount > 0"
28
+ :label="String(pendingCount)"
29
+ color="warning"
30
+ variant="subtle"
31
+ size="xs"
32
+ class="ml-0.5"
33
+ />
34
+ </button>
35
+ </div>
36
+ <div class="flex-1" />
37
+ </div>
38
+
39
+ <!-- Views -->
40
+ <div class="flex-1 overflow-hidden">
41
+ <HrLeaveRequestsTab
42
+ v-if="activeView === 'list'"
43
+ class="h-full"
44
+ />
45
+ <HrLeaveCalendarTab
46
+ v-else-if="activeView === 'calendar'"
47
+ class="h-full"
48
+ />
49
+ <HrLeaveTimelineTab
50
+ v-else-if="activeView === 'timeline'"
51
+ class="h-full"
52
+ />
53
+ </div>
54
+ </div>
55
+ </ModulePage>
56
+ </template>
57
+
58
+ <script lang="ts" setup>
59
+ const { t } = useI18n()
60
+ const { pendingCount, fetchRequests } = useLeave()
61
+ const { fetchHolidays } = useHolidays()
62
+ const { fetchPeople } = usePeople()
63
+
64
+ const activeView = useState<'list' | 'calendar' | 'timeline'>('leave.activeView', () => 'list')
65
+
66
+ const views = computed(() => [
67
+ { id: 'list' as const, label: t('hr.leave.views.list'), icon: 'i-ph-list-light' },
68
+ { id: 'calendar' as const, label: t('hr.leave.views.calendar'), icon: 'i-ph-calendar-light' },
69
+ { id: 'timeline' as const, label: t('hr.leave.views.timeline'), icon: 'i-ph-chart-bar-horizontal-light' },
70
+ ])
71
+
72
+ onMounted(async () => {
73
+ await Promise.all([
74
+ fetchRequests(),
75
+ fetchHolidays(new Date().getFullYear()),
76
+ fetchPeople(),
77
+ ])
78
+ })
79
+ </script>
@@ -0,0 +1,43 @@
1
+ <script lang="ts" setup>
2
+ const route = useRoute()
3
+ const { t } = useI18n()
4
+ const { can } = usePermissionRegistry()
5
+
6
+ if (!can('hr', 'admin')) {
7
+ navigateTo('/hr')
8
+ }
9
+
10
+ const activeTab = computed(() => (route.query.tab as string) || 'general')
11
+
12
+ const tabLabels: Record<string, { label: string, description: string }> = {
13
+ general: { label: t('hr.settings.general'), description: 'Company defaults and lifecycle stage labels' },
14
+ taxonomies: { label: t('hr.settings.taxonomies'), description: 'Leave types and employment types used across HR' },
15
+ policies: { label: t('hr.settings.policies'), description: 'Configure leave, insurance and probation policies' },
16
+ holidays: { label: t('hr.settings.holidays'), description: 'Company holidays shown across the Leave calendar and timeline' },
17
+ recruitment: { label: t('hr.settings.recruitment.title'), description: t('hr.settings.recruitment.description') },
18
+ documents: { label: t('hr.settings.documents'), description: 'Document templates with placeholders and live preview' },
19
+ automation: { label: t('hr.settings.automation'), description: 'Wire lifecycle events to document templates' },
20
+ integrations: { label: t('hr.settings.integrations'), description: t('integrations.description') },
21
+ }
22
+
23
+ const current = computed(() => tabLabels[activeTab.value] ?? tabLabels.general)
24
+ </script>
25
+
26
+ <template>
27
+ <ModulePage
28
+ :title="`Settings · ${current.label}`"
29
+ :subtitle="current.description"
30
+ icon="i-ph-gear-light"
31
+ padding="default"
32
+ max-width="xl"
33
+ >
34
+ <HrSettingsGeneral v-if="activeTab === 'general'" />
35
+ <HrSettingsTaxonomies v-else-if="activeTab === 'taxonomies'" />
36
+ <HrSettingsPolicies v-else-if="activeTab === 'policies'" />
37
+ <HrSettingsHolidays v-else-if="activeTab === 'holidays'" />
38
+ <HrSettingsRecruitment v-else-if="activeTab === 'recruitment'" />
39
+ <HrSettingsDocuments v-else-if="activeTab === 'documents'" />
40
+ <HrSettingsAutomation v-else-if="activeTab === 'automation'" />
41
+ <HrSettingsIntegrations v-else-if="activeTab === 'integrations'" />
42
+ </ModulePage>
43
+ </template>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <SetupWizardRoot module-key="hr" />
3
+ </template>
@@ -0,0 +1,231 @@
1
+ <script lang="ts" setup>
2
+ import type { WorkflowAction } from "@odp/workflow/shared/types"
3
+
4
+ const route = useRoute()
5
+ const { t } = useI18n()
6
+ const toast = useToast()
7
+ const { $api } = useNuxtApp()
8
+ const { loadInstance, approveStep, rejectStep } = useRecruitmentWorkflow()
9
+ const { user: currentUser } = useCurrentUser()
10
+
11
+ const personId = computed(() => String(route.params.id))
12
+ const stepId = computed(() => String(route.params.stepId))
13
+
14
+ interface PersonSummary {
15
+ id: number | string
16
+ display_name: string
17
+ email: string | null
18
+ phone: string | null
19
+ desired_position: string | null
20
+ source: string | null
21
+ stage: string
22
+ }
23
+ interface Application {
24
+ id: number
25
+ status: string
26
+ position: string | null
27
+ notes: string | null
28
+ workflow_instance_id: string | null
29
+ date_created: string
30
+ }
31
+ interface Interview {
32
+ id: number
33
+ application_id: number
34
+ type: string
35
+ status: string
36
+ score: number | null
37
+ interviewer: string | null
38
+ scheduled_at: string | null
39
+ notes: string | null
40
+ label: string | null
41
+ workflow_step_id: string | null
42
+ }
43
+
44
+ const person = ref<PersonSummary | null>(null)
45
+ const application = ref<Application | null>(null)
46
+ const interview = ref<Interview | null>(null)
47
+ const stepDef = ref<any>(null)
48
+ const instanceStep = ref<any>(null)
49
+ const actions = ref<WorkflowAction[]>([])
50
+ const loading = ref(true)
51
+ const rejectComment = ref("")
52
+ const showRejectModal = ref(false)
53
+
54
+ const stepStatus = computed(() => instanceStep.value?.status ?? "pending")
55
+ const canApprove = computed(() => stepStatus.value === "active" && application.value?.status === "active")
56
+ const isActive = computed(() => application.value?.status === "active")
57
+
58
+ async function loadData() {
59
+ loading.value = true
60
+ try {
61
+ const api = useHrApi()
62
+ person.value = await api.getPerson(personId.value)
63
+
64
+ const appsRes = await $api<{ data: Application[] }>(`/hr/people/${personId.value}/applications`)
65
+ const apps = appsRes.data ?? []
66
+ const app = apps.find(a => a.workflow_instance_id)
67
+ if (!app?.workflow_instance_id) { loading.value = false; return }
68
+ application.value = app
69
+
70
+ const progress = await loadInstance(app.workflow_instance_id)
71
+ if (!progress) { loading.value = false; return }
72
+
73
+ stepDef.value = progress.steps.find(s => s.id === stepId.value) ?? null
74
+ instanceStep.value = progress.instanceSteps.find(is => is.step_id === stepId.value) ?? null
75
+ actions.value = progress.actions
76
+
77
+ const ivsRes = await $api<{ data: Interview[] }>(`/hr/people/${personId.value}/applications/${app.id}/interviews`)
78
+ const ivs = ivsRes.data ?? []
79
+ interview.value = ivs.find(iv => iv.workflow_step_id === stepId.value) ?? null
80
+ } catch {
81
+ toast.add({ title: t("hr.toast.error"), color: "error" })
82
+ } finally {
83
+ loading.value = false
84
+ }
85
+ }
86
+
87
+ async function updateInterview(patch: Record<string, unknown>) {
88
+ if (!interview.value || !application.value) return
89
+ try {
90
+ const res = await $api<{ data: Interview }>(
91
+ `/hr/people/${personId.value}/applications/${application.value.id}/interviews/${interview.value.id}`,
92
+ { method: "PATCH", body: patch },
93
+ )
94
+ interview.value = res.data
95
+ } catch {
96
+ toast.add({ title: t("hr.toast.error"), color: "error" })
97
+ }
98
+ }
99
+
100
+ async function handleApprove() {
101
+ if (!application.value?.workflow_instance_id || !instanceStep.value) return
102
+ try {
103
+ await approveStep(application.value.workflow_instance_id, instanceStep.value.step_id)
104
+ if (interview.value?.status === "Scheduled") await updateInterview({ status: "Done" })
105
+ toast.add({ title: t("hr.recruitment.step_approved"), color: "success" })
106
+ await loadData()
107
+ } catch {
108
+ toast.add({ title: t("hr.toast.error"), color: "error" })
109
+ }
110
+ }
111
+
112
+ async function handleReject() {
113
+ if (!application.value?.workflow_instance_id || !instanceStep.value || !rejectComment.value.trim()) return
114
+ try {
115
+ await rejectStep(application.value.workflow_instance_id, instanceStep.value.step_id, rejectComment.value.trim())
116
+ showRejectModal.value = false
117
+ toast.add({ title: t("hr.recruitment.step_rejected"), color: "success" })
118
+ await loadData()
119
+ } catch {
120
+ toast.add({ title: t("hr.toast.error"), color: "error" })
121
+ }
122
+ }
123
+
124
+ onMounted(loadData)
125
+ </script>
126
+
127
+ <template>
128
+ <UDashboardPanel>
129
+ <template #header>
130
+ <UDashboardNavbar
131
+ :title="stepDef?.name ?? t('hr.recruitment.interview')"
132
+ :ui="{ title: 'text-lg font-semibold' }"
133
+ >
134
+ <template #leading>
135
+ <UButton
136
+ icon="i-ph-arrow-left-light"
137
+ variant="ghost"
138
+ size="sm"
139
+ @click="navigateTo(`/hr/talents/${personId}`)"
140
+ />
141
+ </template>
142
+ <template #right>
143
+ <UBadge
144
+ v-if="stepStatus !== 'pending'"
145
+ :color="({ approved: 'success', active: 'info', rejected: 'error' }[stepStatus] ?? 'neutral') as any"
146
+ variant="subtle"
147
+ size="md"
148
+ >
149
+ {{ stepStatus }}
150
+ </UBadge>
151
+ </template>
152
+ </UDashboardNavbar>
153
+ </template>
154
+
155
+ <template #body>
156
+ <div v-if="loading" class="flex items-center justify-center h-64">
157
+ <UIcon name="i-ph-spinner" class="animate-spin size-6 text-muted" />
158
+ </div>
159
+
160
+ <div v-else-if="!person || !stepDef" class="flex items-center justify-center h-64">
161
+ <div class="text-center text-muted">
162
+ <UIcon name="i-ph-warning-circle" class="size-10 mb-2 opacity-40" />
163
+ <p>{{ $t("hr.recruitment.interview_not_found") }}</p>
164
+ </div>
165
+ </div>
166
+
167
+ <div v-else class="max-w-3xl mx-auto py-6 space-y-6">
168
+ <!-- Candidate info card -->
169
+ <div class="border border-default rounded-xl p-4">
170
+ <div class="flex items-center gap-3 mb-3">
171
+ <UAvatar icon="i-ph-user-light" size="lg" class="bg-primary/10 text-primary" />
172
+ <div class="min-w-0 flex-1">
173
+ <h2 class="text-base font-semibold">{{ person.display_name }}</h2>
174
+ <div class="flex items-center gap-2 text-xs text-muted mt-0.5">
175
+ <span v-if="person.email">{{ person.email }}</span>
176
+ <span v-if="person.phone">{{ person.phone }}</span>
177
+ </div>
178
+ </div>
179
+ <HrSharedStageBadge :stage="person.stage" />
180
+ </div>
181
+ <div class="grid grid-cols-2 gap-3 text-sm">
182
+ <div>
183
+ <span class="text-muted text-xs">{{ $t("hr.fields.desired_position") }}</span>
184
+ <p class="font-medium">{{ person.desired_position ?? "—" }}</p>
185
+ </div>
186
+ <div>
187
+ <span class="text-muted text-xs">{{ $t("hr.fields.source") }}</span>
188
+ <p class="font-medium">{{ person.source ?? "—" }}</p>
189
+ </div>
190
+ <div v-if="application">
191
+ <span class="text-muted text-xs">{{ $t("hr.recruitment.position") }}</span>
192
+ <p class="font-medium">{{ application.position ?? "—" }}</p>
193
+ </div>
194
+ <div v-if="application">
195
+ <span class="text-muted text-xs">{{ $t("hr.recruitment.applied") }}</span>
196
+ <p class="font-medium">{{ application.date_created?.slice(0, 10) ?? "—" }}</p>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Interview form -->
202
+ <HrTalentsStepDetail
203
+ v-if="interview && stepDef"
204
+ :step="stepDef"
205
+ :interview="interview"
206
+ :step-status="stepStatus"
207
+ :can-approve="canApprove"
208
+ :disabled="!isActive"
209
+ @update="(_iid, patch) => updateInterview(patch)"
210
+ @approve="handleApprove"
211
+ @reject="showRejectModal = true; rejectComment = ''"
212
+ />
213
+
214
+ <!-- Timeline -->
215
+ <HrSharedWorkflowTimeline :actions="actions" />
216
+ </div>
217
+ </template>
218
+ </UDashboardPanel>
219
+
220
+ <UModal :open="showRejectModal" :title="$t('hr.recruitment.reject_step')" @close="showRejectModal = false">
221
+ <template #body>
222
+ <UTextarea v-model="rejectComment" :placeholder="$t('hr.recruitment.reject_reason_placeholder')" size="sm" :rows="3" autofocus />
223
+ </template>
224
+ <template #footer>
225
+ <div class="flex justify-end gap-2">
226
+ <UButton color="neutral" variant="outline" :label="$t('common.cancel')" @click="showRejectModal = false" />
227
+ <UButton color="error" :label="$t('hr.recruitment.reject_step')" :disabled="!rejectComment.trim()" @click="handleReject" />
228
+ </div>
229
+ </template>
230
+ </UModal>
231
+ </template>