@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,210 @@
1
+ export function useHrApi() {
2
+ const { $api } = useNuxtApp()
3
+
4
+ function unwrap<T>(res: T | { data: T }): T {
5
+ return (res && typeof res === 'object' && 'data' in res) ? (res as any).data : res
6
+ }
7
+
8
+ async function getSettings() {
9
+ return unwrap(await $api<HrSettings | { data: HrSettings }>('/hr/settings'))
10
+ }
11
+
12
+ async function patchSettings(body: Partial<HrSettings>) {
13
+ return unwrap(await $api<HrSettings | { data: HrSettings }>('/hr/settings', { method: 'PATCH', body }))
14
+ }
15
+
16
+ async function getFields(collection: string) {
17
+ return unwrap(await $api<any[] | { data: any[] }>(`/hr/fields/${collection}`))
18
+ }
19
+
20
+ // People CRUD
21
+
22
+ async function listPeople(query?: Record<string, string>) {
23
+ const res = await $api<any>('/hr/people', { query })
24
+ return { data: unwrap(res.data ?? res), meta: res.meta ?? null }
25
+ }
26
+
27
+ async function getPerson(id: string) {
28
+ return unwrap(await $api<Person | { data: Person }>(`/hr/people/${id}`))
29
+ }
30
+
31
+ async function createPerson(body: Partial<Person>) {
32
+ return unwrap(await $api<Person | { data: Person }>('/hr/people', { method: 'POST', body }))
33
+ }
34
+
35
+ async function updatePerson(id: string, body: Partial<Person>) {
36
+ return unwrap(await $api<Person | { data: Person }>(`/hr/people/${id}`, { method: 'PATCH', body }))
37
+ }
38
+
39
+ async function deletePerson(id: string) {
40
+ await $api(`/hr/people/${id}`, { method: 'DELETE' })
41
+ }
42
+
43
+ // Notes
44
+
45
+ async function listNotes(personId: string) {
46
+ return unwrap(await $api<HrNote[] | { data: HrNote[] }>(`/hr/people/${personId}/notes`))
47
+ }
48
+
49
+ async function createNote(personId: string, body: string) {
50
+ return unwrap(await $api<HrNote | { data: HrNote }>(`/hr/people/${personId}/notes`, {
51
+ method: 'POST',
52
+ body: { body },
53
+ }))
54
+ }
55
+
56
+ async function deleteNote(personId: string, noteId: string) {
57
+ await $api(`/hr/people/${personId}/notes/${noteId}`, { method: 'DELETE' })
58
+ }
59
+
60
+ // Leave
61
+
62
+ async function listLeaveTypes() {
63
+ return unwrap(await $api<any[] | { data: any[] }>(`/hr/settings/leave-types`))
64
+ }
65
+
66
+ async function createLeaveRequest(personId: string, body: any) {
67
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/leave-requests`, {
68
+ method: 'POST',
69
+ body,
70
+ }))
71
+ }
72
+
73
+ // Contract
74
+
75
+ async function getContract(personId: string) {
76
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/contracts`))
77
+ }
78
+
79
+ async function updateContract(personId: string, body: any) {
80
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/contracts`, {
81
+ method: 'PATCH',
82
+ body,
83
+ }))
84
+ }
85
+
86
+ // Compensation
87
+
88
+ async function getCompensation(personId: string) {
89
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/compensations`))
90
+ }
91
+
92
+ async function updateCompensation(personId: string, body: any) {
93
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/compensations`, {
94
+ method: 'PATCH',
95
+ body,
96
+ }))
97
+ }
98
+
99
+ // Insurance
100
+
101
+ async function getInsurance(personId: string) {
102
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/insurances`))
103
+ }
104
+
105
+ async function updateInsurance(personId: string, body: any) {
106
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/insurances`, {
107
+ method: 'PATCH',
108
+ body,
109
+ }))
110
+ }
111
+
112
+ // Assets
113
+
114
+ async function listAssets(personId: string) {
115
+ return unwrap(await $api<any[] | { data: any[] }>(`/hr/people/${personId}/assets`))
116
+ }
117
+
118
+ async function createAsset(personId: string, body: any) {
119
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/assets`, {
120
+ method: 'POST',
121
+ body,
122
+ }))
123
+ }
124
+
125
+ async function updateAsset(personId: string, assetId: string | number, body: any) {
126
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/assets/${assetId}`, {
127
+ method: 'PATCH',
128
+ body,
129
+ }))
130
+ }
131
+
132
+ async function deleteAsset(personId: string, assetId: string | number) {
133
+ await $api(`/hr/people/${personId}/assets/${assetId}`, { method: 'DELETE' })
134
+ }
135
+
136
+ // Departments
137
+
138
+ async function listDepartments() {
139
+ return unwrap(await $api<any[] | { data: any[] }>('/hr/departments'))
140
+ }
141
+
142
+ async function createDepartment(body: any) {
143
+ return unwrap(await $api<any | { data: any }>('/hr/departments', {
144
+ method: 'POST',
145
+ body,
146
+ }))
147
+ }
148
+
149
+ async function updateDepartment(id: number, body: any) {
150
+ return unwrap(await $api<any | { data: any }>(`/hr/departments/${id}`, {
151
+ method: 'PATCH',
152
+ body,
153
+ }))
154
+ }
155
+
156
+ async function deleteDepartment(id: number) {
157
+ await $api(`/hr/departments/${id}`, { method: 'DELETE' })
158
+ }
159
+
160
+ // Documents
161
+
162
+ async function listDocuments(personId: string) {
163
+ return unwrap(await $api<any[] | { data: any[] }>(`/hr/people/${personId}/documents`))
164
+ }
165
+
166
+ async function generateDocument(personId: string, templateId: number | string) {
167
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/documents`, {
168
+ method: 'POST',
169
+ body: { type: 'generate', template_id: templateId },
170
+ }))
171
+ }
172
+
173
+ async function deleteDocument(personId: string, docId: number | string) {
174
+ await $api(`/hr/people/${personId}/documents/${docId}`, { method: 'DELETE' })
175
+ }
176
+
177
+ // Provisioning
178
+
179
+ async function getProvisioningStatus(personId: string) {
180
+ return unwrap(await $api<ProvisioningStatus[] | { data: ProvisioningStatus[] }>(`/hr/people/${personId}/provisioning`))
181
+ }
182
+
183
+ async function triggerProvisioning(personId: string, action: 'create' | 'disable' | 'enable') {
184
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/provisioning`, {
185
+ method: 'POST',
186
+ body: { action },
187
+ }))
188
+ }
189
+
190
+ async function retryProvisioning(personId: string, logId: string) {
191
+ return unwrap(await $api<any | { data: any }>(`/hr/people/${personId}/provisioning/${logId}/retry`, {
192
+ method: 'POST',
193
+ }))
194
+ }
195
+
196
+ return {
197
+ unwrap,
198
+ getSettings, patchSettings, getFields,
199
+ listPeople, getPerson, createPerson, updatePerson, deletePerson,
200
+ listNotes, createNote, deleteNote,
201
+ listLeaveTypes, createLeaveRequest,
202
+ getContract, updateContract,
203
+ getCompensation, updateCompensation,
204
+ getInsurance, updateInsurance,
205
+ listAssets, createAsset, updateAsset, deleteAsset,
206
+ listDepartments, createDepartment, updateDepartment, deleteDepartment,
207
+ listDocuments, generateDocument, deleteDocument,
208
+ getProvisioningStatus, triggerProvisioning, retryProvisioning,
209
+ }
210
+ }
@@ -0,0 +1,76 @@
1
+ const INTERFACE_NORMALIZE: Record<string, string> = {
2
+ 'select-dropdown': 'select',
3
+ }
4
+
5
+ // FormRoot renders fields in a 24-col @container grid. Field.vue hard-codes
6
+ // `col-span-full` and only honors `meta.options.ui.base`, so the DB `width`
7
+ // (half/full) is otherwise ignored. Map it to a responsive col-span here so
8
+ // half-width fields sit two-per-row on wide containers and stack on narrow.
9
+ const WIDTH_COLSPAN: Record<string, string> = {
10
+ 'half': '@xl:col-span-12',
11
+ 'half-left': '@xl:col-span-12',
12
+ 'half-right': '@xl:col-span-12',
13
+ }
14
+
15
+ function normalizeField(field: Field): Field {
16
+ if (!field.meta) return field
17
+ let meta = field.meta
18
+ const iface = meta.interface
19
+ if (iface && INTERFACE_NORMALIZE[iface]) {
20
+ meta = { ...meta, interface: INTERFACE_NORMALIZE[iface] }
21
+ }
22
+ const colspan = WIDTH_COLSPAN[meta.width ?? 'full']
23
+ if (colspan && !meta.options?.ui?.base) {
24
+ meta = {
25
+ ...meta,
26
+ options: {
27
+ ...(meta.options ?? {}),
28
+ ui: { ...(meta.options?.ui ?? {}), base: colspan },
29
+ },
30
+ }
31
+ }
32
+ return meta === field.meta ? field : { ...field, meta }
33
+ }
34
+
35
+ export function useHrFieldRegistry(collections: string[]) {
36
+ const api = useHrApi()
37
+ const registry = ref<Record<string, Field[]>>({})
38
+ const loading = ref(false)
39
+
40
+ async function fetchAll() {
41
+ loading.value = true
42
+ try {
43
+ const results = await Promise.all(
44
+ collections.map(c => api.getFields(c).then(fields => ({ c, fields })))
45
+ )
46
+ results.forEach(({ c, fields }) => {
47
+ registry.value[c] = (fields ?? []).map(normalizeField)
48
+ })
49
+ } finally {
50
+ loading.value = false
51
+ }
52
+ }
53
+
54
+ function getField(collection: string, key: string): Field | null {
55
+ return registry.value[collection]?.find(f => f.field === key) ?? null
56
+ }
57
+
58
+ function pickFields(collection: string, names: string[]): Field[] {
59
+ const result: Field[] = []
60
+ let sortIndex = 0
61
+ for (const name of names) {
62
+ const f = getField(collection, name)
63
+ if (f) {
64
+ result.push({ ...f, meta: { ...f.meta!, sort: sortIndex } })
65
+ sortIndex++
66
+ }
67
+ }
68
+ return result
69
+ }
70
+
71
+ function isReady(collection: string): boolean {
72
+ return collection in registry.value
73
+ }
74
+
75
+ return { registry, loading, fetchAll, getField, pickFields, isReady }
76
+ }
@@ -0,0 +1,66 @@
1
+ import type { Policy, PolicyKey, PolicyParams } from "../../shared/types"
2
+
3
+ export function useHrPolicies() {
4
+ const policies = useState<Policy[]>('hr:policies', () => [])
5
+ const isPending = ref(false)
6
+
7
+ async function fetchPolicies(): Promise<void> {
8
+ isPending.value = true
9
+ try {
10
+ const { useHrApi } = await import('./use-hr-api')
11
+ const api = useHrApi()
12
+ const res = await $fetch<{ data: Policy[] } | Policy[]>('/api/hr/policies')
13
+ policies.value = api.unwrap(res) as Policy[]
14
+ } finally {
15
+ isPending.value = false
16
+ }
17
+ }
18
+
19
+ function getPolicy(key: PolicyKey): Policy | undefined {
20
+ return policies.value.find(p => p.key === key)
21
+ }
22
+
23
+ async function togglePolicy(key: PolicyKey, enabled: boolean): Promise<void> {
24
+ const policy = getPolicy(key)
25
+ if (!policy) return
26
+ isPending.value = true
27
+ try {
28
+ const res = await $fetch<{ data: Policy } | Policy>(`/api/hr/policies/${policy.id}`, {
29
+ method: 'PATCH',
30
+ body: { enabled },
31
+ })
32
+ const idx = policies.value.findIndex(p => p.key === key)
33
+ if (idx >= 0) {
34
+ const { useHrApi } = await import('./use-hr-api')
35
+ const api = useHrApi()
36
+ policies.value[idx] = api.unwrap(res) as Policy
37
+ }
38
+ } finally {
39
+ isPending.value = false
40
+ }
41
+ }
42
+
43
+ async function updatePolicyParams(key: PolicyKey, params: PolicyParams): Promise<void> {
44
+ const policy = getPolicy(key)
45
+ if (!policy) return
46
+ isPending.value = true
47
+ try {
48
+ const res = await $fetch<{ data: Policy } | Policy>(`/api/hr/policies/${policy.id}`, {
49
+ method: 'PATCH',
50
+ body: { params },
51
+ })
52
+ const idx = policies.value.findIndex(p => p.key === key)
53
+ if (idx >= 0) {
54
+ const { useHrApi } = await import('./use-hr-api')
55
+ const api = useHrApi()
56
+ policies.value[idx] = api.unwrap(res) as Policy
57
+ }
58
+ } finally {
59
+ isPending.value = false
60
+ }
61
+ }
62
+
63
+ onMounted(fetchPolicies)
64
+
65
+ return { policies, getPolicy, togglePolicy, updatePolicyParams, isPending }
66
+ }
@@ -0,0 +1,118 @@
1
+ import type { LifecycleStage } from "../../shared/types/person"
2
+ import type { LeaveType, EmploymentType, HrGeneralSettings } from "../../shared/types/settings"
3
+ import { LIFECYCLE_STAGES } from "../../shared/types/person"
4
+
5
+ export function useHrSettings() {
6
+ const leaveTypes = useState<LeaveType[]>('hr:leave-types', () => [])
7
+ const employmentTypes = useState<EmploymentType[]>('hr:employment-types', () => [])
8
+ const general = useState<HrGeneralSettings>('hr:general-settings', () => ({
9
+ company_name: '',
10
+ fiscal_year_start_month: 1,
11
+ default_currency: 'VND',
12
+ }))
13
+ const lifecycleLabels = useState<Record<LifecycleStage, string>>(
14
+ 'hr:lifecycle-labels',
15
+ () => Object.fromEntries(LIFECYCLE_STAGES.map(s => [s, s])) as Record<LifecycleStage, string>,
16
+ )
17
+ const isPending = ref(false)
18
+
19
+ function unwrap<T>(res: T | { data: T }): T {
20
+ return (res && typeof res === 'object' && 'data' in res) ? (res as any).data : res
21
+ }
22
+
23
+ async function fetchSettings(): Promise<void> {
24
+ const res = await $fetch<HrGeneralSettings | { data: HrGeneralSettings }>('/api/hr/settings')
25
+ const data = unwrap(res)
26
+ if (data) Object.assign(general.value, data)
27
+ }
28
+
29
+ async function save(payload: Partial<HrGeneralSettings>): Promise<void> {
30
+ isPending.value = true
31
+ try {
32
+ const res = await $fetch<HrGeneralSettings | { data: HrGeneralSettings }>('/api/hr/settings', {
33
+ method: 'PATCH',
34
+ body: payload,
35
+ })
36
+ const data = unwrap(res)
37
+ if (data) Object.assign(general.value, data)
38
+ } finally {
39
+ isPending.value = false
40
+ }
41
+ }
42
+
43
+ async function fetchLeaveTypes(): Promise<void> {
44
+ const res = await $fetch<LeaveType[] | { data: LeaveType[] }>('/api/hr/settings/leave-types')
45
+ leaveTypes.value = unwrap(res) as LeaveType[]
46
+ }
47
+
48
+ async function addLeaveType(input: Omit<LeaveType, 'id' | 'is_archived'>): Promise<void> {
49
+ const res = await $fetch<LeaveType | { data: LeaveType }>('/api/hr/settings/leave-types', {
50
+ method: 'POST',
51
+ body: input,
52
+ })
53
+ leaveTypes.value.push(unwrap(res) as LeaveType)
54
+ }
55
+
56
+ async function updateLeaveType(id: string | number, patch: Partial<LeaveType>): Promise<void> {
57
+ const res = await $fetch<LeaveType | { data: LeaveType }>(`/api/hr/settings/leave-types/${id}`, {
58
+ method: 'PATCH',
59
+ body: patch,
60
+ })
61
+ const updated = unwrap(res) as LeaveType
62
+ const idx = leaveTypes.value.findIndex(t => t.id === id)
63
+ if (idx >= 0) leaveTypes.value[idx] = updated
64
+ }
65
+
66
+ async function archiveLeaveType(id: string | number): Promise<void> {
67
+ await updateLeaveType(id, { status: 'archived' } as any)
68
+ }
69
+
70
+ async function fetchEmploymentTypes(): Promise<void> {
71
+ const res = await $fetch<EmploymentType[] | { data: EmploymentType[] }>('/api/hr/settings/employment-types')
72
+ employmentTypes.value = unwrap(res) as EmploymentType[]
73
+ }
74
+
75
+ async function addEmploymentType(input: Omit<EmploymentType, 'id' | 'is_archived'>): Promise<void> {
76
+ const res = await $fetch<EmploymentType | { data: EmploymentType }>('/api/hr/settings/employment-types', {
77
+ method: 'POST',
78
+ body: input,
79
+ })
80
+ employmentTypes.value.push(unwrap(res) as EmploymentType)
81
+ }
82
+
83
+ async function updateEmploymentType(id: string | number, patch: Partial<EmploymentType>): Promise<void> {
84
+ const res = await $fetch<EmploymentType | { data: EmploymentType }>(`/api/hr/settings/employment-types/${id}`, {
85
+ method: 'PATCH',
86
+ body: patch,
87
+ })
88
+ const updated = unwrap(res) as EmploymentType
89
+ const idx = employmentTypes.value.findIndex(t => t.id === id)
90
+ if (idx >= 0) employmentTypes.value[idx] = updated
91
+ }
92
+
93
+ async function archiveEmploymentType(id: string | number): Promise<void> {
94
+ await $fetch(`/api/hr/settings/employment-types/${id}`, { method: 'DELETE' })
95
+ const idx = employmentTypes.value.findIndex(t => t.id === id)
96
+ if (idx >= 0) employmentTypes.value[idx] = { ...employmentTypes.value[idx]!, is_archived: true }
97
+ }
98
+
99
+ function setStageLabel(stage: LifecycleStage, label: string): void {
100
+ lifecycleLabels.value[stage] = label
101
+ }
102
+
103
+ onMounted(async () => {
104
+ await Promise.all([fetchSettings(), fetchLeaveTypes(), fetchEmploymentTypes()])
105
+ })
106
+
107
+ return {
108
+ leaveTypes,
109
+ employmentTypes,
110
+ general,
111
+ lifecycleLabels,
112
+ isPending,
113
+ save,
114
+ addLeaveType, updateLeaveType, archiveLeaveType,
115
+ addEmploymentType, updateEmploymentType, archiveEmploymentType,
116
+ setStageLabel,
117
+ }
118
+ }
@@ -0,0 +1,71 @@
1
+ export function useLeave() {
2
+ const leaveRequests = useState<LeaveRequest[]>('leave.requests', () => [])
3
+ const loading = useState('leave.loading', () => false)
4
+
5
+ const pendingLeave = computed(() => leaveRequests.value.filter(r => r.status?.toLowerCase() === 'pending'))
6
+ const approvedLeave = computed(() => leaveRequests.value.filter(r => r.status?.toLowerCase() === 'approved'))
7
+ const rejectedLeave = computed(() => leaveRequests.value.filter(r => r.status?.toLowerCase() === 'rejected'))
8
+ const totalDaysUsed = computed(() => approvedLeave.value.reduce((sum, r) => sum + (r.days_count ?? r.days ?? 0), 0))
9
+ const pendingCount = computed(() => pendingLeave.value.length)
10
+
11
+ const stats = computed(() => ({
12
+ pending: pendingLeave.value.length,
13
+ approved: approvedLeave.value.length,
14
+ rejected: rejectedLeave.value.length,
15
+ totalDays: totalDaysUsed.value,
16
+ }))
17
+
18
+ const { unwrap } = useHrApi()
19
+
20
+ async function fetchRequests(params?: Record<string, string>) {
21
+ loading.value = true
22
+ try {
23
+ const res = await $fetch<any>('/api/hr/leave/requests', { query: params })
24
+ leaveRequests.value = unwrap(res?.data ?? res) ?? []
25
+ }
26
+ finally {
27
+ loading.value = false
28
+ }
29
+ }
30
+
31
+ async function setStatus(id: string | number, status: LeaveRequest['status'], rejectReason?: string) {
32
+ const { unwrap: u } = useHrApi()
33
+ await $fetch(`/api/hr/leave/requests/${id}`, {
34
+ method: 'PATCH',
35
+ body: { status, reject_reason: rejectReason },
36
+ })
37
+ const idx = leaveRequests.value.findIndex(r => String(r.id) === String(id))
38
+ if (idx >= 0) {
39
+ leaveRequests.value[idx] = { ...leaveRequests.value[idx]!, status }
40
+ }
41
+ }
42
+
43
+ async function createRequest(personId: string | number, payload: {
44
+ leave_type_id: number
45
+ start_date: string
46
+ end_date: string
47
+ reason?: string
48
+ }) {
49
+ const res = await $fetch<any>(`/api/hr/people/${personId}/leave-requests`, {
50
+ method: 'POST',
51
+ body: payload,
52
+ })
53
+ const created: LeaveRequest = unwrap(res?.data ?? res)
54
+ leaveRequests.value = [created, ...leaveRequests.value]
55
+ return created
56
+ }
57
+
58
+ return {
59
+ leaveRequests,
60
+ loading,
61
+ pendingLeave,
62
+ approvedLeave,
63
+ rejectedLeave,
64
+ totalDaysUsed,
65
+ pendingCount,
66
+ stats,
67
+ fetchRequests,
68
+ setStatus,
69
+ createRequest,
70
+ }
71
+ }
@@ -0,0 +1,49 @@
1
+ export function useOffboarding() {
2
+ const { $api } = useNuxtApp()
3
+ const unwrap = (r: any) => (r && typeof r === 'object' && 'data' in r ? r.data : r)
4
+ // Shared signal so the Off-boarding tab re-fetches after an action fired
5
+ // from elsewhere (e.g. the Initiate modal in the detail navbar) even when
6
+ // the route doesn't change.
7
+ const refreshKey = useState('hr:offboarding:refresh', () => 0)
8
+
9
+ async function fetchForPerson(personId: string | number): Promise<Offboarding | null> {
10
+ return unwrap(await $api<any>(`/hr/people/${personId}/offboarding`))
11
+ }
12
+ async function listForPerson(personId: string | number): Promise<Offboarding[]> {
13
+ return unwrap(await $api<any>(`/hr/people/${personId}/offboarding/cases`)) ?? []
14
+ }
15
+ async function list(): Promise<Offboarding[]> {
16
+ return unwrap(await $api<any>(`/hr/offboarding`)) ?? []
17
+ }
18
+ async function get(id: string | number): Promise<Offboarding> {
19
+ return unwrap(await $api<any>(`/hr/offboarding/${id}`))
20
+ }
21
+ async function initiate(personId: string | number, body: Partial<Offboarding>): Promise<Offboarding> {
22
+ const created = unwrap(await $api<any>(`/hr/people/${personId}/offboarding`, { method: 'POST', body }))
23
+ refreshKey.value++
24
+ return created
25
+ }
26
+ async function updateCase(id: string | number, body: Partial<Offboarding>): Promise<Offboarding> {
27
+ return unwrap(await $api<any>(`/hr/offboarding/${id}`, { method: 'PATCH', body }))
28
+ }
29
+ async function updateTask(id: string | number, taskId: string | number, body: Partial<OffboardingTask>) {
30
+ return $api<any>(`/hr/offboarding/${id}/tasks/${taskId}`, { method: 'PATCH', body })
31
+ }
32
+ async function returnAssets(id: string | number) {
33
+ return unwrap(await $api<any>(`/hr/offboarding/${id}/return-assets`, { method: 'POST' }))
34
+ }
35
+ async function deprovision(id: string | number) {
36
+ return unwrap(await $api<any>(`/hr/offboarding/${id}/deprovision`, { method: 'POST' }))
37
+ }
38
+ async function getSettlement(id: string | number): Promise<OffboardingSettlement> {
39
+ return unwrap(await $api<any>(`/hr/offboarding/${id}/settlement`))
40
+ }
41
+ async function finalize(id: string | number): Promise<Offboarding> {
42
+ return unwrap(await $api<any>(`/hr/offboarding/${id}/finalize`, { method: 'POST' }))
43
+ }
44
+ async function cancel(id: string | number): Promise<Offboarding> {
45
+ return unwrap(await $api<any>(`/hr/offboarding/${id}/cancel`, { method: 'POST' }))
46
+ }
47
+
48
+ return { refreshKey, fetchForPerson, listForPerson, list, get, initiate, updateCase, updateTask, returnAssets, deprovision, getSettlement, finalize, cancel }
49
+ }