@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,52 @@
1
+ <script lang="ts" setup>
2
+ const route = useRoute()
3
+ const api = useHrApi()
4
+
5
+ const { data: person, status } = await useAsyncData(
6
+ `hr-person-${route.params.id}`,
7
+ () => api.getPerson(String(route.params.id)),
8
+ )
9
+
10
+ const enabledTabs = ['profile', 'recruitment', 'activity']
11
+ </script>
12
+
13
+ <template>
14
+ <HrPersonDetail
15
+ v-if="person"
16
+ :person="person"
17
+ back-to="/hr/talents"
18
+ :enabled-tabs="enabledTabs"
19
+ />
20
+ <UDashboardPanel v-else-if="status === 'pending'">
21
+ <template #body>
22
+ <div class="flex items-center justify-center h-64">
23
+ <UIcon name="i-ph-spinner" class="animate-spin size-6 text-muted" />
24
+ </div>
25
+ </template>
26
+ </UDashboardPanel>
27
+ <UDashboardPanel v-else>
28
+ <template #header>
29
+ <UDashboardNavbar
30
+ title="Talent"
31
+ :ui="{ title: 'text-lg font-semibold' }"
32
+ >
33
+ <template #leading>
34
+ <UButton
35
+ icon="i-ph-arrow-left-light"
36
+ variant="ghost"
37
+ size="sm"
38
+ @click="navigateTo('/hr/talents')"
39
+ />
40
+ </template>
41
+ </UDashboardNavbar>
42
+ </template>
43
+ <template #body>
44
+ <div class="flex items-center justify-center h-64">
45
+ <div class="text-center text-muted">
46
+ <UIcon name="i-ph-warning-circle" class="size-10 mb-2 opacity-40" />
47
+ <p>{{ $t('hr.talents.not_found') }}</p>
48
+ </div>
49
+ </div>
50
+ </template>
51
+ </UDashboardPanel>
52
+ </template>
@@ -0,0 +1,224 @@
1
+ <script lang="ts" setup>
2
+ import HrPersonStageBadge from "../../../components/hr/shared/stage-badge.vue"
3
+
4
+ const { t } = useI18n()
5
+ const { talents, loading, fetchPeople } = usePeople()
6
+ const { can } = usePermissionRegistry()
7
+ const toast = useToast()
8
+
9
+ const canCreate = computed(() => can('hr', 'recruitment.write'))
10
+
11
+ const showAddModal = ref(false)
12
+
13
+ function onTalentCreated(person: any) {
14
+ fetchPeople()
15
+ navigateTo(`/hr/talents/${person.id}`)
16
+ }
17
+
18
+ await fetchPeople()
19
+
20
+ const TALENT_STAGES = ['talent', 'interviewing', 'offer'] as const
21
+
22
+ const stageFilter = ref<string>("all")
23
+ const search = ref("")
24
+
25
+ const filtered = computed(() => {
26
+ let list = talents.value
27
+ if (stageFilter.value !== "all") list = list.filter(p => p.stage === stageFilter.value)
28
+ if (search.value.trim()) {
29
+ const q = search.value.trim().toLowerCase()
30
+ list = list.filter(p =>
31
+ p.display_name.toLowerCase().includes(q)
32
+ || (p.email ?? "").toLowerCase().includes(q)
33
+ || (p.desired_position ?? "").toLowerCase().includes(q),
34
+ )
35
+ }
36
+ return list
37
+ })
38
+
39
+ const stageTabs = computed(() => [
40
+ { value: "all", label: t('common.all'), count: talents.value.length },
41
+ ...TALENT_STAGES.map(s => ({
42
+ value: s,
43
+ label: t(`hr.stages.${s}`),
44
+ count: talents.value.filter(p => p.stage === s).length,
45
+ })),
46
+ ])
47
+
48
+ const hasActiveFilter = computed(() => stageFilter.value !== "all" || !!search.value.trim())
49
+
50
+ function clearFilters() {
51
+ stageFilter.value = "all"
52
+ search.value = ""
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <ModulePage
58
+ :title="$t('hr.talents.title')"
59
+ icon="i-ph-user-circle-plus-light"
60
+ padding="none"
61
+ max-width="full"
62
+ >
63
+ <template #actions>
64
+ <UButton
65
+ v-if="canCreate"
66
+ icon="i-ph-plus"
67
+ :label="$t('hr.talents.actions.add')"
68
+ color="primary"
69
+ size="sm"
70
+ @click="showAddModal = true"
71
+ />
72
+ </template>
73
+
74
+ <div class="flex flex-col h-full">
75
+ <!-- Toolbar -->
76
+ <div class="flex items-center gap-3 py-3 border-b border-default">
77
+ <div class="flex items-center gap-1 bg-elevated rounded-lg p-0.5">
78
+ <button
79
+ v-for="tab in stageTabs"
80
+ :key="tab.value"
81
+ class="px-3 py-1.5 text-xs font-medium rounded-md transition-colors"
82
+ :class="stageFilter === tab.value
83
+ ? 'bg-default text-default shadow-sm'
84
+ : 'text-muted hover:text-default'"
85
+ @click="stageFilter = tab.value"
86
+ >
87
+ {{ tab.label }}
88
+ <UBadge
89
+ :label="String(tab.count)"
90
+ variant="subtle"
91
+ size="md"
92
+ class="ml-1.5"
93
+ />
94
+ </button>
95
+ </div>
96
+
97
+ <div class="flex-1" />
98
+
99
+ <UInput
100
+ v-model="search"
101
+ icon="i-ph-magnifying-glass"
102
+ :placeholder="$t('hr.talents.search_placeholder')"
103
+ size="sm"
104
+ class="w-64"
105
+ :ui="{ base: 'bg-elevated' }"
106
+ />
107
+ </div>
108
+
109
+ <!-- Table -->
110
+ <div class="flex-1 overflow-auto">
111
+ <table class="w-full text-sm">
112
+ <thead class="bg-elevated/50 sticky top-0 z-10">
113
+ <tr class="text-left text-xs text-muted uppercase tracking-wider">
114
+ <th class="px-4 py-2.5 font-medium w-[280px]">
115
+ Candidate
116
+ </th>
117
+ <th class="px-4 py-2.5 font-medium">
118
+ Source
119
+ </th>
120
+ <th class="px-4 py-2.5 font-medium">
121
+ Desired Position
122
+ </th>
123
+ <th class="px-4 py-2.5 font-medium w-[120px]">
124
+ Stage
125
+ </th>
126
+ </tr>
127
+ </thead>
128
+ <tbody class="divide-y divide-default">
129
+ <tr
130
+ v-if="loading"
131
+ >
132
+ <td
133
+ colspan="4"
134
+ class="px-4 py-16 text-center text-muted"
135
+ >
136
+ <UIcon
137
+ name="i-ph-spinner"
138
+ class="animate-spin size-6 mx-auto"
139
+ />
140
+ </td>
141
+ </tr>
142
+ <tr
143
+ v-for="p in filtered"
144
+ v-else
145
+ :key="p.id"
146
+ class="hover:bg-elevated/50 cursor-pointer transition-colors"
147
+ @click="navigateTo(`/hr/talents/${p.id}`)"
148
+ >
149
+ <td class="px-4 py-3">
150
+ <div class="flex items-center gap-3">
151
+ <UAvatar
152
+ icon="i-ph-user-light"
153
+ size="sm"
154
+ class="bg-primary/10 text-primary shrink-0"
155
+ />
156
+ <div class="min-w-0">
157
+ <div class="font-medium text-default truncate">
158
+ {{ p.display_name }}
159
+ </div>
160
+ <div class="text-xs text-muted truncate">
161
+ {{ p.email }}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </td>
166
+ <td class="px-4 py-3 text-muted">
167
+ {{ p.source ?? '—' }}
168
+ </td>
169
+ <td class="px-4 py-3 text-muted">
170
+ {{ p.desired_position ?? '—' }}
171
+ </td>
172
+ <td class="px-4 py-3">
173
+ <HrSharedStageBadge :stage="p.stage" />
174
+ </td>
175
+ </tr>
176
+
177
+ <tr v-if="!loading && filtered.length === 0">
178
+ <td
179
+ colspan="4"
180
+ class="px-4 py-16 text-center text-muted"
181
+ >
182
+ <UIcon
183
+ :name="hasActiveFilter ? 'i-ph-funnel-x-light' : 'i-ph-user-circle-plus-light'"
184
+ class="size-10 mx-auto mb-3 opacity-40"
185
+ />
186
+ <p class="text-sm font-medium text-default mb-1">
187
+ {{ hasActiveFilter ? $t('hr.talents.no_match') : $t('hr.talents.empty') }}
188
+ </p>
189
+ <p class="text-xs mb-4">
190
+ {{ hasActiveFilter ? $t('hr.talents.clear_hint') : $t('hr.talents.empty_hint') }}
191
+ </p>
192
+ <div class="flex items-center justify-center gap-2">
193
+ <UButton
194
+ v-if="hasActiveFilter"
195
+ :label="$t('hr.talents.clear_filters')"
196
+ icon="i-ph-arrow-counter-clockwise-light"
197
+ variant="outline"
198
+ size="sm"
199
+ @click="clearFilters"
200
+ />
201
+ <UButton
202
+ v-if="canCreate"
203
+ :label="$t('hr.talents.actions.add')"
204
+ icon="i-ph-plus"
205
+ color="primary"
206
+ size="sm"
207
+ @click="showAddModal = true"
208
+ />
209
+ </div>
210
+ </td>
211
+ </tr>
212
+ </tbody>
213
+ </table>
214
+ </div>
215
+
216
+ <!-- Footer -->
217
+ <div class="flex items-center justify-between px-4 py-2.5 border-t border-default text-xs text-muted">
218
+ <span>{{ filtered.length }} {{ filtered.length === 1 ? 'candidate' : 'candidates' }}</span>
219
+ </div>
220
+ </div>
221
+ </ModulePage>
222
+
223
+ <HrPersonFormModal v-model:open="showAddModal" @saved="onTalentCreated" />
224
+ </template>
@@ -0,0 +1,129 @@
1
+ <script lang="ts" setup>
2
+ const { t } = useI18n()
3
+ const route = useRoute()
4
+ const { can } = usePermissionRegistry()
5
+ const canAdmin = computed(() => can('hr', 'admin'))
6
+
7
+ const isSettings = computed(() => route.path.startsWith("/hr/settings"))
8
+ const settingsTab = computed(() => (route.query.tab as string) || "general")
9
+
10
+ const peopleItems = computed(() => [
11
+ {
12
+ label: t('hr.sidebar.talents'),
13
+ icon: "i-ph-user-circle-plus-light",
14
+ to: "/hr/talents",
15
+ active: route.path.startsWith("/hr/talents"),
16
+ },
17
+ {
18
+ label: t('hr.sidebar.employees'),
19
+ icon: "i-ph-users-light",
20
+ to: "/hr/employees",
21
+ active: route.path.startsWith("/hr/employees"),
22
+ },
23
+ ])
24
+
25
+ const orgItems = computed(() => [
26
+ {
27
+ label: t('hr.sidebar.departments'),
28
+ icon: "i-ph-tree-structure-light",
29
+ to: "/hr/departments",
30
+ active: route.path.startsWith("/hr/departments"),
31
+ },
32
+ {
33
+ label: t('hr.sidebar.leave'),
34
+ icon: "i-ph-calendar-check-light",
35
+ to: "/hr/leave",
36
+ active: route.path.startsWith("/hr/leave"),
37
+ },
38
+ ])
39
+
40
+ const settingsItems = computed(() => [
41
+ { label: t('hr.settings.general'), icon: "i-ph-sliders-light", to: { path: "/hr/settings", query: { tab: "general" } }, active: isSettings.value && settingsTab.value === "general" },
42
+ { label: t('hr.settings.taxonomies'), icon: "i-ph-tag-light", to: { path: "/hr/settings", query: { tab: "taxonomies" } }, active: isSettings.value && settingsTab.value === "taxonomies" },
43
+ { label: t('hr.settings.policies'), icon: "i-ph-shield-check-light", to: { path: "/hr/settings", query: { tab: "policies" } }, active: isSettings.value && settingsTab.value === "policies" },
44
+ { label: t('hr.settings.holidays'), icon: "i-ph-calendar-light", to: { path: "/hr/settings", query: { tab: "holidays" } }, active: isSettings.value && settingsTab.value === "holidays" },
45
+ { label: t('hr.settings.recruitment.title'), icon: "i-ph-clipboard-text-light", to: { path: "/hr/settings", query: { tab: "recruitment" } }, active: isSettings.value && settingsTab.value === "recruitment" },
46
+ { label: t('hr.settings.documents'), icon: "i-ph-file-doc-light", to: { path: "/hr/settings", query: { tab: "documents" } }, active: isSettings.value && settingsTab.value === "documents" },
47
+ { label: t('hr.settings.automation'), icon: "i-ph-lightning-light", to: { path: "/hr/settings", query: { tab: "automation" } }, active: isSettings.value && settingsTab.value === "automation" },
48
+ { label: t('hr.settings.integrations'), icon: "i-ph-plugs-connected-light", to: { path: "/hr/settings", query: { tab: "integrations" } }, active: isSettings.value && settingsTab.value === "integrations" },
49
+ ])
50
+ </script>
51
+
52
+ <template>
53
+ <UDashboardSidebar
54
+ id="hr-sidebar"
55
+ :min-size="14"
56
+ :default-size="14"
57
+ :max-size="20"
58
+ collapsible
59
+ :ui="{
60
+ header: 'border-b border-default',
61
+ footer: 'border-t border-default pl-2 min-h-(--ui-footer-height)'
62
+ }"
63
+ >
64
+ <template #header>
65
+ <div class="flex items-center gap-1.5 font-semibold text-highlighted truncate w-full">
66
+ <UIcon
67
+ name="i-ph-user-list-light"
68
+ class="size-4 shrink-0"
69
+ />
70
+ {{ $t('hr.title') }}
71
+ </div>
72
+ </template>
73
+
74
+ <template #default>
75
+ <div
76
+ v-if="isSettings"
77
+ class="p-2"
78
+ >
79
+ <UNavigationMenu
80
+ :items="settingsItems"
81
+ orientation="vertical"
82
+ variant="pill"
83
+ />
84
+ </div>
85
+
86
+ <div
87
+ v-else
88
+ class="space-y-4 p-2"
89
+ >
90
+ <div>
91
+ <div class="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted">
92
+ {{ $t('hr.sidebar.group_people') }}
93
+ </div>
94
+ <UNavigationMenu
95
+ :items="peopleItems"
96
+ orientation="vertical"
97
+ variant="pill"
98
+ />
99
+ </div>
100
+ <div>
101
+ <div class="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted">
102
+ {{ $t('hr.sidebar.group_organization') }}
103
+ </div>
104
+ <UNavigationMenu
105
+ :items="orgItems"
106
+ orientation="vertical"
107
+ variant="pill"
108
+ />
109
+ </div>
110
+ </div>
111
+ </template>
112
+
113
+ <template #footer>
114
+ <div class="flex items-center h-full">
115
+ <UButton
116
+ v-if="canAdmin || isSettings"
117
+ :label="isSettings ? $t('hr.sidebar.back_to_hr') : $t('hr.sidebar.settings')"
118
+ :to="isSettings ? '/hr/employees' : '/hr/settings'"
119
+ variant="link"
120
+ color="neutral"
121
+ :icon="isSettings ? 'i-ph-arrow-left-light' : 'i-ph-gear-light'"
122
+ size="sm"
123
+ />
124
+ </div>
125
+ </template>
126
+ </UDashboardSidebar>
127
+
128
+ <NuxtPage />
129
+ </template>
@@ -0,0 +1,3 @@
1
+ // TODO: Enable when backend POST /hr/contacts/sync endpoint is implemented
2
+ // Currently disabled to avoid 404 on every page load.
3
+ export default defineNuxtPlugin(() => {})
@@ -0,0 +1,36 @@
1
+ export default defineNuxtPlugin(() => {
2
+ const extensionStore = useExtensionStore()
3
+ const { register: registerType } = useContactTypeRegistry()
4
+
5
+ // Register HR-owned contact types
6
+ registerType({
7
+ key: 'employee',
8
+ label: 'Employee',
9
+ icon: 'i-ph-identification-badge-light',
10
+ color: 'neutral',
11
+ owner_module: 'hr',
12
+ system: true,
13
+ })
14
+
15
+ registerType({
16
+ key: 'talent',
17
+ label: 'Talent',
18
+ icon: 'i-ph-sparkle-light',
19
+ color: 'info',
20
+ owner_module: 'hr',
21
+ system: true,
22
+ })
23
+
24
+ extensionStore.register('contacts.detail.tabs', {
25
+ id: 'hr',
26
+ label: 'HR',
27
+ icon: 'i-ph-identification-badge-light',
28
+ component: resolveComponent('HrHrContactTab'),
29
+ sort: 20,
30
+ visible: (ctx: any) => Boolean(ctx.contact?.hr_person_id),
31
+ })
32
+
33
+ // Sync of contact.types is handled by:
34
+ // - backend hook hr_stage_transitions.items.create — fires syncContactTypes()
35
+ // - hr-contacts-sync.client.ts — one-shot backfill on first load
36
+ })
@@ -0,0 +1,5 @@
1
+ export default defineNuxtPlugin(() => {
2
+ const { registerSchema, extendSchema } = useSetupWizard("hr")
3
+ registerSchema(hrSchema)
4
+ extendSchema({ steps: [createPermissionStep(hrPermissions), createHrPolicySeedStep()] })
5
+ })
@@ -0,0 +1,22 @@
1
+ export default defineNuxtPlugin({
2
+ name: 'hr-navigation',
3
+ dependsOn: ['core-auth'],
4
+ setup() {
5
+ defineLayerPermissions(hrPermissions)
6
+
7
+ const { hasModuleAccess } = usePermissionRegistry()
8
+ const { registerNavItem, unregisterNavItem } = useNavigation()
9
+ const { loggedIn } = useCurrentUser()
10
+ const { isReady, checkStatus } = useSetupWizard("hr")
11
+
12
+ if (loggedIn.value) checkStatus()
13
+
14
+ watch([isReady, () => hasModuleAccess('hr')], ([ready, hasAccess]) => {
15
+ if (ready || hasAccess) {
16
+ registerNavItem(hrPermissions.navMapping!)
17
+ } else {
18
+ unregisterNavItem("hr")
19
+ }
20
+ }, { immediate: true })
21
+ }
22
+ })
@@ -0,0 +1,27 @@
1
+ export const hrPermissions: LayerPermissionConfig = {
2
+ module: 'hr',
3
+ displayName: 'HR',
4
+ icon: 'i-ph-user-list-light',
5
+ description: 'Human Resources — people, leave, documents, integrations',
6
+ routePrefix: '/hr',
7
+ actions: [
8
+ { key: 'read', displayName: 'View HR Data', riskLevel: 'low' },
9
+ { key: 'create', displayName: 'Create Records', riskLevel: 'low' },
10
+ { key: 'update', displayName: 'Edit Records', riskLevel: 'medium' },
11
+ { key: 'delete', displayName: 'Delete Records', riskLevel: 'medium' },
12
+ { key: 'admin', displayName: 'Manage Settings', description: 'Configure HR module, policies, templates', riskLevel: 'high' },
13
+ { key: 'leave.approve', displayName: 'Approve Leave', description: 'Approve or reject leave requests', riskLevel: 'medium' },
14
+ { key: 'sensitive.read', displayName: 'View Sensitive Data', description: 'View compensation, insurance and personal documents', riskLevel: 'high' },
15
+ { key: 'sensitive.update', displayName: 'Edit Sensitive Data', description: 'Edit compensation, insurance and personal documents', riskLevel: 'high' },
16
+ { key: 'recruitment.read', displayName: 'View Recruitment', description: 'View talents, applications and interviews', riskLevel: 'low' },
17
+ { key: 'recruitment.write', displayName: 'Manage Recruitment', description: 'Create/edit talents, applications, interviews and move recruitment stages', riskLevel: 'medium' },
18
+ { key: 'leave.approve.dept', displayName: 'Approve Leave (own department)', description: 'Approve leave only for members of departments the user heads', riskLevel: 'medium' },
19
+ ],
20
+ navMapping: {
21
+ id: 'hr',
22
+ label: 'HR',
23
+ icon: 'i-ph-user-list-light',
24
+ to: '/hr',
25
+ order: 6,
26
+ },
27
+ }
@@ -0,0 +1,110 @@
1
+ import { hrPermissions } from './hr-permissions'
2
+
3
+ interface HandlerStepDefinition {
4
+ key: string
5
+ title: string
6
+ icon?: string
7
+ after?: string
8
+ handler: (ctx: any) => Promise<any[]>
9
+ }
10
+
11
+ interface RolePolicy {
12
+ name: string
13
+ icon: string
14
+ description: string
15
+ actions: string[]
16
+ }
17
+
18
+ // Phase-1 role → HR action grants (all collection_scope '__global__').
19
+ // Seeds the policies + app-permission grants only; attaching a policy to an
20
+ // actual role/user is left to an admin via the Permissions UI (env-specific).
21
+ const HR_ROLE_POLICIES: RolePolicy[] = [
22
+ {
23
+ name: 'HR Admin',
24
+ icon: 'i-ph-user-gear-light',
25
+ description: 'Full HR access including settings, policies, providers and templates',
26
+ actions: ['read', 'create', 'update', 'delete', 'admin', 'leave.approve', 'sensitive.read', 'sensitive.update', 'recruitment.read', 'recruitment.write'],
27
+ },
28
+ {
29
+ name: 'HR Manager',
30
+ icon: 'i-ph-users-three-light',
31
+ description: 'People lifecycle, recruitment, leave approval and sensitive data — no module settings',
32
+ actions: ['read', 'create', 'update', 'delete', 'leave.approve', 'sensitive.read', 'sensitive.update', 'recruitment.read', 'recruitment.write'],
33
+ },
34
+ {
35
+ name: 'HR Recruiter',
36
+ icon: 'i-ph-user-plus-light',
37
+ description: 'Recruitment pipeline only (talents, applications, interviews) — cannot touch employee records or sensitive data',
38
+ actions: ['read', 'recruitment.read', 'recruitment.write'],
39
+ },
40
+ {
41
+ name: 'HR Department Manager',
42
+ icon: 'i-ph-users-light',
43
+ description: 'View members and approve leave only for departments the user heads',
44
+ actions: ['read', 'leave.approve.dept'],
45
+ },
46
+ {
47
+ name: 'HR Viewer',
48
+ icon: 'i-ph-eye-light',
49
+ description: 'Read-only HR access — excludes compensation, insurance and documents',
50
+ actions: ['read'],
51
+ },
52
+ ]
53
+
54
+ export function createHrPolicySeedStep(options?: { after?: string }): HandlerStepDefinition {
55
+ return {
56
+ key: 'hr-policy-seed',
57
+ title: 'Seed HR Role Policies',
58
+ icon: 'i-ph-shield-check-light',
59
+ after: options?.after ?? 'permissions',
60
+ handler: async (ctx: any): Promise<any[]> => {
61
+ const results: any[] = []
62
+ const module = hrPermissions.module
63
+
64
+ // Load once for idempotency.
65
+ const existingPolicies: any[] = (await ctx.api('/policies').catch(() => [])) ?? []
66
+ const existingGrants: any[] = (await ctx.api('/app-permissions').catch(() => [])) ?? []
67
+
68
+ for (const rp of HR_ROLE_POLICIES) {
69
+ try {
70
+ let policy = existingPolicies.find((p: any) => p.name === rp.name)
71
+ if (!policy) {
72
+ policy = await ctx.api('/policies', {
73
+ method: 'POST',
74
+ body: {
75
+ name: rp.name,
76
+ icon: rp.icon,
77
+ description: rp.description,
78
+ app_access: true,
79
+ admin_access: false,
80
+ },
81
+ })
82
+ results.push({ type: 'seed', name: `policy:${rp.name}`, status: 'created' })
83
+ }
84
+ else {
85
+ results.push({ type: 'seed', name: `policy:${rp.name}`, status: 'skipped' })
86
+ }
87
+
88
+ const policyId = policy?.id ?? policy?.data?.id
89
+ if (!policyId) continue
90
+
91
+ for (const action of rp.actions) {
92
+ const exists = existingGrants.some(
93
+ (g: any) => g.policy === policyId && g.module === module && g.action === action,
94
+ )
95
+ if (exists) continue
96
+ await ctx.api('/app-permissions', {
97
+ method: 'POST',
98
+ body: { policy: policyId, module, action, collection_scope: '__global__' },
99
+ })
100
+ }
101
+ }
102
+ catch (err: any) {
103
+ results.push({ type: 'seed', name: `policy:${rp.name}`, status: 'error', error: err?.message ?? 'Unexpected error' })
104
+ }
105
+ }
106
+
107
+ return results
108
+ },
109
+ }
110
+ }