@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,149 @@
1
+ // Shared state — only 1 fetch at a time
2
+ let initialFetch: Promise<void> | null = null
3
+
4
+ export function usePeople() {
5
+ const api = useHrApi()
6
+ const { t } = useI18n()
7
+ const toast = useToast()
8
+
9
+ const people = useState<Person[]>('hr_people', () => [])
10
+ const loading = ref(false)
11
+ const searchQuery = ref('')
12
+ const stageFilter = ref<string>('all')
13
+ const departmentFilter = ref<number | null>(null)
14
+ const meta = ref<{ total_count?: number; filter_count?: number }>({})
15
+
16
+ async function fetchPeople(force = false) {
17
+ if (initialFetch && !force) return initialFetch
18
+ loading.value = true
19
+ initialFetch = (async () => {
20
+ try {
21
+ const res = await api.listPeople({})
22
+ people.value = Array.isArray(res.data) ? res.data : []
23
+ meta.value = res.meta ?? {}
24
+ }
25
+ finally {
26
+ loading.value = false
27
+ initialFetch = null
28
+ }
29
+ })()
30
+ return initialFetch
31
+ }
32
+
33
+ // Derived: talent-stage people (talent, interviewing, offer)
34
+ const talents = computed(() =>
35
+ people.value.filter(p => ['talent', 'interviewing', 'offer'].includes(p.stage))
36
+ )
37
+
38
+ // Derived: employee-stage people (probation, active, resigned)
39
+ const EMPLOYEE_STAGES = ['probation', 'active', 'resigned']
40
+
41
+ const employees = computed(() =>
42
+ people.value.filter(p => EMPLOYEE_STAGES.includes(p.stage))
43
+ )
44
+
45
+ const activeEmployees = computed(() =>
46
+ people.value.filter(p => p.stage === 'active')
47
+ )
48
+
49
+ const expiringContracts = computed(() => {
50
+ const today = new Date()
51
+ const in30days = new Date(today)
52
+ in30days.setDate(today.getDate() + 30)
53
+ return employees.value.filter(p => {
54
+ if (!p.contract?.end_date) return false
55
+ const end = new Date(p.contract.end_date)
56
+ return end >= today && end <= in30days
57
+ })
58
+ })
59
+
60
+ function getPerson(id: string): Person | undefined {
61
+ return people.value.find(p => p.id === id)
62
+ }
63
+
64
+ async function createPerson(input: Partial<Person>): Promise<Person> {
65
+ const person = await api.createPerson(input)
66
+ await fetchPeople()
67
+ return person
68
+ }
69
+
70
+ async function updatePerson(id: string, patch: Partial<Person>): Promise<Person> {
71
+ const person = await api.updatePerson(id, patch)
72
+ const idx = people.value.findIndex(p => p.id === id)
73
+ if (idx >= 0) people.value[idx] = person
74
+ return person
75
+ }
76
+
77
+ async function deletePerson(id: string): Promise<void> {
78
+ await api.deletePerson(id)
79
+ people.value = people.value.filter(p => p.id !== id)
80
+ }
81
+
82
+ async function refreshPerson(id: string): Promise<void> {
83
+ const person = await api.getPerson(id)
84
+ const idx = people.value.findIndex(p => p.id === id)
85
+ if (idx >= 0) people.value[idx] = person
86
+ }
87
+
88
+ // Phase 4: confirm hire → re-fetch person (now stage=probation) then redirect to employee profile
89
+ async function confirmHire(personId: string): Promise<void> {
90
+ await refreshPerson(personId)
91
+ await navigateTo(`/hr/employees/${personId}`)
92
+ }
93
+
94
+ // Hire a talent: any stage → probation (creates employee_id, fires provisioning hook), then redirect
95
+ async function hire(personId: string, opts: { reason?: string; start_date?: string } = {}): Promise<void> {
96
+ await $fetch(`/api/hr/people/${personId}/transition`, {
97
+ method: 'POST',
98
+ body: { to: 'probation', reason: opts.reason ?? '', start_date: opts.start_date || undefined },
99
+ })
100
+ await refreshPerson(personId)
101
+ toast.add({ title: t('hr.toast.hired'), color: 'success' })
102
+ await navigateTo(`/hr/employees/${personId}`)
103
+ }
104
+
105
+ // Phase 5: stage actions for employees
106
+ async function confirmProbation(personId: string): Promise<void> {
107
+ await $fetch(`/api/hr/people/${personId}/transition`, {
108
+ method: 'POST',
109
+ body: { to: 'active', reason: '' },
110
+ })
111
+ await refreshPerson(personId)
112
+ toast.add({ title: t('hr.toast.confirmed'), color: 'success' })
113
+ }
114
+
115
+ // confirmResign removed — resignation is now handled by the Off-boarding flow
116
+ // (composables/use-offboarding.ts → POST /hr/people/:id/offboarding + finalize).
117
+
118
+ async function reactivate(personId: string): Promise<void> {
119
+ await $fetch(`/api/hr/people/${personId}/transition`, {
120
+ method: 'POST',
121
+ body: { to: 'active', reason: 'Reactivated' },
122
+ })
123
+ await refreshPerson(personId)
124
+ toast.add({ title: t('hr.toast.reactivated'), color: 'success' })
125
+ }
126
+
127
+ return {
128
+ people,
129
+ loading,
130
+ meta,
131
+ searchQuery,
132
+ stageFilter,
133
+ departmentFilter,
134
+ talents,
135
+ employees,
136
+ activeEmployees,
137
+ expiringContracts,
138
+ fetchPeople,
139
+ getPerson,
140
+ createPerson,
141
+ updatePerson,
142
+ deletePerson,
143
+ refreshPerson,
144
+ confirmHire,
145
+ hire,
146
+ confirmProbation,
147
+ reactivate,
148
+ }
149
+ }
@@ -0,0 +1,44 @@
1
+ export function useProviders() {
2
+ const providers = useState<HrProvider[]>('hr:providers', () => [])
3
+ const isPending = ref(false)
4
+
5
+ async function fetchProviders() {
6
+ isPending.value = true
7
+ try {
8
+ const res = await $fetch<any>('/api/hr/providers')
9
+ providers.value = Array.isArray(res) ? res : (res?.data ?? [])
10
+ }
11
+ finally {
12
+ isPending.value = false
13
+ }
14
+ }
15
+
16
+ async function addProvider(payload: Omit<HrProvider, 'id' | 'date_created' | 'date_updated'>) {
17
+ const res = await $fetch<any>('/api/hr/providers', { method: 'POST', body: payload })
18
+ const created: HrProvider = res?.data ?? res
19
+ providers.value = [...providers.value, created]
20
+ return created
21
+ }
22
+
23
+ async function updateProvider(id: string, payload: Partial<HrProvider>) {
24
+ const res = await $fetch<any>(`/api/hr/providers/${id}`, { method: 'PATCH', body: payload })
25
+ const updated: HrProvider = res?.data ?? res
26
+ const idx = providers.value.findIndex(p => p.id === id)
27
+ if (idx >= 0) providers.value[idx] = updated
28
+ return updated
29
+ }
30
+
31
+ async function deleteProvider(id: string) {
32
+ await $fetch(`/api/hr/providers/${id}`, { method: 'DELETE' })
33
+ providers.value = providers.value.filter(p => p.id !== id)
34
+ }
35
+
36
+ async function testConnection(id: string): Promise<{ success: boolean; message: string }> {
37
+ const res = await $fetch<{ success: boolean; message: string }>(`/api/hr/providers/${id}/test`, { method: 'POST' })
38
+ return res
39
+ }
40
+
41
+ onMounted(fetchProviders)
42
+
43
+ return { providers, isPending, fetchProviders, addProvider, updateProvider, deleteProvider, testConnection }
44
+ }
@@ -0,0 +1,173 @@
1
+ import type {
2
+ WorkflowDef,
3
+ WorkflowInstanceFull,
4
+ WorkflowStep,
5
+ WorkflowInstanceStep,
6
+ WorkflowAction,
7
+ } from '@odp/workflow/shared/types'
8
+
9
+ export interface RecruitmentPipeline {
10
+ id: string
11
+ name: string
12
+ description: string | null
13
+ steps: WorkflowStep[]
14
+ }
15
+
16
+ export interface PipelineProgress {
17
+ instance: WorkflowInstanceFull
18
+ steps: WorkflowStep[]
19
+ instanceSteps: WorkflowInstanceStep[]
20
+ currentStepId: string | null
21
+ actions: WorkflowAction[]
22
+ isCompleted: boolean
23
+ isRejected: boolean
24
+ }
25
+
26
+ export function useRecruitmentWorkflow() {
27
+ const { $api } = useNuxtApp()
28
+
29
+ async function getAvailablePipelines(): Promise<RecruitmentPipeline[]> {
30
+ const res = await $api<{ data: WorkflowDef[] }>('/workflows', {
31
+ params: {
32
+ filter: JSON.stringify({
33
+ collection: { _eq: 'hr_applications' },
34
+ status: { _eq: 'active' },
35
+ trigger_event: { _eq: 'manual' },
36
+ }),
37
+ },
38
+ })
39
+ const workflows = res.data ?? res ?? []
40
+ const arr = Array.isArray(workflows) ? workflows : []
41
+
42
+ const pipelines: RecruitmentPipeline[] = []
43
+ for (const wf of arr) {
44
+ const full = await $api<{ data: any }>(`/workflows/${encodeURIComponent(wf.id)}`)
45
+ const data = full.data ?? full
46
+ pipelines.push({
47
+ id: wf.id,
48
+ name: wf.name,
49
+ description: wf.description,
50
+ steps: data.steps ?? [],
51
+ })
52
+ }
53
+ return pipelines
54
+ }
55
+
56
+ function guessInterviewType(stepName: string): string {
57
+ const lower = stepName.toLowerCase()
58
+ if (lower.includes('screen') || lower.includes('hr')) return 'Screening'
59
+ if (lower.includes('techni') || lower.includes('code') || lower.includes('coding')) return 'Technical'
60
+ if (lower.includes('cultur') || lower.includes('fit') || lower.includes('behav')) return 'Culture'
61
+ if (lower.includes('final') || lower.includes('manager') || lower.includes('director')) return 'Final'
62
+ return 'Other'
63
+ }
64
+
65
+ async function createInterviewsForSteps(
66
+ personId: string | number,
67
+ appId: number,
68
+ steps: WorkflowStep[],
69
+ ): Promise<void> {
70
+ const approvalSteps = steps
71
+ .filter(s => s.type === 'approval')
72
+ .sort((a, b) => a.sort_order - b.sort_order)
73
+
74
+ for (const step of approvalSteps) {
75
+ await $api(
76
+ `/hr/people/${personId}/applications/${appId}/interviews`,
77
+ {
78
+ method: 'POST',
79
+ body: {
80
+ type: guessInterviewType(step.name),
81
+ label: step.name,
82
+ status: 'Scheduled',
83
+ workflow_step_id: step.id,
84
+ },
85
+ },
86
+ )
87
+ }
88
+ }
89
+
90
+ async function startPipeline(
91
+ personId: string | number,
92
+ appId: number,
93
+ workflowId: string,
94
+ ): Promise<WorkflowInstanceFull> {
95
+ const res = await $api<{ data: any }>('/workflow-instances', {
96
+ method: 'POST',
97
+ body: {
98
+ workflow_id: workflowId,
99
+ collection: 'hr_applications',
100
+ item_id: String(appId),
101
+ },
102
+ })
103
+ const created = res.data ?? res
104
+
105
+ await $api(`/hr/people/${personId}/applications/${appId}`, {
106
+ method: 'PATCH',
107
+ body: { workflow_instance_id: created.id },
108
+ })
109
+
110
+ const fullRes = await $api<{ data: WorkflowInstanceFull }>(
111
+ `/workflow-instances/${encodeURIComponent(created.id)}`,
112
+ )
113
+ const instance = fullRes.data ?? fullRes as any as WorkflowInstanceFull
114
+
115
+ const steps: WorkflowStep[] = instance.workflow_steps ?? []
116
+ if (steps.length) {
117
+ await createInterviewsForSteps(personId, appId, steps)
118
+ }
119
+
120
+ return instance
121
+ }
122
+
123
+ async function loadInstance(instanceId: string): Promise<PipelineProgress | null> {
124
+ try {
125
+ const res = await $api<{ data: WorkflowInstanceFull }>(
126
+ `/workflow-instances/${encodeURIComponent(instanceId)}`,
127
+ )
128
+ const inst = res.data ?? res as any as WorkflowInstanceFull
129
+ return {
130
+ instance: inst,
131
+ steps: inst.workflow_steps ?? [],
132
+ instanceSteps: inst.instance_steps ?? [],
133
+ currentStepId: inst.current_step_id,
134
+ actions: inst.actions ?? [],
135
+ isCompleted: inst.status === 'completed',
136
+ isRejected: inst.status === 'rejected',
137
+ }
138
+ }
139
+ catch {
140
+ return null
141
+ }
142
+ }
143
+
144
+ async function approveStep(instanceId: string, stepId: string, comment?: string): Promise<void> {
145
+ await $api(
146
+ `/workflow-instances/${encodeURIComponent(instanceId)}/steps/${encodeURIComponent(stepId)}/approve`,
147
+ { method: 'POST', body: { comment } },
148
+ )
149
+ }
150
+
151
+ async function rejectStep(instanceId: string, stepId: string, comment: string): Promise<void> {
152
+ await $api(
153
+ `/workflow-instances/${encodeURIComponent(instanceId)}/steps/${encodeURIComponent(stepId)}/reject`,
154
+ { method: 'POST', body: { comment } },
155
+ )
156
+ }
157
+
158
+ async function cancelWorkflow(instanceId: string, comment?: string): Promise<void> {
159
+ await $api(
160
+ `/workflow-instances/${encodeURIComponent(instanceId)}/cancel`,
161
+ { method: 'POST', body: { comment } },
162
+ )
163
+ }
164
+
165
+ return {
166
+ getAvailablePipelines,
167
+ startPipeline,
168
+ loadInstance,
169
+ approveStep,
170
+ rejectStep,
171
+ cancelWorkflow,
172
+ }
173
+ }
@@ -0,0 +1,44 @@
1
+ export function useTemplates() {
2
+ const templates = useState<any[]>('hr:templates', () => [])
3
+
4
+ async function fetchTemplates(includeArchived = false) {
5
+ const qs = includeArchived ? '?include_archived=1' : ''
6
+ const res = await $fetch<any>(`/api/hr/documents/templates${qs}`)
7
+ templates.value = Array.isArray(res) ? res : (res?.data ?? [])
8
+ }
9
+
10
+ async function saveTemplate(id: string | undefined, payload: Record<string, any>) {
11
+ if (id) {
12
+ const res = await $fetch<any>(`/api/hr/documents/templates/${id}`, {
13
+ method: 'PATCH',
14
+ body: payload,
15
+ })
16
+ const updated = res?.data ?? res
17
+ const idx = templates.value.findIndex((t: any) => t.id === id)
18
+ if (idx >= 0) templates.value[idx] = updated
19
+ return updated
20
+ }
21
+ else {
22
+ const res = await $fetch<any>('/api/hr/documents/templates', {
23
+ method: 'POST',
24
+ body: payload,
25
+ })
26
+ const created = res?.data ?? res
27
+ templates.value.push(created)
28
+ return created
29
+ }
30
+ }
31
+
32
+ async function archiveTemplate(id: string) {
33
+ await $fetch(`/api/hr/documents/templates/${id}`, { method: 'DELETE' })
34
+ const idx = templates.value.findIndex((t: any) => t.id === id)
35
+ if (idx >= 0) templates.value[idx] = { ...templates.value[idx], is_archived: true }
36
+ }
37
+
38
+ return {
39
+ templates,
40
+ fetchTemplates,
41
+ saveTemplate,
42
+ archiveTemplate,
43
+ }
44
+ }
@@ -0,0 +1,26 @@
1
+ export function useTriggers() {
2
+ const triggers = useState<any[]>('hr:triggers', () => [])
3
+
4
+ async function fetchTriggers() {
5
+ const res = await $fetch<any>('/api/hr/documents/triggers')
6
+ triggers.value = Array.isArray(res) ? res : (res?.data ?? [])
7
+ }
8
+
9
+ async function updateTrigger(id: string, payload: { auto_generate?: boolean, template_ids?: string[] }) {
10
+ const res = await $fetch<any>(`/api/hr/documents/triggers/${id}`, {
11
+ method: 'PATCH',
12
+ body: payload,
13
+ })
14
+ const idx = triggers.value.findIndex((t: any) => t.id === id)
15
+ if (idx >= 0) {
16
+ triggers.value[idx] = { ...triggers.value[idx], ...payload }
17
+ }
18
+ return res
19
+ }
20
+
21
+ return {
22
+ triggers,
23
+ fetchTriggers,
24
+ updateTrigger,
25
+ }
26
+ }
@@ -0,0 +1,4 @@
1
+ // Column renderers for HR module list views.
2
+ // Populated incrementally per phase.
3
+ // Phase 2: employee list columns
4
+ // Phase 5: employee detail columns
@@ -0,0 +1,193 @@
1
+ import type { HrFormLayout, HrInfoLayout } from '../../shared/types/form-layout'
2
+
3
+ export const TALENT_FORM_LAYOUT: HrFormLayout = [
4
+ {
5
+ title: 'Identity',
6
+ columns: 2,
7
+ fields: [
8
+ { key: 'first_name', type: 'input', required: true, colspan: 1 },
9
+ { key: 'last_name', type: 'input', required: true, colspan: 1 },
10
+ { key: 'email', type: 'email', colspan: 1 },
11
+ { key: 'phone', type: 'phone', colspan: 1 },
12
+ { key: 'date_of_birth', type: 'date', colspan: 1 },
13
+ {
14
+ key: 'gender',
15
+ type: 'select',
16
+ colspan: 1,
17
+ options: [
18
+ { value: 'male', label: 'Male' },
19
+ { value: 'female', label: 'Female' },
20
+ { value: 'other', label: 'Other' },
21
+ ],
22
+ },
23
+ { key: 'nationality', type: 'input', colspan: 1 },
24
+ { key: 'id_number', type: 'input', colspan: 1 },
25
+ ],
26
+ },
27
+ {
28
+ title: 'Recruitment',
29
+ columns: 2,
30
+ fields: [
31
+ { key: 'desired_position', type: 'input', colspan: 2 },
32
+ { key: 'source', type: 'input', colspan: 1 },
33
+ { key: 'department_id', type: 'select', colspan: 1 },
34
+ ],
35
+ },
36
+ ]
37
+
38
+ // Phase 4: Hire form layout (for hire-form-modal.vue)
39
+ export const HIRE_FORM_LAYOUT = {
40
+ sections: [
41
+ {
42
+ key: 'auto_ids',
43
+ label: 'Auto-generated IDs',
44
+ fields: ['employee_id', 'contract_number'],
45
+ readonly: true,
46
+ },
47
+ {
48
+ key: 'position',
49
+ label: 'Position',
50
+ fields: ['job_title', 'department', 'employment_type_id'],
51
+ },
52
+ {
53
+ key: 'contract',
54
+ label: 'Contract',
55
+ fields: ['start_date', 'contract_type', 'probation_months'],
56
+ },
57
+ ],
58
+ } as const
59
+
60
+ export const TALENT_INFO_LAYOUT: HrInfoLayout = [
61
+ {
62
+ title: 'Identity',
63
+ rows: [
64
+ { label: 'Full Name', key: 'display_name' },
65
+ { label: 'Email', key: 'email' },
66
+ { label: 'Phone', key: 'phone' },
67
+ { label: 'Date of Birth', key: 'date_of_birth', format: 'date' },
68
+ { label: 'Gender', key: 'gender' },
69
+ { label: 'Nationality', key: 'nationality' },
70
+ { label: 'ID Number', key: 'id_number' },
71
+ ],
72
+ },
73
+ {
74
+ title: 'Recruitment',
75
+ rows: [
76
+ { label: 'Desired Position', key: 'desired_position' },
77
+ { label: 'Source', key: 'source' },
78
+ { label: 'Department', key: 'department_id' },
79
+ ],
80
+ },
81
+ ]
82
+
83
+ // Phase 5: Employee Management layouts
84
+
85
+ export const EMPLOYEE_FORM_LAYOUT = {
86
+ sections: [
87
+ {
88
+ key: 'identity',
89
+ label: 'employees.sections.identity',
90
+ fields: [
91
+ { key: 'first_name', type: 'text', required: true },
92
+ { key: 'last_name', type: 'text', required: true },
93
+ { key: 'email', type: 'email', required: true },
94
+ { key: 'phone', type: 'phone' },
95
+ { key: 'date_of_birth', type: 'date' },
96
+ ],
97
+ },
98
+ {
99
+ key: 'employment',
100
+ label: 'employees.sections.employment',
101
+ fields: [
102
+ { key: 'employee_id', type: 'text', readonly: true },
103
+ { key: 'department_id', type: 'relation', collection: 'hr_departments' },
104
+ { key: 'job_title', type: 'text' },
105
+ { key: 'employment_type_id', type: 'relation', collection: 'hr_employment_types' },
106
+ { key: 'start_date', type: 'date' },
107
+ { key: 'manager_id', type: 'relation', collection: 'hr_people' },
108
+ ],
109
+ },
110
+ {
111
+ key: 'emergency',
112
+ label: 'employees.sections.emergency',
113
+ fields: [
114
+ { key: 'emergency_contact_name', type: 'text' },
115
+ { key: 'emergency_contact_phone', type: 'phone' },
116
+ { key: 'emergency_contact_relation', type: 'text' },
117
+ ],
118
+ },
119
+ ],
120
+ } as const
121
+
122
+ export const CONTRACT_LAYOUT = {
123
+ sections: [
124
+ {
125
+ key: 'contract_details',
126
+ label: 'contract.sections.details',
127
+ fields: [
128
+ { key: 'contract_type', type: 'select', options: ['Indefinite', 'Definite'], required: true },
129
+ { key: 'contract_number', type: 'text', required: true },
130
+ { key: 'start_date', type: 'date', required: true },
131
+ { key: 'end_date', type: 'date', conditional: (form: any) => form.contract_type === 'Definite' },
132
+ { key: 'probation_end', type: 'date', readonly: true },
133
+ { key: 'renewal_count', type: 'number', min: 0 },
134
+ ],
135
+ },
136
+ ],
137
+ } as const
138
+
139
+ export const COMPENSATION_LAYOUT = {
140
+ sections: [
141
+ {
142
+ key: 'salary',
143
+ label: 'compensation.sections.salary',
144
+ fields: [
145
+ { key: 'base_salary', type: 'currency', required: true },
146
+ { key: 'currency', type: 'select', source: 'settings.currency' },
147
+ { key: 'pay_frequency', type: 'select', options: ['Monthly', 'Bi-weekly', 'Weekly'] },
148
+ { key: 'bonus_target', type: 'percentage' },
149
+ ],
150
+ },
151
+ {
152
+ key: 'bank',
153
+ label: 'compensation.sections.bank',
154
+ fields: [
155
+ { key: 'bank_name', type: 'text' },
156
+ { key: 'bank_account_number', type: 'text' },
157
+ ],
158
+ },
159
+ {
160
+ key: 'allowances',
161
+ label: 'compensation.sections.allowances',
162
+ type: 'dynamic_list',
163
+ itemFields: [
164
+ { key: 'name', type: 'text', required: true },
165
+ { key: 'amount', type: 'currency', required: true },
166
+ { key: 'type', type: 'select', options: ['Fixed', 'Percentage'] },
167
+ ],
168
+ },
169
+ ],
170
+ } as const
171
+
172
+ export const INSURANCE_LAYOUT = {
173
+ sections: [
174
+ {
175
+ key: 'insurance_ids',
176
+ label: 'insurance.sections.ids',
177
+ fields: [
178
+ { key: 'social_insurance_number', type: 'text' },
179
+ { key: 'health_insurance_number', type: 'text' },
180
+ { key: 'insurance_contribution_rate', type: 'percentage' },
181
+ ],
182
+ },
183
+ {
184
+ key: 'tax',
185
+ label: 'insurance.sections.tax',
186
+ fields: [
187
+ { key: 'tax_id', type: 'text' },
188
+ { key: 'dependents_count', type: 'number', min: 0 },
189
+ { key: 'personal_deduction', type: 'currency' },
190
+ ],
191
+ },
192
+ ],
193
+ } as const