@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,164 @@
1
+ <script lang="ts" setup>
2
+ import HrInfoSection from "../shared/section.vue"
3
+ import HrPolicySourceBadge from "../shared/source-badge.vue"
4
+ import HrBenefitOverrideModal from "../settings/policy/benefit-override-modal.vue"
5
+
6
+ const props = defineProps<{ person: Person, readonly?: boolean }>()
7
+
8
+ const api = useHrApi()
9
+ const toast = useToast()
10
+ const { t } = useI18n()
11
+
12
+ const data = ref<Record<string, any>>({})
13
+ const edits = ref<Record<string, any>>({})
14
+ const editing = ref(false)
15
+ const formModel = computed({
16
+ get: () => editing.value ? edits.value : data.value,
17
+ set: (val) => { edits.value = val },
18
+ })
19
+ const loading = ref(false)
20
+ const saving = ref(false)
21
+
22
+ async function load() {
23
+ loading.value = true
24
+ try { data.value = (await api.getInsurance(props.person.id)) ?? {} }
25
+ finally { loading.value = false }
26
+ }
27
+
28
+ onMounted(() => Promise.all([load(), fetchFields()]))
29
+
30
+ function startEdit() {
31
+ edits.value = { ...data.value }
32
+ editing.value = true
33
+ }
34
+
35
+ function cancelEdit() {
36
+ edits.value = {}
37
+ editing.value = false
38
+ }
39
+
40
+ async function save() {
41
+ saving.value = true
42
+ try {
43
+ await api.updateInsurance(props.person.id, edits.value)
44
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
45
+ editing.value = false
46
+ await load()
47
+ }
48
+ catch { toast.add({ title: t('hr.toast.error'), color: 'error' }) }
49
+ finally { saving.value = false }
50
+ }
51
+
52
+ const healthEligible = computed(() => props.person.benefits_eligibility?.find((b: any) => b.key === 'health_insurance'))
53
+ const socialEligible = computed(() => props.person.benefits_eligibility?.find((b: any) => b.key === 'social_insurance'))
54
+
55
+ type BenefitKind = 'health' | 'social'
56
+ const overrideKind = ref<BenefitKind | null>(null)
57
+ const overrideEntry = computed(() =>
58
+ overrideKind.value === 'health' ? healthEligible.value
59
+ : overrideKind.value === 'social' ? socialEligible.value : null)
60
+ const overrideLabel = computed(() =>
61
+ overrideKind.value === 'health' ? t('hr.insurance.health')
62
+ : overrideKind.value === 'social' ? t('hr.insurance.social') : '')
63
+
64
+ function openOverride(kind: BenefitKind) { overrideKind.value = kind }
65
+
66
+ async function confirmOverride(overrideData: { eligible: boolean, note: string }) {
67
+ if (!overrideKind.value) return
68
+ const overrideField = overrideKind.value === 'health' ? 'override_health_insurance' : 'override_social_insurance'
69
+ const reasonField = overrideKind.value === 'health' ? 'override_health_reason' : 'override_social_reason'
70
+ try {
71
+ await api.updateInsurance(props.person.id, { [overrideField]: overrideData.eligible, [reasonField]: overrideData.note })
72
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
73
+ }
74
+ catch { toast.add({ title: t('hr.toast.error'), color: 'error' }) }
75
+ overrideKind.value = null
76
+ }
77
+
78
+ async function reset(kind: BenefitKind) {
79
+ const overrideField = kind === 'health' ? 'override_health_insurance' : 'override_social_insurance'
80
+ try {
81
+ await api.updateInsurance(props.person.id, { [overrideField]: null })
82
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
83
+ }
84
+ catch { toast.add({ title: t('hr.toast.error'), color: 'error' }) }
85
+ }
86
+
87
+ const INSURANCE_FIELD_NAMES = [
88
+ 'social_insurance_number', 'health_insurance_number', 'insurance_base_salary',
89
+ 'tax_code', 'employee_contribution', 'employer_contribution', 'tax_dependents',
90
+ ]
91
+ const { pickFields, fetchAll: fetchFields, isReady } = useHrFieldRegistry(['hr_insurances'])
92
+
93
+ const insuranceFields = computed(() => pickFields('hr_insurances', INSURANCE_FIELD_NAMES))
94
+ </script>
95
+
96
+ <template>
97
+ <div class="space-y-4">
98
+ <!-- Eligibility badges -->
99
+ <HrInfoSection :title="$t('hr.insurance.eligibility')" icon="i-ph-shield-check-light">
100
+ <div class="p-4 flex items-center gap-3 flex-wrap text-sm">
101
+ <div class="flex items-center gap-1">
102
+ <UBadge :color="healthEligible?.eligible ? 'success' : 'neutral'" variant="subtle" size="md">
103
+ {{ $t('hr.insurance.health') }}: {{ healthEligible?.eligible ? $t('hr.insurance.eligible') : $t('hr.insurance.not_eligible') }}
104
+ </UBadge>
105
+ <HrPolicySourceBadge v-if="healthEligible" :source="healthEligible.source"
106
+ :policy-label="$t('hr.insurance.health_eligibility')"
107
+ :policy-value="healthEligible.policy_value ? $t('hr.insurance.eligible') : $t('hr.insurance.not_eligible')"
108
+ :override-note="healthEligible.override_note" />
109
+ <UDropdownMenu v-if="!readonly" :items="[
110
+ [{ label: $t('hr.insurance.override.title'), icon: 'i-ph-pencil-simple-line-light', onSelect: () => openOverride('health') }],
111
+ healthEligible?.source === 'override' ? [{ label: $t('hr.insurance.override.reset'), icon: 'i-ph-arrow-counter-clockwise-light', onSelect: () => reset('health') }] : []
112
+ ]">
113
+ <UButton variant="ghost" size="xs" icon="i-ph-dots-three-vertical-light" />
114
+ </UDropdownMenu>
115
+ </div>
116
+ <div class="flex items-center gap-1">
117
+ <UBadge :color="socialEligible?.eligible ? 'success' : 'neutral'" variant="subtle" size="md">
118
+ {{ $t('hr.insurance.social') }}: {{ socialEligible?.eligible ? $t('hr.insurance.eligible') : $t('hr.insurance.not_eligible') }}
119
+ </UBadge>
120
+ <HrPolicySourceBadge v-if="socialEligible" :source="socialEligible.source"
121
+ :policy-label="$t('hr.insurance.social_eligibility')"
122
+ :policy-value="socialEligible.policy_value ? $t('hr.insurance.eligible') : $t('hr.insurance.not_eligible')"
123
+ :override-note="socialEligible.override_note" />
124
+ <UDropdownMenu v-if="!readonly" :items="[
125
+ [{ label: $t('hr.insurance.override.title'), icon: 'i-ph-pencil-simple-line-light', onSelect: () => openOverride('social') }],
126
+ socialEligible?.source === 'override' ? [{ label: $t('hr.insurance.override.reset'), icon: 'i-ph-arrow-counter-clockwise-light', onSelect: () => reset('social') }] : []
127
+ ]">
128
+ <UButton variant="ghost" size="xs" icon="i-ph-dots-three-vertical-light" />
129
+ </UDropdownMenu>
130
+ </div>
131
+ </div>
132
+ </HrInfoSection>
133
+
134
+ <!-- Insurance details — inline FormRoot, disabled by default -->
135
+ <HrInfoSection :title="$t('hr.insurance.title')" icon="i-ph-shield-light">
136
+ <template #actions>
137
+ <div v-if="!readonly" class="flex items-center gap-2">
138
+ <template v-if="editing">
139
+ <UButton size="xs" variant="ghost" :label="$t('common.cancel')" @click="cancelEdit" />
140
+ <UButton size="xs" color="primary" :label="$t('common.save')" :loading="saving" @click="save" />
141
+ </template>
142
+ <UButton v-else size="xs" variant="soft" icon="i-ph-pencil-simple-light" :label="$t('common.edit')" @click="startEdit" />
143
+ </div>
144
+ </template>
145
+
146
+ <div v-if="loading || !isReady('hr_insurances')" class="px-4 py-6 text-sm text-muted">{{ $t('common.loading') }}</div>
147
+ <FormRoot
148
+ v-else-if="insuranceFields.length"
149
+ v-model="formModel"
150
+ :fields="insuranceFields"
151
+ :initial-values="data"
152
+ :disabled="!editing || readonly"
153
+ />
154
+ </HrInfoSection>
155
+
156
+ <HrBenefitOverrideModal
157
+ :open="overrideKind !== null"
158
+ :entry="overrideEntry ?? null"
159
+ :label="overrideLabel"
160
+ @confirm="confirmOverride"
161
+ @cancel="overrideKind = null"
162
+ />
163
+ </div>
164
+ </template>
@@ -0,0 +1,180 @@
1
+ <script lang="ts" setup>
2
+ const props = defineProps<{ person: Person, readonly?: boolean }>()
3
+
4
+ const { t } = useI18n()
5
+ const { listLeaveBalances, updateLeaveBalance } = useHrApi()
6
+
7
+ const balances = ref<LeaveBalance[]>([])
8
+ const loading = ref(false)
9
+ const overrideEntry = ref<LeaveBalance | null>(null)
10
+ const showRequestModal = ref(false)
11
+
12
+ async function loadBalances() {
13
+ loading.value = true
14
+ try {
15
+ const data = await listLeaveBalances(props.person.id)
16
+ balances.value = (Array.isArray(data) ? data : []).map(b => ({
17
+ ...b,
18
+ total: displayTotal(b),
19
+ remaining: displayRemaining(b),
20
+ source: sourceType(b),
21
+ }))
22
+ }
23
+ finally {
24
+ loading.value = false
25
+ }
26
+ }
27
+
28
+ function displayTotal(b: LeaveBalance): number {
29
+ return b.override_total != null ? b.override_total : (b.policy_total ?? 0)
30
+ }
31
+
32
+ function displayRemaining(b: LeaveBalance): number {
33
+ return Math.max(0, displayTotal(b) - (b.used ?? 0))
34
+ }
35
+
36
+ function sourceType(b: LeaveBalance): 'policy' | 'override' {
37
+ return b.override_total != null ? 'override' : 'policy'
38
+ }
39
+
40
+ function openOverride(entry: LeaveBalance) {
41
+ overrideEntry.value = entry
42
+ }
43
+
44
+ async function confirmOverride(data: { total: number, note: string }) {
45
+ if (!overrideEntry.value) return
46
+ await updateLeaveBalance(props.person.id, overrideEntry.value.id, {
47
+ override_total: data.total,
48
+ override_reason: data.note,
49
+ })
50
+ overrideEntry.value = null
51
+ await loadBalances()
52
+ }
53
+
54
+ async function reset(entry: LeaveBalance) {
55
+ await updateLeaveBalance(props.person.id, entry.id, { action: 'reset' })
56
+ await loadBalances()
57
+ }
58
+
59
+ onMounted(loadBalances)
60
+ watch(() => props.person.id, loadBalances)
61
+ </script>
62
+
63
+ <template>
64
+ <div class="p-6 max-w-4xl space-y-6">
65
+ <HrInfoSection
66
+ :title="$t('hr.leave.balance.type')"
67
+ icon="i-ph-calendar-blank-light"
68
+ >
69
+ <div
70
+ v-if="loading"
71
+ class="p-6 text-muted text-sm"
72
+ >
73
+ {{ $t('common.loading') }}
74
+ </div>
75
+ <div
76
+ v-else-if="!balances.length"
77
+ class="p-6 text-muted text-sm"
78
+ >
79
+ {{ $t('common.no_data') }}
80
+ </div>
81
+ <table
82
+ v-else
83
+ class="w-full text-sm"
84
+ >
85
+ <thead>
86
+ <tr class="text-left text-xs text-muted uppercase tracking-wider">
87
+ <th class="px-4 py-2.5 font-medium">
88
+ {{ $t('hr.leave.balance.type') }}
89
+ </th>
90
+ <th class="px-4 py-2.5 font-medium text-right">
91
+ {{ $t('hr.leave.balance.total') }}
92
+ </th>
93
+ <th class="px-4 py-2.5 font-medium text-right">
94
+ {{ $t('hr.leave.balance.used') }}
95
+ </th>
96
+ <th class="px-4 py-2.5 font-medium text-right">
97
+ {{ $t('hr.leave.balance.remaining') }}
98
+ </th>
99
+ <th class="px-4 py-2.5 font-medium">
100
+ {{ $t('hr.leave.balance.source') }}
101
+ </th>
102
+ <th class="px-4 py-2.5 font-medium w-10" />
103
+ </tr>
104
+ </thead>
105
+ <tbody class="divide-y divide-default">
106
+ <tr
107
+ v-for="lb in balances"
108
+ :key="lb.id"
109
+ class="hover:bg-elevated/30"
110
+ >
111
+ <td class="px-4 py-3 capitalize font-medium">
112
+ {{ typeof lb.leave_type_id === 'object' ? (lb.leave_type_id as any).name : lb.type ?? lb.leave_type_id }}
113
+ </td>
114
+ <td class="px-4 py-3 text-right">
115
+ {{ displayTotal(lb) }}
116
+ </td>
117
+ <td class="px-4 py-3 text-right text-muted">
118
+ {{ lb.used ?? 0 }}
119
+ </td>
120
+ <td
121
+ class="px-4 py-3 text-right"
122
+ :class="displayRemaining(lb) === 0 ? 'text-error font-medium' : ''"
123
+ >
124
+ {{ displayRemaining(lb) }}
125
+ </td>
126
+ <td class="px-4 py-3">
127
+ <HrPolicySourceBadge
128
+ :source="sourceType(lb)"
129
+ :policy-label="typeof lb.leave_type_id === 'object' ? (lb.leave_type_id as any).name : undefined"
130
+ :policy-value="lb.policy_total"
131
+ :override-note="lb.override_reason ?? undefined"
132
+ />
133
+ </td>
134
+ <td class="px-4 py-3 text-right">
135
+ <UDropdownMenu
136
+ v-if="!readonly"
137
+ :items="[
138
+ [{ label: $t('hr.leave.override.title'), icon: 'i-ph-pencil-simple-line-light', onSelect: () => openOverride(lb) }],
139
+ sourceType(lb) === 'override' ? [{ label: $t('hr.leave.override.reset'), icon: 'i-ph-arrow-counter-clockwise-light', onSelect: () => reset(lb) }] : []
140
+ ]"
141
+ >
142
+ <UButton
143
+ variant="ghost"
144
+ size="xs"
145
+ icon="i-ph-dots-three-vertical-light"
146
+ />
147
+ </UDropdownMenu>
148
+ </td>
149
+ </tr>
150
+ </tbody>
151
+ </table>
152
+
153
+ <div
154
+ v-if="!readonly"
155
+ class="px-4 py-3 border-t border-default"
156
+ >
157
+ <UButton
158
+ icon="i-ph-plus"
159
+ size="sm"
160
+ variant="outline"
161
+ :label="$t('hr.leave.actions.create')"
162
+ @click="showRequestModal = true"
163
+ />
164
+ </div>
165
+ </HrInfoSection>
166
+
167
+ <HrLeaveOverrideModal
168
+ :open="overrideEntry !== null"
169
+ :entry="overrideEntry"
170
+ @confirm="confirmOverride"
171
+ @cancel="overrideEntry = null"
172
+ />
173
+
174
+ <HrLeaveRequestModal
175
+ v-model:open="showRequestModal"
176
+ :person-id="person.id"
177
+ @created="loadBalances"
178
+ />
179
+ </div>
180
+ </template>
@@ -0,0 +1,219 @@
1
+ <template>
2
+ <UModal
3
+ :open="open"
4
+ :title="provider ? $t('common.edit') : $t('integrations.add_provider')"
5
+ @update:open="!$event && $emit('cancel')"
6
+ >
7
+ <template #body>
8
+ <div class="space-y-4">
9
+ <UFormField :label="$t('hr.form.talent.display_name')" required>
10
+ <UInput
11
+ v-model="form.name"
12
+ class="w-full"
13
+ />
14
+ </UFormField>
15
+
16
+ <UFormField label="Type" required>
17
+ <USelect
18
+ v-model="form.type"
19
+ :options="typeOptions"
20
+ class="w-full"
21
+ />
22
+ </UFormField>
23
+
24
+ <!-- Config: URL (authentik / webhook) -->
25
+ <UFormField
26
+ v-if="form.type === 'authentik' || form.type === 'webhook'"
27
+ label="URL"
28
+ required
29
+ >
30
+ <UInput
31
+ v-model="form.config.url"
32
+ placeholder="https://"
33
+ class="w-full"
34
+ />
35
+ </UFormField>
36
+
37
+ <!-- Config: Token (authentik) -->
38
+ <UFormField
39
+ v-if="form.type === 'authentik'"
40
+ label="API Token"
41
+ >
42
+ <UInput
43
+ v-model="form.config.token"
44
+ type="password"
45
+ placeholder="Leave blank to keep existing"
46
+ class="w-full"
47
+ />
48
+ </UFormField>
49
+
50
+ <!-- Config: Domain (google / microsoft) -->
51
+ <UFormField
52
+ v-if="form.type === 'google_workspace' || form.type === 'microsoft_365'"
53
+ label="Domain"
54
+ required
55
+ >
56
+ <UInput
57
+ v-model="form.config.domain"
58
+ placeholder="company.com"
59
+ class="w-full"
60
+ />
61
+ </UFormField>
62
+
63
+ <!-- Config: Service Account JSON (google) -->
64
+ <UFormField
65
+ v-if="form.type === 'google_workspace'"
66
+ label="Service Account JSON"
67
+ >
68
+ <UTextarea
69
+ v-model="form.config.service_account"
70
+ placeholder="Paste JSON credentials here"
71
+ class="w-full font-mono text-xs"
72
+ :rows="4"
73
+ />
74
+ </UFormField>
75
+
76
+ <!-- Config: Webhook headers (webhook) -->
77
+ <UFormField
78
+ v-if="form.type === 'webhook'"
79
+ label="Authorization Header"
80
+ >
81
+ <UInput
82
+ v-model="form.config.headers"
83
+ placeholder="Bearer token123"
84
+ class="w-full"
85
+ />
86
+ </UFormField>
87
+
88
+ <UFormField label="Default Group / OU">
89
+ <UInput
90
+ v-model="form.default_group"
91
+ class="w-full"
92
+ />
93
+ </UFormField>
94
+
95
+ <!-- Actions -->
96
+ <div class="space-y-2">
97
+ <p class="text-sm font-medium text-default">
98
+ {{ $t('integrations.actions_configured') }}
99
+ </p>
100
+ <div class="flex flex-col gap-2">
101
+ <UCheckbox
102
+ v-model="form.actions.on_hire"
103
+ label="On hire (create account)"
104
+ />
105
+ <UCheckbox
106
+ v-model="form.actions.on_resign"
107
+ label="On resign (disable account)"
108
+ />
109
+ <UCheckbox
110
+ v-model="form.actions.on_reactivate"
111
+ label="On reactivate (re-enable account)"
112
+ />
113
+ </div>
114
+ </div>
115
+
116
+ <UFormField label="Enabled">
117
+ <UToggle v-model="form.enabled" />
118
+ </UFormField>
119
+ </div>
120
+ </template>
121
+
122
+ <template #footer>
123
+ <div class="flex justify-end gap-2">
124
+ <UButton
125
+ :label="$t('common.cancel')"
126
+ variant="ghost"
127
+ @click="$emit('cancel')"
128
+ />
129
+ <UButton
130
+ :label="$t('common.save')"
131
+ :loading="saving"
132
+ @click="submit"
133
+ />
134
+ </div>
135
+ </template>
136
+ </UModal>
137
+ </template>
138
+
139
+ <script lang="ts" setup>
140
+ const props = defineProps<{
141
+ open: boolean
142
+ provider?: HrProvider | null
143
+ }>()
144
+
145
+ const emit = defineEmits<{
146
+ saved: []
147
+ cancel: []
148
+ }>()
149
+
150
+ const { addProvider, updateProvider } = useProviders()
151
+ const toast = useToast()
152
+ const { t } = useI18n()
153
+
154
+ const typeOptions = [
155
+ { label: t('integrations.types.authentik'), value: 'authentik' },
156
+ // Google Workspace & Microsoft 365 provisioning are coming in a later phase — hidden until implemented.
157
+ { label: t('integrations.types.webhook'), value: 'webhook' },
158
+ ]
159
+
160
+ const saving = ref(false)
161
+
162
+ const defaultForm = () => ({
163
+ name: '',
164
+ type: 'authentik' as ProviderType,
165
+ config: {} as Record<string, string>,
166
+ enabled: true,
167
+ default_group: '',
168
+ actions: { on_hire: true, on_resign: true, on_reactivate: true },
169
+ })
170
+
171
+ const form = ref(defaultForm())
172
+
173
+ watch(() => props.open, (open) => {
174
+ if (open) {
175
+ if (props.provider) {
176
+ form.value = {
177
+ name: props.provider.name,
178
+ type: props.provider.type,
179
+ config: { ...(props.provider.config as Record<string, string>) },
180
+ enabled: props.provider.enabled,
181
+ default_group: props.provider.default_group ?? '',
182
+ actions: { ...props.provider.actions },
183
+ }
184
+ }
185
+ else {
186
+ form.value = defaultForm()
187
+ }
188
+ }
189
+ }, { immediate: true })
190
+
191
+ async function submit() {
192
+ if (!form.value.name || !form.value.type) return
193
+ saving.value = true
194
+ try {
195
+ const payload = {
196
+ name: form.value.name,
197
+ type: form.value.type,
198
+ config: form.value.config,
199
+ enabled: form.value.enabled,
200
+ default_group: form.value.default_group || null,
201
+ actions: form.value.actions,
202
+ }
203
+ if (props.provider) {
204
+ await updateProvider(props.provider.id, payload)
205
+ }
206
+ else {
207
+ await addProvider(payload)
208
+ }
209
+ emit('saved')
210
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
211
+ }
212
+ catch (err: any) {
213
+ toast.add({ title: err.message ?? t('hr.toast.error'), color: 'error' })
214
+ }
215
+ finally {
216
+ saving.value = false
217
+ }
218
+ }
219
+ </script>