@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,128 @@
1
+ <script lang="ts" setup>
2
+ import type { RecruitmentPipeline } from '../../../composables/use-recruitment-workflow'
3
+
4
+ const { t } = useI18n()
5
+ const toast = useToast()
6
+ const router = useRouter()
7
+ const { getAvailablePipelines } = useRecruitmentWorkflow()
8
+
9
+ const pipelines = ref<RecruitmentPipeline[]>([])
10
+ const loading = ref(false)
11
+
12
+ async function fetchPipelines() {
13
+ loading.value = true
14
+ try {
15
+ pipelines.value = await getAvailablePipelines()
16
+ }
17
+ catch {
18
+ toast.add({ title: t('hr.toast.error'), color: 'error' })
19
+ }
20
+ finally {
21
+ loading.value = false
22
+ }
23
+ }
24
+
25
+ function openWorkflowEditor(id: string) {
26
+ router.push(`/workflow/definitions/${id}`)
27
+ }
28
+
29
+ function createNewPipeline() {
30
+ router.push('/workflow/definitions/+')
31
+ }
32
+
33
+ onMounted(fetchPipelines)
34
+ </script>
35
+
36
+ <template>
37
+ <div class="space-y-5">
38
+ <div class="flex items-center justify-between">
39
+ <div>
40
+ <div class="text-sm font-semibold text-default">
41
+ {{ t('hr.settings.recruitment.title') }}
42
+ </div>
43
+ <p class="mt-0.5 text-xs text-muted">
44
+ {{ t('hr.settings.recruitment.description') }}
45
+ </p>
46
+ </div>
47
+ <UButton
48
+ color="primary"
49
+ size="sm"
50
+ icon="i-ph-plus-light"
51
+ :label="t('hr.settings.recruitment.create_pipeline')"
52
+ @click="createNewPipeline"
53
+ />
54
+ </div>
55
+
56
+ <!-- Loading -->
57
+ <div
58
+ v-if="loading"
59
+ class="p-6 text-sm text-muted text-center"
60
+ >
61
+ {{ t('common.loading') }}
62
+ </div>
63
+
64
+ <!-- Empty state -->
65
+ <div
66
+ v-else-if="!pipelines.length"
67
+ class="rounded-xl border border-dashed border-default bg-elevated/30 p-8 text-center"
68
+ >
69
+ <UIcon
70
+ name="i-ph-flow-arrow-light"
71
+ class="size-8 text-muted mx-auto mb-2"
72
+ />
73
+ <p class="text-sm font-medium text-default">
74
+ {{ t('hr.settings.recruitment.no_pipelines') }}
75
+ </p>
76
+ <p class="mt-1 text-xs text-muted">
77
+ {{ t('hr.settings.recruitment.no_pipelines_hint') }}
78
+ </p>
79
+ </div>
80
+
81
+ <!-- Pipeline list -->
82
+ <ul
83
+ v-else
84
+ class="space-y-2"
85
+ >
86
+ <li
87
+ v-for="pipeline in pipelines"
88
+ :key="pipeline.id"
89
+ class="flex items-center gap-3 rounded-lg border border-default bg-default p-4 cursor-pointer hover:bg-elevated/50 transition-colors"
90
+ @click="openWorkflowEditor(pipeline.id)"
91
+ >
92
+ <UIcon
93
+ name="i-ph-flow-arrow-light"
94
+ class="size-5 text-muted shrink-0"
95
+ />
96
+ <div class="flex-1 min-w-0">
97
+ <div class="flex items-center gap-2">
98
+ <span class="text-sm font-medium text-default truncate">{{ pipeline.name }}</span>
99
+ </div>
100
+ <p
101
+ v-if="pipeline.description"
102
+ class="text-xs text-muted mt-0.5 truncate"
103
+ >
104
+ {{ pipeline.description }}
105
+ </p>
106
+ </div>
107
+ <div class="flex items-center gap-2 shrink-0">
108
+ <UBadge
109
+ :label="`${pipeline.steps.length} step${pipeline.steps.length === 1 ? '' : 's'}`"
110
+ color="neutral"
111
+ variant="subtle"
112
+ size="sm"
113
+ />
114
+ <UIcon
115
+ name="i-ph-arrow-square-out-light"
116
+ class="size-4 text-muted"
117
+ />
118
+ </div>
119
+ </li>
120
+ </ul>
121
+
122
+ <div class="pt-2">
123
+ <p class="text-xs text-muted">
124
+ {{ t('hr.settings.recruitment.workflow_hint') }}
125
+ </p>
126
+ </div>
127
+ </div>
128
+ </template>
@@ -0,0 +1,170 @@
1
+ <script lang="ts" setup>
2
+ import slugify from "slugify"
3
+
4
+ const {
5
+ leaveTypes, addLeaveType, updateLeaveType, archiveLeaveType,
6
+ employmentTypes, addEmploymentType, updateEmploymentType, archiveEmploymentType,
7
+ } = useHrSettings()
8
+
9
+ const leaveDraft = ref({ label: "", default_days: 0, paid: true })
10
+ const employmentDraft = ref({ label: "" })
11
+
12
+ function addLeave() {
13
+ if (!leaveDraft.value.label.trim()) return
14
+ const key = slugify(leaveDraft.value.label, { lower: true, strict: true })
15
+ addLeaveType({ key, label: leaveDraft.value.label.trim(), default_days: leaveDraft.value.default_days, paid: leaveDraft.value.paid })
16
+ leaveDraft.value = { label: "", default_days: 0, paid: true }
17
+ }
18
+
19
+ function addEmployment() {
20
+ const label = employmentDraft.value.label.trim()
21
+ if (!label) return
22
+ const key = slugify(label, { lower: true, strict: true, replacement: "_" })
23
+ addEmploymentType({ key, label })
24
+ employmentDraft.value.label = ""
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <div class="space-y-6">
30
+ <!-- Leave types -->
31
+ <section class="space-y-3">
32
+ <div class="rounded-xl border border-dashed border-default bg-elevated/30 p-4">
33
+ <div class="text-sm font-semibold text-default">
34
+ {{ $t('taxonomies.add_leave_type') }}
35
+ </div>
36
+ <p class="mt-0.5 text-xs text-muted">
37
+ {{ $t('taxonomies.leave_type_desc') }}
38
+ </p>
39
+ <div class="mt-3 flex flex-wrap items-center gap-2">
40
+ <UInput
41
+ v-model="leaveDraft.label"
42
+ size="sm"
43
+ :placeholder="$t('taxonomies.leave_type_placeholder')"
44
+ maxlength="40"
45
+ class="flex-1 min-w-[160px]"
46
+ />
47
+ <UInputNumber
48
+ v-model="leaveDraft.default_days"
49
+ :min="0"
50
+ size="sm"
51
+ class="w-28"
52
+ />
53
+ <UCheckbox
54
+ v-model="leaveDraft.paid"
55
+ :label="$t('taxonomies.leave_type.paid')"
56
+ />
57
+ <UButton
58
+ color="primary"
59
+ size="sm"
60
+ icon="i-ph-plus-light"
61
+ :label="$t('common.add')"
62
+ :disabled="!leaveDraft.label.trim()"
63
+ @click="addLeave"
64
+ />
65
+ </div>
66
+ </div>
67
+
68
+ <ul class="space-y-2">
69
+ <li
70
+ v-for="t in leaveTypes"
71
+ :key="t.key"
72
+ class="flex flex-wrap items-center gap-3 rounded-lg border border-default bg-default p-3"
73
+ :class="t.is_archived && 'opacity-50'"
74
+ >
75
+ <UIcon
76
+ name="i-ph-calendar-blank-light"
77
+ class="size-4 text-muted shrink-0"
78
+ />
79
+ <div class="flex-1 min-w-[120px]">
80
+ <UInput
81
+ :model-value="t.label"
82
+ size="sm"
83
+ @update:model-value="(v) => updateLeaveType(t.id, { label: String(v) })"
84
+ />
85
+ </div>
86
+ <UInputNumber
87
+ :model-value="t.default_days"
88
+ :min="0"
89
+ size="sm"
90
+ class="w-24"
91
+ @update:model-value="(v) => updateLeaveType(t.id, { default_days: Number(v) })"
92
+ />
93
+ <UCheckbox
94
+ :model-value="t.paid"
95
+ :label="$t('taxonomies.leave_type.paid')"
96
+ @update:model-value="(v) => updateLeaveType(t.id, { paid: !!v })"
97
+ />
98
+ <code class="text-xs text-muted hidden sm:inline">{{ t.key }}</code>
99
+ <UButton
100
+ size="xs"
101
+ color="neutral"
102
+ variant="ghost"
103
+ icon="i-ph-archive-light"
104
+ :disabled="t.is_archived"
105
+ @click="archiveLeaveType(t.id)"
106
+ />
107
+ </li>
108
+ </ul>
109
+ </section>
110
+
111
+ <!-- Employment types -->
112
+ <section class="space-y-3 border-t border-default pt-6">
113
+ <div class="rounded-xl border border-dashed border-default bg-elevated/30 p-4">
114
+ <div class="text-sm font-semibold text-default">
115
+ {{ $t('taxonomies.add_employment_type') }}
116
+ </div>
117
+ <p class="mt-0.5 text-xs text-muted">
118
+ {{ $t('taxonomies.employment_type_desc') }}
119
+ </p>
120
+ <div class="mt-3 flex flex-wrap items-center gap-2">
121
+ <UInput
122
+ v-model="employmentDraft.label"
123
+ size="sm"
124
+ :placeholder="$t('taxonomies.employment_type_placeholder')"
125
+ maxlength="40"
126
+ class="flex-1 min-w-[200px]"
127
+ />
128
+ <UButton
129
+ color="primary"
130
+ size="sm"
131
+ icon="i-ph-plus-light"
132
+ :label="$t('common.add')"
133
+ :disabled="!employmentDraft.label.trim()"
134
+ @click="addEmployment"
135
+ />
136
+ </div>
137
+ </div>
138
+
139
+ <ul class="space-y-2">
140
+ <li
141
+ v-for="t in employmentTypes"
142
+ :key="t.key"
143
+ class="flex flex-wrap items-center gap-3 rounded-lg border border-default bg-default p-3"
144
+ :class="t.is_archived && 'opacity-50'"
145
+ >
146
+ <UIcon
147
+ name="i-ph-briefcase-light"
148
+ class="size-4 text-muted shrink-0"
149
+ />
150
+ <div class="flex-1 min-w-[200px]">
151
+ <UInput
152
+ :model-value="t.label"
153
+ size="sm"
154
+ @update:model-value="(v) => updateEmploymentType(t.id, { label: String(v) })"
155
+ />
156
+ </div>
157
+ <code class="text-xs text-muted hidden sm:inline">{{ t.key }}</code>
158
+ <UButton
159
+ size="xs"
160
+ color="neutral"
161
+ variant="ghost"
162
+ icon="i-ph-archive-light"
163
+ :disabled="t.is_archived"
164
+ @click="archiveEmploymentType(t.id)"
165
+ />
166
+ </li>
167
+ </ul>
168
+ </section>
169
+ </div>
170
+ </template>
@@ -0,0 +1,21 @@
1
+ <script lang="ts" setup>
2
+ defineProps<{
3
+ label: string
4
+ value?: string | number | null
5
+ icon?: string
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div class="flex items-center px-4 py-2.5">
11
+ <UIcon
12
+ v-if="icon"
13
+ :name="icon"
14
+ class="size-4 text-muted mr-3 shrink-0"
15
+ />
16
+ <span class="text-xs text-muted w-32 shrink-0">{{ label }}</span>
17
+ <div class="text-sm text-default truncate">
18
+ <slot>{{ value || '—' }}</slot>
19
+ </div>
20
+ </div>
21
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts" setup>
2
+ defineProps<{ title: string; icon: string }>();
3
+ </script>
4
+
5
+ <template>
6
+ <div class="border border-default rounded-lg">
7
+ <div
8
+ class="px-4 py-3 border-b border-default bg-elevated/30 flex items-center justify-between"
9
+ >
10
+ <h3 class="text-sm font-medium flex items-center gap-2">
11
+ <UIcon :name="icon" class="size-4 text-muted" />
12
+ {{ title }}
13
+ </h3>
14
+ <slot name="actions" />
15
+ </div>
16
+ <div class="divide-y divide-default p-4">
17
+ <slot />
18
+ </div>
19
+ </div>
20
+ </template>
@@ -0,0 +1,42 @@
1
+ <script lang="ts" setup>
2
+ const props = defineProps<{
3
+ source: "policy" | "override"
4
+ policyLabel?: string
5
+ policyValue?: string | number | boolean
6
+ overrideNote?: string
7
+ }>()
8
+
9
+ const { t } = useI18n()
10
+
11
+ const tooltip = computed(() => {
12
+ if (props.source === 'policy') {
13
+ return props.policyLabel
14
+ ? `${t('policy.source.policy')}: ${props.policyLabel}`
15
+ : t('policy.source.policy')
16
+ }
17
+ if (props.policyValue !== undefined) {
18
+ return t('policy.source.override_value', {
19
+ override: props.overrideNote ?? '?',
20
+ policy: props.policyValue,
21
+ })
22
+ }
23
+ return t('policy.source.override')
24
+ })
25
+ </script>
26
+
27
+ <template>
28
+ <UTooltip :text="tooltip">
29
+ <UBadge
30
+ :color="source === 'override' ? 'warning' : 'neutral'"
31
+ variant="subtle"
32
+ size="md"
33
+ >
34
+ {{ source === 'override' ? $t('hr.policy.override') : $t('hr.policy.from_policy') }}
35
+ <UIcon
36
+ v-if="source === 'override'"
37
+ name="i-ph-pencil-simple-line-light"
38
+ class="size-3 ml-1"
39
+ />
40
+ </UBadge>
41
+ </UTooltip>
42
+ </template>
@@ -0,0 +1,24 @@
1
+ <script lang="ts" setup>
2
+ const props = defineProps<{ stage: LifecycleStage }>()
3
+
4
+ const COLORS: Record<LifecycleStage, "neutral" | "info" | "warning" | "success" | "error"> = {
5
+ talent: "neutral",
6
+ interviewing: "info",
7
+ offer: "warning",
8
+ probation: "warning",
9
+ active: "success",
10
+ resigned: "neutral",
11
+ }
12
+
13
+ const variant = computed(() => COLORS[props.stage] ?? "neutral")
14
+ </script>
15
+
16
+ <template>
17
+ <UBadge
18
+ :color="variant"
19
+ variant="subtle"
20
+ size="md"
21
+ >
22
+ {{ stage }}
23
+ </UBadge>
24
+ </template>
@@ -0,0 +1,27 @@
1
+ <script lang="ts" setup>
2
+ import type { WorkflowAction } from '@odp/workflow/shared/types'
3
+
4
+ defineProps<{ actions: WorkflowAction[] }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div v-if="actions.length" class="pt-2">
9
+ <p class="text-xs text-muted font-medium uppercase tracking-wide mb-2">
10
+ {{ $t("hr.recruitment.timeline") }}
11
+ </p>
12
+ <div class="space-y-1.5">
13
+ <div v-for="action in actions" :key="action.id" class="flex items-start gap-2 text-xs">
14
+ <UBadge
15
+ :color="action.action === 'approve' ? 'success' : action.action === 'reject' ? 'error' : 'neutral'"
16
+ variant="subtle" size="xs"
17
+ >
18
+ {{ action.action }}
19
+ </UBadge>
20
+ <span v-if="action.comment" class="text-muted truncate">{{ action.comment }}</span>
21
+ <span class="text-muted ml-auto shrink-0">
22
+ {{ action.created_at?.slice(0, 16).replace('T', ' ') }}
23
+ </span>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </template>
@@ -0,0 +1,54 @@
1
+ <script lang="ts" setup>
2
+ defineProps<{
3
+ applications: Array<{ id: number; position: string | null; status: string; workflow_instance_id: string | null; date_created: string }>
4
+ selectedAppId: number | null
5
+ loading: boolean
6
+ }>()
7
+
8
+ const emit = defineEmits<{ select: [id: number]; create: [] }>()
9
+
10
+ const STATUS_COLOR: Record<string, string> = {
11
+ active: "info",
12
+ hired: "success",
13
+ rejected: "error",
14
+ withdrawn: "neutral",
15
+ }
16
+ </script>
17
+
18
+ <template>
19
+ <div class="w-56 shrink-0 border-r border-default p-4 space-y-3 overflow-y-auto">
20
+ <div class="flex items-center justify-between">
21
+ <p class="text-xs text-muted font-medium uppercase tracking-wide">
22
+ {{ $t("hr.recruitment.applications") }}
23
+ </p>
24
+ <UButton icon="i-ph-plus" size="2xs" variant="ghost" @click="emit('create')" />
25
+ </div>
26
+
27
+ <div v-if="loading" class="text-xs text-muted py-4 text-center">{{ $t("common.loading") }}</div>
28
+
29
+ <button
30
+ v-for="app in applications"
31
+ :key="app.id"
32
+ class="w-full text-left p-2.5 rounded-lg border transition-colors"
33
+ :class="selectedAppId === app.id ? 'border-primary bg-primary/5' : 'border-default hover:bg-elevated'"
34
+ @click="emit('select', app.id)"
35
+ >
36
+ <div class="text-xs font-medium truncate">
37
+ {{ app.position ?? $t("hr.recruitment.no_position") }}
38
+ </div>
39
+ <div class="flex items-center gap-1 mt-1">
40
+ <UBadge :color="(STATUS_COLOR[app.status] ?? 'neutral') as any" variant="subtle" size="xs">
41
+ {{ $t(`hr.recruitment.app_status.${app.status}`) }}
42
+ </UBadge>
43
+ <UBadge v-if="app.workflow_instance_id" color="primary" variant="subtle" size="xs">
44
+ {{ $t("hr.recruitment.has_pipeline") }}
45
+ </UBadge>
46
+ </div>
47
+ <div class="text-xs text-muted mt-0.5">{{ app.date_created?.slice(0, 10) }}</div>
48
+ </button>
49
+
50
+ <div v-if="!loading && !applications.length" class="text-xs text-muted text-center py-4">
51
+ {{ $t("hr.recruitment.no_applications") }}
52
+ </div>
53
+ </div>
54
+ </template>
@@ -0,0 +1,114 @@
1
+ <script lang="ts" setup>
2
+ import type { RecruitmentPipeline } from '../../../composables/use-recruitment-workflow'
3
+
4
+ const open = defineModel<boolean>('open', { default: false })
5
+ const props = defineProps<{
6
+ personId: string | number
7
+ pipelines: RecruitmentPipeline[]
8
+ }>()
9
+ const emit = defineEmits<{
10
+ saved: [app: any, workflowId: string | null]
11
+ }>()
12
+
13
+ const { $api } = useNuxtApp()
14
+ const { t } = useI18n()
15
+ const toast = useToast()
16
+
17
+ const APP_FIELD_NAMES = ['position', 'notes']
18
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(['hr_applications'])
19
+
20
+ const formFields = computed(() => pickFields('hr_applications', APP_FIELD_NAMES))
21
+
22
+ const form = ref<Record<string, any>>({})
23
+ const selectedWorkflowId = ref<string | null>(null)
24
+ const saving = ref(false)
25
+
26
+ const pipelineOptions = computed(() =>
27
+ props.pipelines.map(p => ({
28
+ label: `${p.name} (${p.steps.length} steps)`,
29
+ value: p.id,
30
+ })),
31
+ )
32
+
33
+ const canSave = computed(() => !!form.value.position?.trim())
34
+
35
+ watch(open, (v) => {
36
+ if (v) {
37
+ form.value = {}
38
+ selectedWorkflowId.value = null
39
+ fetchAll()
40
+ }
41
+ })
42
+
43
+ async function save() {
44
+ if (!canSave.value) return
45
+ saving.value = true
46
+ try {
47
+ const res = await $api<{ data: any }>(
48
+ `/hr/people/${props.personId}/applications`,
49
+ { method: 'POST', body: { ...form.value } },
50
+ )
51
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
52
+ open.value = false
53
+ emit('saved', res.data, selectedWorkflowId.value)
54
+ }
55
+ catch {
56
+ toast.add({ title: t('hr.toast.error'), color: 'error' })
57
+ }
58
+ finally {
59
+ saving.value = false
60
+ }
61
+ }
62
+ </script>
63
+
64
+ <template>
65
+ <UModal v-model:open="open">
66
+ <template #content>
67
+ <div class="p-6 space-y-4">
68
+ <h3 class="text-base font-semibold">
69
+ {{ $t('hr.recruitment.create_application') }}
70
+ </h3>
71
+
72
+ <div v-if="!isReady('hr_applications')" class="py-6 text-sm text-muted text-center">
73
+ {{ $t('common.loading') }}
74
+ </div>
75
+ <template v-else>
76
+ <FormRoot
77
+ v-if="formFields.length"
78
+ v-model="form"
79
+ :fields="formFields"
80
+ :initial-values="{}"
81
+ />
82
+
83
+ <UFormField
84
+ v-if="pipelineOptions.length"
85
+ :label="$t('hr.recruitment.pipeline_label')"
86
+ >
87
+ <USelect
88
+ v-model="selectedWorkflowId"
89
+ :items="pipelineOptions"
90
+ value-key="value"
91
+ :placeholder="$t('hr.recruitment.select_pipeline')"
92
+ size="sm"
93
+ class="w-full"
94
+ />
95
+ <template #hint>
96
+ <span class="text-xs text-muted">{{ $t('hr.recruitment.pipeline_hint') }}</span>
97
+ </template>
98
+ </UFormField>
99
+ </template>
100
+
101
+ <div class="flex justify-end gap-2 pt-2">
102
+ <UButton :label="$t('common.cancel')" variant="ghost" @click="open = false" />
103
+ <UButton
104
+ :label="$t('common.create')"
105
+ color="primary"
106
+ :disabled="!canSave"
107
+ :loading="saving"
108
+ @click="save"
109
+ />
110
+ </div>
111
+ </div>
112
+ </template>
113
+ </UModal>
114
+ </template>
@@ -0,0 +1,56 @@
1
+ <script lang="ts" setup>
2
+ import type { RecruitmentPipeline } from "../../../composables/use-recruitment-workflow";
3
+
4
+ const props = defineProps<{ pipelines: RecruitmentPipeline[] }>();
5
+ const emit = defineEmits<{ start: [workflowId: string] }>();
6
+
7
+ const selectedId = ref<string | null>(null);
8
+
9
+ const options = computed(() =>
10
+ props.pipelines.map((p) => ({
11
+ label: `${p.name} (${p.steps.length} steps)`,
12
+ value: p.id,
13
+ })),
14
+ );
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ class="p-4 rounded-xl border border-dashed border-default bg-elevated/30 text-center"
20
+ >
21
+ <UIcon
22
+ name="i-ph-flow-arrow-light"
23
+ class="size-8 text-muted mx-auto mb-2"
24
+ />
25
+ <p class="text-sm font-medium mb-1">
26
+ {{ $t("hr.recruitment.no_pipeline") }}
27
+ </p>
28
+ <p class="text-xs text-muted mb-3">
29
+ {{ $t("hr.recruitment.no_pipeline_hint") }}
30
+ </p>
31
+ <div
32
+ v-if="options.length"
33
+ class="flex items-center justify-center gap-2"
34
+ >
35
+ <USelect
36
+ v-model="selectedId"
37
+ :items="options"
38
+ value-key="value"
39
+ :placeholder="$t('hr.recruitment.select_pipeline')"
40
+ size="sm"
41
+ class="w-64"
42
+ />
43
+ <UButton
44
+ size="sm"
45
+ color="primary"
46
+ icon="i-ph-play-light"
47
+ :label="$t('hr.recruitment.start_pipeline')"
48
+ :disabled="!selectedId"
49
+ @click="selectedId && emit('start', selectedId)"
50
+ />
51
+ </div>
52
+ <p v-else class="text-xs text-muted">
53
+ {{ $t("hr.recruitment.no_pipelines_available") }}
54
+ </p>
55
+ </div>
56
+ </template>