@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,116 @@
1
+ import type {
2
+ Person, Policy, PolicyKey,
3
+ AnnualLeaveBaseParams, AnnualLeaveTenureBonusParams,
4
+ SickLeaveBaseParams, PersonalLeaveBaseParams,
5
+ InsuranceEligibilityParams, ProbationPeriodParams,
6
+ LeaveBalance, BenefitFlag,
7
+ } from "../types"
8
+
9
+ const MS_PER_YEAR = 365.25 * 24 * 3600 * 1000
10
+
11
+ export function tenureYears(start_date: string | undefined, asOf: Date = new Date()): number {
12
+ if (!start_date) return 0
13
+ const start = new Date(start_date).getTime()
14
+ if (Number.isNaN(start)) return 0
15
+ return Math.max(0, (asOf.getTime() - start) / MS_PER_YEAR)
16
+ }
17
+
18
+ export function highestApplicableBonus(
19
+ tiers: AnnualLeaveTenureBonusParams["tiers"],
20
+ years: number,
21
+ ): number {
22
+ let bonus = 0
23
+ for (const t of tiers) {
24
+ if (years >= t.after_years && t.bonus > bonus) bonus = t.bonus
25
+ }
26
+ return bonus
27
+ }
28
+
29
+ function getPolicy<P>(policies: Policy[], key: PolicyKey): P | null {
30
+ const p = policies.find(p => p.key === key && p.enabled)
31
+ return p ? (p.params as P) : null
32
+ }
33
+
34
+ interface ComputedLeaveTotal { type: string, total: number }
35
+
36
+ export function computeLeavePolicyValues(person: Person, policies: Policy[]): ComputedLeaveTotal[] {
37
+ const out: ComputedLeaveTotal[] = []
38
+ const empType = person.employment_type ?? "Full-time"
39
+
40
+ const annualBase = getPolicy<AnnualLeaveBaseParams>(policies, "annual_leave_base")
41
+ const annualBonus = getPolicy<AnnualLeaveTenureBonusParams>(policies, "annual_leave_tenure_bonus")
42
+ const sickBase = getPolicy<SickLeaveBaseParams>(policies, "sick_leave_base")
43
+ const personalBase = getPolicy<PersonalLeaveBaseParams>(policies, "personal_leave_base")
44
+
45
+ if (annualBase) {
46
+ const base = annualBase.days_by_employment_type[empType] ?? 0
47
+ const bonus = annualBonus
48
+ ? highestApplicableBonus(annualBonus.tiers, tenureYears(person.start_date))
49
+ : 0
50
+ out.push({ type: "annual", total: base + bonus })
51
+ }
52
+ if (sickBase) {
53
+ out.push({ type: "sick", total: sickBase.days_by_employment_type[empType] ?? 0 })
54
+ }
55
+ if (personalBase) {
56
+ out.push({ type: "personal", total: personalBase.days_by_employment_type[empType] ?? 0 })
57
+ }
58
+ return out
59
+ }
60
+
61
+ export function computeBenefitsPolicyValues(person: Person, policies: Policy[]): { key: string, eligible: boolean }[] {
62
+ const out: { key: string, eligible: boolean }[] = []
63
+ const empType = person.employment_type ?? "Full-time"
64
+
65
+ const health = getPolicy<InsuranceEligibilityParams>(policies, "health_insurance_eligibility")
66
+ const social = getPolicy<InsuranceEligibilityParams>(policies, "social_insurance_eligibility")
67
+
68
+ if (health) {
69
+ out.push({ key: "health_insurance", eligible: health.employment_types.includes(empType) })
70
+ }
71
+ if (social) {
72
+ out.push({ key: "social_insurance", eligible: social.employment_types.includes(empType) })
73
+ }
74
+ return out
75
+ }
76
+
77
+ export function suggestProbationMonths(person: Person, policies: Policy[]): number | null {
78
+ const probation = getPolicy<ProbationPeriodParams>(policies, "probation_period")
79
+ if (!probation) return null
80
+ const empType = person.employment_type ?? "Full-time"
81
+ return probation.months_by_employment_type[empType] ?? null
82
+ }
83
+
84
+ export function applyPolicies(person: Person, policies: Policy[]): Person {
85
+ const leaveTotals = computeLeavePolicyValues(person, policies)
86
+ const benefitVals = computeBenefitsPolicyValues(person, policies)
87
+
88
+ const next_leave_balance: LeaveBalance[] = leaveTotals.map(({ type, total }) => {
89
+ const existing = person.leave_balance.find(l => l.type === type)
90
+ if (existing && existing.source === "override") {
91
+ return { ...existing, policy_value: total, remaining: existing.total - existing.used }
92
+ }
93
+ const used = existing?.used ?? 0
94
+ return { type, total, used, remaining: total - used, source: "policy", policy_value: total }
95
+ })
96
+
97
+ const policyTypes = new Set(leaveTotals.map(l => l.type))
98
+ for (const existing of person.leave_balance) {
99
+ if (!policyTypes.has(existing.type)) next_leave_balance.push(existing)
100
+ }
101
+
102
+ const next_benefits_eligibility: BenefitFlag[] = benefitVals.map(({ key, eligible }) => {
103
+ const existing = person.benefits_eligibility.find(b => b.key === key)
104
+ if (existing && existing.source === "override") {
105
+ return { ...existing, policy_value: eligible }
106
+ }
107
+ return { key, eligible, source: "policy", policy_value: eligible }
108
+ })
109
+
110
+ const policyBenefitKeys = new Set(benefitVals.map(b => b.key))
111
+ for (const existing of person.benefits_eligibility) {
112
+ if (!policyBenefitKeys.has(existing.key)) next_benefits_eligibility.push(existing)
113
+ }
114
+
115
+ return { ...person, leave_balance: next_leave_balance, benefits_eligibility: next_benefits_eligibility }
116
+ }
@@ -0,0 +1,114 @@
1
+ <template>
2
+ <ModulePage
3
+ :title="$t('departments.title')"
4
+ icon="i-ph-tree-structure-light"
5
+ padding="default"
6
+ max-width="full"
7
+ >
8
+ <template #actions>
9
+ <UButton
10
+ v-if="canCreate"
11
+ icon="i-ph-plus"
12
+ :label="$t('departments.create')"
13
+ color="primary"
14
+ size="sm"
15
+ @click="showForm = true"
16
+ />
17
+ </template>
18
+
19
+ <div
20
+ v-if="loading"
21
+ class="flex items-center justify-center py-16"
22
+ >
23
+ <UIcon
24
+ name="i-ph-spinner-gap-light"
25
+ class="size-6 animate-spin text-muted"
26
+ />
27
+ </div>
28
+
29
+ <div
30
+ v-else-if="!departments.length"
31
+ class="flex flex-col items-center justify-center py-16 text-muted"
32
+ >
33
+ <UIcon
34
+ name="i-ph-tree-structure-light"
35
+ class="size-12 mb-3 opacity-30"
36
+ />
37
+ <p class="text-sm">
38
+ {{ $t('departments.empty') }}
39
+ </p>
40
+ <UButton
41
+ v-if="canCreate"
42
+ class="mt-4"
43
+ icon="i-ph-plus"
44
+ :label="$t('departments.create')"
45
+ color="primary"
46
+ size="sm"
47
+ @click="showForm = true"
48
+ />
49
+ </div>
50
+
51
+ <div
52
+ v-else
53
+ class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
54
+ >
55
+ <HrDepartmentCard
56
+ v-for="dept in departments"
57
+ :key="dept.id"
58
+ :department="dept"
59
+ @edit="openEdit(dept)"
60
+ @delete="handleDelete(dept)"
61
+ />
62
+ </div>
63
+
64
+ <HrDepartmentFormModal
65
+ v-if="showForm"
66
+ :department="editingDept"
67
+ @close="closeForm"
68
+ @saved="onSaved"
69
+ />
70
+ </ModulePage>
71
+ </template>
72
+
73
+ <script lang="ts" setup>
74
+ const { t } = useI18n()
75
+ const toast = useToast()
76
+ const { departments, loading, fetchDepartments, deleteDepartment } = useDepartments()
77
+ const { can } = usePermissionRegistry()
78
+ const canCreate = computed(() => can('hr', 'create'))
79
+ const canDelete = computed(() => can('hr', 'delete'))
80
+
81
+ const showForm = ref(false)
82
+ const editingDept = ref<any | null>(null)
83
+
84
+ onMounted(() => fetchDepartments(true))
85
+
86
+ function openEdit(dept: any) {
87
+ editingDept.value = dept
88
+ showForm.value = true
89
+ }
90
+
91
+ function closeForm() {
92
+ showForm.value = false
93
+ editingDept.value = null
94
+ }
95
+
96
+ async function onSaved() {
97
+ closeForm()
98
+ await fetchDepartments(true)
99
+ }
100
+
101
+ async function handleDelete(dept: any) {
102
+ try {
103
+ await deleteDepartment(dept.id)
104
+ toast.add({ title: t('departments.toast.deleted'), color: 'success' })
105
+ }
106
+ catch (err: any) {
107
+ const code = err?.data?.code ?? err?.code
108
+ const msg = code === 'DEPT_HAS_MEMBERS'
109
+ ? t('departments.errors.has_members')
110
+ : (err?.message ?? t('hr.toast.error'))
111
+ toast.add({ title: msg, color: 'error' })
112
+ }
113
+ }
114
+ </script>
@@ -0,0 +1,10 @@
1
+ <script lang="ts" setup>
2
+ const { person, isReadonly } = useEmployeeDetail()
3
+ </script>
4
+
5
+ <template>
6
+ <HrPersonActivityTab
7
+ :person-id="person.id"
8
+ :readonly="isReadonly('activity')"
9
+ />
10
+ </template>
@@ -0,0 +1,14 @@
1
+ <script lang="ts" setup>
2
+ const route = useRoute()
3
+ const { person, isReadonly } = useEmployeeDetail()
4
+
5
+ const EMPLOYEE_STAGES = ["probation", "active", "resigned"]
6
+
7
+ if (!EMPLOYEE_STAGES.includes(person.value.stage)) {
8
+ await navigateTo(`/hr/employees/${route.params.id}/profile`, { replace: true })
9
+ }
10
+ </script>
11
+
12
+ <template>
13
+ <HrEmployeesAssetsTab :person="person" :readonly="isReadonly('assets')" />
14
+ </template>
@@ -0,0 +1,14 @@
1
+ <script lang="ts" setup>
2
+ const route = useRoute()
3
+ const { person, isReadonly } = useEmployeeDetail()
4
+
5
+ const EMPLOYEE_STAGES = ["probation", "active", "resigned"]
6
+
7
+ if (!EMPLOYEE_STAGES.includes(person.value.stage)) {
8
+ await navigateTo(`/hr/employees/${route.params.id}/profile`, { replace: true })
9
+ }
10
+ </script>
11
+
12
+ <template>
13
+ <HrEmployeesTab :person="person" :readonly="isReadonly('employment')" />
14
+ </template>
@@ -0,0 +1,9 @@
1
+ <script lang="ts" setup>
2
+ definePageMeta({
3
+ redirect: (to) => `/hr/employees/${to.params.id}/profile`,
4
+ })
5
+ </script>
6
+
7
+ <template>
8
+ <div />
9
+ </template>
@@ -0,0 +1,7 @@
1
+ <script lang="ts" setup>
2
+ const { person } = useEmployeeDetail()
3
+ </script>
4
+
5
+ <template>
6
+ <HrOffboardingTab :person="person" />
7
+ </template>
@@ -0,0 +1,11 @@
1
+ <script lang="ts" setup>
2
+ const { person, isReadonly, onUpdated } = useEmployeeDetail()
3
+ </script>
4
+
5
+ <template>
6
+ <HrPersonProfileTab
7
+ :person="person"
8
+ :readonly="isReadonly('profile')"
9
+ @updated="onUpdated"
10
+ />
11
+ </template>
@@ -0,0 +1,17 @@
1
+ <script lang="ts" setup>
2
+ const route = useRoute()
3
+ const { person, isReadonly } = useEmployeeDetail()
4
+
5
+ const EMPLOYEE_STAGES = ["probation", "active", "resigned"]
6
+
7
+ if (!EMPLOYEE_STAGES.includes(person.value.stage)) {
8
+ await navigateTo(`/hr/employees/${route.params.id}/profile`, { replace: true })
9
+ }
10
+ </script>
11
+
12
+ <template>
13
+ <HrEmployeesProvisioningTab
14
+ :person="person"
15
+ :readonly="isReadonly('provisioning')"
16
+ />
17
+ </template>
@@ -0,0 +1,313 @@
1
+ <script lang="ts" setup>
2
+ import type { Ref } from "vue"
3
+
4
+ const { t } = useI18n()
5
+ const route = useRoute()
6
+ const api = useHrApi()
7
+
8
+ const { data: person, status, refresh } = await useAsyncData(
9
+ `hr-person-${route.params.id}`,
10
+ () => api.getPerson(String(route.params.id)),
11
+ { watch: [() => route.params.id] },
12
+ )
13
+
14
+ const localPerson = ref<Person | null>(
15
+ person.value ? { ...person.value } : null,
16
+ )
17
+ watch(person, (p) => {
18
+ localPerson.value = p ? { ...p } : null
19
+ })
20
+
21
+ const EMPLOYEE_STAGES = ["probation", "active", "resigned"]
22
+
23
+ const isEmployee = computed(() =>
24
+ localPerson.value ? EMPLOYEE_STAGES.includes(localPerson.value.stage) : false,
25
+ )
26
+
27
+ const tabDefs = computed(() => {
28
+ if (!localPerson.value) return []
29
+ const tabs = [
30
+ { id: "profile", label: t("hr.tabs.profile"), icon: "i-ph-user-light" },
31
+ ]
32
+ if (isEmployee.value) {
33
+ tabs.push(
34
+ {
35
+ id: "employment",
36
+ label: t("hr.tabs.employment"),
37
+ icon: "i-ph-briefcase-light",
38
+ },
39
+ {
40
+ id: "assets",
41
+ label: t("assets.tab_label"),
42
+ icon: "i-heroicons-computer-desktop",
43
+ },
44
+ {
45
+ id: "provisioning",
46
+ label: t("hr.tabs.provisioning"),
47
+ icon: "i-ph-plugs-connected-light",
48
+ },
49
+ {
50
+ id: "offboarding",
51
+ label: t("hr.tabs.offboarding"),
52
+ icon: "i-ph-sign-out-light",
53
+ },
54
+ )
55
+ }
56
+ tabs.push({
57
+ id: "activity",
58
+ label: t("hr.tabs.activity"),
59
+ icon: "i-ph-clock-light",
60
+ })
61
+ return tabs
62
+ })
63
+
64
+ const basePath = computed(() => `/hr/employees/${route.params.id}`)
65
+
66
+ const tabItems = computed(() =>
67
+ tabDefs.value.map((tab) => ({
68
+ label: tab.label,
69
+ icon: tab.icon,
70
+ value: tab.id,
71
+ })),
72
+ )
73
+
74
+ const activeTab = computed(() => {
75
+ const rest = route.path.slice(basePath.value.length).split("/").filter(Boolean)
76
+ return rest[0] ?? "profile"
77
+ })
78
+
79
+ function onTabChange(value: string | number) {
80
+ navigateTo(`${basePath.value}/${value}`)
81
+ }
82
+
83
+ const readonlyTabs = computed(() =>
84
+ localPerson.value?.stage === "resigned" ? ["employment", "assets"] : [],
85
+ )
86
+
87
+ provideEmployeeDetail({
88
+ person: localPerson as Ref<Person>,
89
+ isReadonly: (tab: string) => readonlyTabs.value.includes(tab),
90
+ onUpdated: (p: Person) => {
91
+ localPerson.value = { ...p }
92
+ refresh()
93
+ },
94
+ })
95
+
96
+ const subtitle = computed(() => {
97
+ const p = localPerson.value
98
+ if (!p) return ""
99
+ const bits: string[] = []
100
+ if (p.job_title) bits.push(p.job_title)
101
+ if (p.desired_position && !p.job_title) bits.push(p.desired_position)
102
+ if (p.employee_id) bits.push(p.employee_id)
103
+ return bits.join(" · ")
104
+ })
105
+
106
+ // Employee lifecycle actions
107
+ const { confirmProbation, reactivate } = usePeople()
108
+ const { initiate: initiateOffboarding } = useOffboarding()
109
+ const toast = useToast()
110
+ const acting = ref(false)
111
+ const showOffboard = ref(false)
112
+ const offboardForm = ref<Record<string, any>>({ exit_type: "voluntary", rehire_eligible: true })
113
+
114
+ const OFFBOARD_FIELD_NAMES = ["exit_type", "notice_date", "last_working_day", "rehire_eligible", "reason"]
115
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(["hr_offboarding"])
116
+ const offboardFields = computed(() => pickFields("hr_offboarding", OFFBOARD_FIELD_NAMES))
117
+
118
+ watch(showOffboard, (open) => {
119
+ if (open) {
120
+ offboardForm.value = { exit_type: "voluntary", rehire_eligible: true }
121
+ fetchAll()
122
+ }
123
+ })
124
+
125
+ async function runAction(fn: () => Promise<void>) {
126
+ acting.value = true
127
+ try {
128
+ await fn()
129
+ await refresh()
130
+ } catch {
131
+ // composable surfaces its own error toast
132
+ } finally {
133
+ acting.value = false
134
+ }
135
+ }
136
+
137
+ function onConfirmActive() {
138
+ return runAction(() => confirmProbation(String(route.params.id)))
139
+ }
140
+
141
+ function onReactivate() {
142
+ return runAction(() => reactivate(String(route.params.id)))
143
+ }
144
+
145
+ async function onInitiateOffboarding() {
146
+ if (!offboardForm.value.last_working_day) return
147
+ acting.value = true
148
+ try {
149
+ await initiateOffboarding(String(route.params.id), { ...offboardForm.value })
150
+ showOffboard.value = false
151
+ toast.add({ title: t("hr.offboarding.toast.initiated"), color: "success" })
152
+ navigateTo(`${basePath.value}/offboarding`)
153
+ }
154
+ catch (err: any) {
155
+ toast.add({ title: err?.message ?? t("hr.toast.error"), color: "error" })
156
+ }
157
+ finally {
158
+ acting.value = false
159
+ }
160
+ }
161
+ </script>
162
+
163
+ <template>
164
+ <UDashboardPanel v-if="localPerson">
165
+ <template #header>
166
+ <UDashboardNavbar
167
+ :title="localPerson.display_name"
168
+ :ui="{ title: 'text-lg font-semibold' }"
169
+ >
170
+ <template #leading>
171
+ <UButton
172
+ icon="i-ph-arrow-left-light"
173
+ variant="ghost"
174
+ size="sm"
175
+ @click="navigateTo('/hr/employees')"
176
+ />
177
+ </template>
178
+
179
+ <template #right>
180
+ <span
181
+ v-if="subtitle"
182
+ class="text-xs text-muted hidden lg:inline"
183
+ >{{ subtitle }}</span
184
+ >
185
+ <UButton
186
+ v-if="localPerson.stage === 'probation'"
187
+ size="sm"
188
+ color="primary"
189
+ variant="soft"
190
+ icon="i-ph-check-circle-light"
191
+ :label="$t('hr.employees.actions.confirm_probation')"
192
+ :loading="acting"
193
+ @click="onConfirmActive"
194
+ />
195
+ <UButton
196
+ v-if="localPerson.stage === 'probation' || localPerson.stage === 'active'"
197
+ size="sm"
198
+ color="error"
199
+ variant="soft"
200
+ icon="i-ph-sign-out-light"
201
+ :label="$t('hr.offboarding.initiate')"
202
+ :disabled="acting"
203
+ @click="showOffboard = true"
204
+ />
205
+ <UButton
206
+ v-if="localPerson.stage === 'resigned'"
207
+ size="sm"
208
+ color="primary"
209
+ variant="soft"
210
+ icon="i-ph-arrow-counter-clockwise-light"
211
+ :label="$t('hr.employees.actions.reactivate')"
212
+ :loading="acting"
213
+ @click="onReactivate"
214
+ />
215
+ </template>
216
+ </UDashboardNavbar>
217
+ </template>
218
+
219
+ <template #body>
220
+ <div class="flex flex-col h-full">
221
+ <UTabs
222
+ :model-value="activeTab"
223
+ :items="tabItems"
224
+ variant="link"
225
+ :content="false"
226
+ @update:model-value="onTabChange"
227
+ />
228
+
229
+ <div class="flex-1 overflow-auto py-6">
230
+ <NuxtPage />
231
+ </div>
232
+ </div>
233
+ </template>
234
+ </UDashboardPanel>
235
+
236
+ <UDashboardPanel v-else-if="status === 'pending'">
237
+ <template #body>
238
+ <div class="flex items-center justify-center h-64">
239
+ <UIcon
240
+ name="i-ph-spinner"
241
+ class="animate-spin size-6 text-muted"
242
+ />
243
+ </div>
244
+ </template>
245
+ </UDashboardPanel>
246
+
247
+ <UDashboardPanel v-else>
248
+ <template #header>
249
+ <UDashboardNavbar
250
+ :title="$t('hr.employees.title')"
251
+ :ui="{ title: 'text-lg font-semibold' }"
252
+ >
253
+ <template #leading>
254
+ <UButton
255
+ icon="i-ph-arrow-left-light"
256
+ variant="ghost"
257
+ size="sm"
258
+ @click="navigateTo('/hr/employees')"
259
+ />
260
+ </template>
261
+ </UDashboardNavbar>
262
+ </template>
263
+ <template #body>
264
+ <div class="flex items-center justify-center h-64">
265
+ <div class="text-center text-muted">
266
+ <UIcon
267
+ name="i-ph-warning-circle"
268
+ class="size-10 mb-2 opacity-40"
269
+ />
270
+ <p>{{ $t('hr.employees.not_found') }}</p>
271
+ </div>
272
+ </div>
273
+ </template>
274
+ </UDashboardPanel>
275
+
276
+ <UModal
277
+ :open="showOffboard"
278
+ :title="$t('hr.offboarding.initiate')"
279
+ :ui="{ content: 'sm:max-w-2xl' }"
280
+ @update:open="showOffboard = $event"
281
+ >
282
+ <template #body>
283
+ <div class="space-y-4">
284
+ <p class="text-sm text-muted">{{ $t('hr.offboarding.initiate_hint') }}</p>
285
+ <div v-if="!isReady('hr_offboarding')" class="py-4 text-sm text-muted text-center">
286
+ {{ $t('common.loading') }}
287
+ </div>
288
+ <FormRoot
289
+ v-else-if="offboardFields.length"
290
+ v-model="offboardForm"
291
+ :fields="offboardFields"
292
+ />
293
+ </div>
294
+ </template>
295
+ <template #footer>
296
+ <div class="flex justify-end gap-2">
297
+ <UButton
298
+ color="neutral"
299
+ variant="outline"
300
+ :label="$t('common.cancel')"
301
+ @click="showOffboard = false"
302
+ />
303
+ <UButton
304
+ color="primary"
305
+ :label="$t('hr.offboarding.initiate')"
306
+ :loading="acting"
307
+ :disabled="!offboardForm.last_working_day"
308
+ @click="onInitiateOffboarding"
309
+ />
310
+ </div>
311
+ </template>
312
+ </UModal>
313
+ </template>