@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,171 @@
1
+ <script lang="ts" setup>
2
+ import HrInfoSection from "../shared/section.vue"
3
+
4
+ const { t } = useI18n()
5
+ const { holidays, loading, fetchHolidays, createHoliday, deleteHoliday, copyFromPreviousYear } = useHolidays()
6
+ const toast = useToast()
7
+
8
+ const currentYear = new Date().getFullYear()
9
+ const year = ref(currentYear)
10
+
11
+ const HOLIDAY_FIELD_NAMES = ['name', 'date', 'recurring']
12
+ const { pickFields, fetchAll, isReady } = useHrFieldRegistry(['hr_company_holidays'])
13
+ const holidayFields = computed(() => pickFields('hr_company_holidays', HOLIDAY_FIELD_NAMES))
14
+
15
+ const form = ref<Record<string, any>>({ recurring: false })
16
+
17
+ const saving = ref(false)
18
+ const copying = ref(false)
19
+
20
+ const canAdd = computed(() =>
21
+ !!(form.value.name?.trim() && form.value.date),
22
+ )
23
+
24
+ const sortedHolidays = computed(() =>
25
+ [...holidays.value].sort((a, b) => (a.date || '').localeCompare(b.date || '')),
26
+ )
27
+
28
+ function fmt(iso?: string): string {
29
+ if (!iso) return '—'
30
+ const d = new Date(iso)
31
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
32
+ }
33
+
34
+ async function load() {
35
+ await fetchHolidays(year.value)
36
+ }
37
+
38
+ function changeYear(delta: number) {
39
+ year.value += delta
40
+ load()
41
+ }
42
+
43
+ async function addHoliday() {
44
+ if (!canAdd.value) return
45
+ saving.value = true
46
+ try {
47
+ await createHoliday({
48
+ name: form.value.name.trim(),
49
+ date: form.value.date,
50
+ year: new Date(form.value.date).getFullYear(),
51
+ recurring: !!form.value.recurring,
52
+ })
53
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
54
+ form.value = { recurring: false }
55
+ await load()
56
+ }
57
+ catch {
58
+ toast.add({ title: t('hr.toast.error'), color: 'error' })
59
+ }
60
+ finally {
61
+ saving.value = false
62
+ }
63
+ }
64
+
65
+ async function removeHoliday(id: number) {
66
+ try {
67
+ await deleteHoliday(id)
68
+ toast.add({ title: t('hr.toast.deleted'), color: 'success' })
69
+ }
70
+ catch {
71
+ toast.add({ title: t('hr.toast.error'), color: 'error' })
72
+ }
73
+ }
74
+
75
+ async function copyPrev() {
76
+ copying.value = true
77
+ try {
78
+ await copyFromPreviousYear(year.value)
79
+ toast.add({ title: t('hr.toast.saved'), color: 'success' })
80
+ }
81
+ catch {
82
+ toast.add({ title: t('hr.toast.error'), color: 'error' })
83
+ }
84
+ finally {
85
+ copying.value = false
86
+ }
87
+ }
88
+
89
+ onMounted(() => Promise.all([load(), fetchAll()]))
90
+ </script>
91
+
92
+ <template>
93
+ <div class="space-y-6">
94
+ <p class="text-sm text-muted">
95
+ Company-wide holidays. These appear alongside employee leave in the Leave calendar &amp; timeline.
96
+ </p>
97
+
98
+ <!-- Year nav + copy -->
99
+ <div class="flex items-center justify-between">
100
+ <div class="flex items-center gap-2">
101
+ <UButton icon="i-ph-caret-left-light" variant="ghost" size="sm" @click="changeYear(-1)" />
102
+ <span class="font-semibold text-base w-16 text-center">{{ year }}</span>
103
+ <UButton icon="i-ph-caret-right-light" variant="ghost" size="sm" @click="changeYear(1)" />
104
+ </div>
105
+ <UButton
106
+ icon="i-ph-copy-light"
107
+ variant="soft"
108
+ size="sm"
109
+ :loading="copying"
110
+ :label="`Copy from ${year - 1}`"
111
+ @click="copyPrev"
112
+ />
113
+ </div>
114
+
115
+ <!-- Add holiday -->
116
+ <HrInfoSection title="Add Holiday" icon="i-ph-calendar-plus-light">
117
+ <div class="p-4 space-y-4">
118
+ <div v-if="!isReady('hr_company_holidays')" class="py-2 text-sm text-muted">
119
+ {{ t('common.loading') }}
120
+ </div>
121
+ <FormRoot
122
+ v-else-if="holidayFields.length"
123
+ v-model="form"
124
+ :fields="holidayFields"
125
+ />
126
+ <div class="flex justify-end">
127
+ <UButton
128
+ icon="i-ph-plus-light"
129
+ :loading="saving"
130
+ :disabled="!canAdd"
131
+ label="Add Holiday"
132
+ @click="addHoliday"
133
+ />
134
+ </div>
135
+ </div>
136
+ </HrInfoSection>
137
+
138
+ <!-- List -->
139
+ <HrInfoSection title="Holidays" icon="i-ph-calendar-light">
140
+ <div v-if="loading" class="px-4 py-6 text-sm text-muted">{{ t('common.loading') }}</div>
141
+ <div v-else-if="sortedHolidays.length === 0" class="px-4 py-10 text-center text-sm text-muted">
142
+ No holidays for {{ year }}.
143
+ </div>
144
+ <ul v-else class="divide-y divide-default">
145
+ <li
146
+ v-for="h in sortedHolidays"
147
+ :key="h.id"
148
+ class="flex items-center gap-3 px-4 py-3"
149
+ >
150
+ <UIcon name="i-ph-calendar-dot-light" class="size-5 text-error shrink-0" />
151
+ <div class="flex-1 min-w-0">
152
+ <div class="flex items-center gap-2">
153
+ <span class="font-medium text-sm text-default truncate">{{ h.name }}</span>
154
+ <UBadge v-if="h.recurring" color="info" variant="subtle" size="sm">Annual</UBadge>
155
+ </div>
156
+ <div class="text-xs text-muted mt-0.5">
157
+ {{ fmt(h.date) }}
158
+ </div>
159
+ </div>
160
+ <UButton
161
+ icon="i-ph-trash-light"
162
+ color="error"
163
+ variant="ghost"
164
+ size="xs"
165
+ @click="removeHoliday(h.id)"
166
+ />
167
+ </li>
168
+ </ul>
169
+ </HrInfoSection>
170
+ </div>
171
+ </template>
@@ -0,0 +1,185 @@
1
+ <template>
2
+ <div class="space-y-6">
3
+ <div class="flex items-center justify-between">
4
+ <p class="text-sm text-muted max-w-xl">
5
+ {{ $t('integrations.description') }}
6
+ </p>
7
+ <UButton
8
+ icon="i-ph-plus-light"
9
+ :label="$t('integrations.add_provider')"
10
+ @click="openAdd"
11
+ />
12
+ </div>
13
+
14
+ <!-- Provider list -->
15
+ <div
16
+ v-if="providers.length"
17
+ class="space-y-3"
18
+ >
19
+ <div
20
+ v-for="p in providers"
21
+ :key="p.id"
22
+ class="border border-default rounded-lg p-4"
23
+ >
24
+ <div class="flex items-start gap-4">
25
+ <div class="flex-1 min-w-0">
26
+ <div class="flex items-center gap-2 flex-wrap">
27
+ <span class="font-medium text-default">{{ p.name }}</span>
28
+ <UBadge
29
+ :label="$t(`integrations.types.${p.type}`)"
30
+ color="neutral"
31
+ variant="subtle"
32
+ size="sm"
33
+ />
34
+ <UBadge
35
+ v-if="!p.enabled"
36
+ label="Disabled"
37
+ color="error"
38
+ variant="subtle"
39
+ size="sm"
40
+ />
41
+ </div>
42
+ <div class="text-xs text-muted mt-1">
43
+ {{ $t('integrations.actions_configured') }}:
44
+ <span v-if="p.actions?.on_hire">hire</span><span v-if="p.actions?.on_hire && (p.actions?.on_resign || p.actions?.on_reactivate)">, </span><span v-if="p.actions?.on_resign">resign</span><span v-if="p.actions?.on_resign && p.actions?.on_reactivate">, </span><span v-if="p.actions?.on_reactivate">reactivate</span>
45
+ </div>
46
+ <!-- Test result -->
47
+ <div
48
+ v-if="testResults[p.id]"
49
+ class="mt-2 text-xs"
50
+ :class="testResults[p.id]?.success ? 'text-success' : 'text-error'"
51
+ >
52
+ {{ testResults[p.id]?.message }}
53
+ </div>
54
+ </div>
55
+ <div class="flex items-center gap-2 shrink-0">
56
+ <UButton
57
+ icon="i-ph-plug-light"
58
+ :label="testingId === p.id ? $t('integrations.testing') : $t('integrations.test_connection')"
59
+ variant="outline"
60
+ size="sm"
61
+ :loading="testingId === p.id"
62
+ @click="testConnection(p.id)"
63
+ />
64
+ <UButton
65
+ icon="i-ph-pencil-simple-light"
66
+ variant="ghost"
67
+ size="sm"
68
+ @click="openEdit(p)"
69
+ />
70
+ <UButton
71
+ icon="i-ph-trash-light"
72
+ variant="ghost"
73
+ color="error"
74
+ size="sm"
75
+ @click="confirmDelete(p)"
76
+ />
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <div
83
+ v-else-if="!isPending"
84
+ class="text-sm text-muted py-8 text-center border border-dashed border-default rounded-lg"
85
+ >
86
+ {{ $t('integrations.add_provider') }}
87
+ </div>
88
+
89
+ <!-- Provider form modal -->
90
+ <HrProviderFormModal
91
+ :open="showModal"
92
+ :provider="editingProvider"
93
+ @saved="onSaved"
94
+ @cancel="closeModal"
95
+ />
96
+
97
+ <!-- Delete confirm modal -->
98
+ <UModal
99
+ v-model:open="showDeleteConfirm"
100
+ :title="$t('common.delete')"
101
+ >
102
+ <template #body>
103
+ <p class="text-sm text-muted">
104
+ Delete provider <strong>{{ deletingProvider?.name }}</strong>?
105
+ </p>
106
+ </template>
107
+ <template #footer>
108
+ <div class="flex justify-end gap-2">
109
+ <UButton
110
+ :label="$t('common.cancel')"
111
+ variant="ghost"
112
+ @click="showDeleteConfirm = false"
113
+ />
114
+ <UButton
115
+ :label="$t('common.delete')"
116
+ color="error"
117
+ :loading="deleting"
118
+ @click="doDelete"
119
+ />
120
+ </div>
121
+ </template>
122
+ </UModal>
123
+ </div>
124
+ </template>
125
+
126
+ <script lang="ts" setup>
127
+ const { providers, isPending, fetchProviders, deleteProvider, testConnection: apiTest } = useProviders()
128
+
129
+ const showModal = ref(false)
130
+ const editingProvider = ref<HrProvider | null>(null)
131
+ const showDeleteConfirm = ref(false)
132
+ const deletingProvider = ref<HrProvider | null>(null)
133
+ const deleting = ref(false)
134
+ const testingId = ref<string | null>(null)
135
+ const testResults = ref<Record<string, { success: boolean; message: string }>>({})
136
+
137
+ function openAdd() {
138
+ editingProvider.value = null
139
+ showModal.value = true
140
+ }
141
+
142
+ function openEdit(p: HrProvider) {
143
+ editingProvider.value = p
144
+ showModal.value = true
145
+ }
146
+
147
+ function closeModal() {
148
+ showModal.value = false
149
+ editingProvider.value = null
150
+ }
151
+
152
+ async function onSaved() {
153
+ closeModal()
154
+ await fetchProviders()
155
+ }
156
+
157
+ function confirmDelete(p: HrProvider) {
158
+ deletingProvider.value = p
159
+ showDeleteConfirm.value = true
160
+ }
161
+
162
+ async function doDelete() {
163
+ if (!deletingProvider.value) return
164
+ deleting.value = true
165
+ try {
166
+ await deleteProvider(deletingProvider.value.id)
167
+ showDeleteConfirm.value = false
168
+ deletingProvider.value = null
169
+ }
170
+ finally {
171
+ deleting.value = false
172
+ }
173
+ }
174
+
175
+ async function testConnection(id: string) {
176
+ testingId.value = id
177
+ try {
178
+ const result = await apiTest(id)
179
+ testResults.value[id] = result
180
+ }
181
+ finally {
182
+ testingId.value = null
183
+ }
184
+ }
185
+ </script>
@@ -0,0 +1,83 @@
1
+ <script lang="ts" setup>
2
+ import type {
3
+ PolicyKey,
4
+ AnnualLeaveBaseParams, AnnualLeaveTenureBonusParams,
5
+ SickLeaveBaseParams, PersonalLeaveBaseParams,
6
+ InsuranceEligibilityParams, ProbationPeriodParams,
7
+ } from "../../../../shared/types"
8
+ import HrPolicyEditorLeaveBase from "./policy/editor-leave-base.vue"
9
+ import HrPolicyEditorTenureBonus from "./policy/editor-tenure-bonus.vue"
10
+ import HrPolicyEditorEligibility from "./policy/editor-eligibility.vue"
11
+
12
+ const { policies, togglePolicy, updatePolicyParams, isPending } = useHrPolicies()
13
+
14
+ function paramsOf<T>(key: PolicyKey): T {
15
+ return policies.value.find(p => p.key === key)!.params as T
16
+ }
17
+
18
+ function update(key: PolicyKey, params: object) {
19
+ updatePolicyParams(key, params as never)
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div class="space-y-3">
25
+ <p class="text-xs text-muted">
26
+ {{ $t('policies.recalc_notice') }}
27
+ </p>
28
+ <ul class="space-y-2">
29
+ <li
30
+ v-for="p in policies"
31
+ :key="p.key"
32
+ class="rounded-lg border border-default bg-default p-4"
33
+ :class="!p.enabled && 'opacity-60'"
34
+ >
35
+ <div class="flex items-start justify-between gap-3 mb-3">
36
+ <div class="flex-1 min-w-0">
37
+ <div class="text-sm font-semibold text-default">
38
+ {{ $t(`policies.${p.key}.label`) }}
39
+ </div>
40
+ <p class="mt-0.5 text-xs text-muted">
41
+ {{ $t(`policies.${p.key}.description`) }}
42
+ </p>
43
+ </div>
44
+ <UCheckbox
45
+ :model-value="p.enabled"
46
+ :label="$t('policies.enabled')"
47
+ :loading="isPending"
48
+ @update:model-value="(v) => togglePolicy(p.key, !!v)"
49
+ />
50
+ </div>
51
+
52
+ <template v-if="p.enabled">
53
+ <HrPolicyEditorLeaveBase
54
+ v-if="p.key === 'annual_leave_base' || p.key === 'sick_leave_base' || p.key === 'personal_leave_base'"
55
+ :label="$t(`policies.${p.key}.label`)"
56
+ :helper="$t('policies.base_days_helper')"
57
+ :value-label="$t('policies.days_per_year')"
58
+ :days="paramsOf<AnnualLeaveBaseParams | SickLeaveBaseParams | PersonalLeaveBaseParams>(p.key).days_by_employment_type"
59
+ @update="(v) => update(p.key, { days_by_employment_type: v })"
60
+ />
61
+ <HrPolicyEditorTenureBonus
62
+ v-else-if="p.key === 'annual_leave_tenure_bonus'"
63
+ :tiers="paramsOf<AnnualLeaveTenureBonusParams>(p.key).tiers"
64
+ @update="(v) => update(p.key, { tiers: v })"
65
+ />
66
+ <HrPolicyEditorEligibility
67
+ v-else-if="p.key === 'health_insurance_eligibility' || p.key === 'social_insurance_eligibility'"
68
+ :employment-type-labels="paramsOf<InsuranceEligibilityParams>(p.key).employment_types"
69
+ @update="(v) => update(p.key, { employment_types: v })"
70
+ />
71
+ <HrPolicyEditorLeaveBase
72
+ v-else-if="p.key === 'probation_period'"
73
+ :label="$t(`policies.${p.key}.label`)"
74
+ :helper="$t('policies.probation_months_helper')"
75
+ :value-label="$t('policies.months')"
76
+ :days="paramsOf<ProbationPeriodParams>(p.key).months_by_employment_type"
77
+ @update="(v) => update(p.key, { months_by_employment_type: v })"
78
+ />
79
+ </template>
80
+ </li>
81
+ </ul>
82
+ </div>
83
+ </template>
@@ -0,0 +1,59 @@
1
+ <script lang="ts" setup>
2
+ const props = defineProps<{ open: boolean, entry: any | null, label: string }>()
3
+ const emit = defineEmits<{
4
+ confirm: [data: { eligible: boolean, note: string }]
5
+ cancel: []
6
+ }>()
7
+
8
+ const eligible = ref(false)
9
+ const note = ref('')
10
+
11
+ watch(() => props.open, (v) => {
12
+ if (v && props.entry) {
13
+ eligible.value = props.entry.eligible
14
+ note.value = props.entry.override_note ?? ''
15
+ }
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <UModal
21
+ :open="open"
22
+ @update:open="(v) => !v && emit('cancel')"
23
+ >
24
+ <template #content>
25
+ <UCard v-if="entry">
26
+ <div class="space-y-3 text-sm">
27
+ <h3 class="font-semibold">
28
+ {{ $t('hr.insurance.override.title') }} {{ label }}
29
+ </h3>
30
+ <div class="text-muted">
31
+ {{ $t('hr.insurance.policy_value') }}: <strong>{{ entry.policy_value ? $t('hr.insurance.eligible') : $t('hr.insurance.not_eligible') }}</strong>
32
+ </div>
33
+ <UCheckbox
34
+ v-model="eligible"
35
+ :label="`${$t('hr.insurance.eligible_for')} ${label}`"
36
+ />
37
+ <UFormField :label="$t('hr.insurance.override.reason')">
38
+ <UTextarea
39
+ v-model="note"
40
+ :rows="2"
41
+ />
42
+ </UFormField>
43
+ <div class="flex justify-end gap-2">
44
+ <UButton
45
+ variant="soft"
46
+ color="neutral"
47
+ @click="emit('cancel')"
48
+ >
49
+ {{ $t('common.cancel') }}
50
+ </UButton>
51
+ <UButton @click="emit('confirm', { eligible, note })">
52
+ {{ $t('hr.insurance.override.save') }}
53
+ </UButton>
54
+ </div>
55
+ </div>
56
+ </UCard>
57
+ </template>
58
+ </UModal>
59
+ </template>
@@ -0,0 +1,27 @@
1
+ <script lang="ts" setup>
2
+
3
+ const props = defineProps<{ employmentTypeLabels: string[] }>()
4
+ const emit = defineEmits<{ update: [next: string[]] }>()
5
+
6
+ const { employmentTypes } = useHrSettings()
7
+
8
+ function toggle(label: string, on: boolean) {
9
+ if (on) emit("update", [...new Set([...props.employmentTypeLabels, label])])
10
+ else emit("update", props.employmentTypeLabels.filter(l => l !== label))
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <div class="space-y-2 text-sm">
16
+ <p class="text-muted text-xs">
17
+ {{ $t('policies.eligible_for') }}
18
+ </p>
19
+ <UCheckbox
20
+ v-for="t in employmentTypes"
21
+ :key="t.key"
22
+ :label="t.label"
23
+ :model-value="employmentTypeLabels.includes(t.label)"
24
+ @update:model-value="(v) => toggle(t.label, !!v)"
25
+ />
26
+ </div>
27
+ </template>
@@ -0,0 +1,37 @@
1
+ <script lang="ts" setup>
2
+
3
+ const props = defineProps<{
4
+ label: string
5
+ helper: string
6
+ valueLabel: string
7
+ days: Record<string, number>
8
+ }>()
9
+ const emit = defineEmits<{ update: [next: Record<string, number>] }>()
10
+
11
+ const { employmentTypes } = useHrSettings()
12
+
13
+ function set(empLabel: string, val: number) {
14
+ emit("update", { ...props.days, [empLabel]: val })
15
+ }
16
+ </script>
17
+
18
+ <template>
19
+ <div class="space-y-2 text-sm">
20
+ <p class="text-muted text-xs">
21
+ {{ helper }}
22
+ </p>
23
+ <div
24
+ v-for="t in employmentTypes"
25
+ :key="t.key"
26
+ class="grid grid-cols-3 gap-2 items-center"
27
+ >
28
+ <span>{{ t.label }}</span>
29
+ <UInputNumber
30
+ :model-value="days[t.label] ?? 0"
31
+ :min="0"
32
+ class="col-span-2"
33
+ @update:model-value="(v) => set(t.label, Number(v))"
34
+ />
35
+ </div>
36
+ </div>
37
+ </template>
@@ -0,0 +1,61 @@
1
+ <script lang="ts" setup>
2
+ import type { TenureTier } from "../../../../shared/types"
3
+
4
+ const props = defineProps<{ tiers: TenureTier[] }>()
5
+ const emit = defineEmits<{ update: [next: TenureTier[]] }>()
6
+
7
+ function add() {
8
+ emit("update", [...props.tiers, { after_years: 0, bonus: 0 }])
9
+ }
10
+ function update(idx: number, p: Partial<TenureTier>) {
11
+ const next = [...props.tiers]
12
+ next[idx] = { ...next[idx]!, ...p }
13
+ emit("update", next)
14
+ }
15
+ function remove(idx: number) {
16
+ emit("update", props.tiers.filter((_, i) => i !== idx))
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <div class="space-y-2 text-sm">
22
+ <p class="text-muted text-xs">
23
+ {{ $t('policies.tenure_bonus_desc') }}
24
+ </p>
25
+ <div
26
+ v-for="(t, i) in tiers"
27
+ :key="i"
28
+ class="grid grid-cols-12 gap-2 items-center"
29
+ >
30
+ <span class="col-span-3 text-muted">{{ $t('policies.after') }}</span>
31
+ <UInputNumber
32
+ :model-value="t.after_years"
33
+ :min="0"
34
+ class="col-span-3"
35
+ @update:model-value="(v) => update(i, { after_years: Number(v) })"
36
+ />
37
+ <span class="col-span-2">{{ $t('policies.years_bonus') }}</span>
38
+ <UInputNumber
39
+ :model-value="t.bonus"
40
+ :min="0"
41
+ class="col-span-3"
42
+ @update:model-value="(v) => update(i, { bonus: Number(v) })"
43
+ />
44
+ <UButton
45
+ size="xs"
46
+ variant="ghost"
47
+ color="neutral"
48
+ icon="i-ph-trash-light"
49
+ class="col-span-1"
50
+ @click="remove(i)"
51
+ />
52
+ </div>
53
+ <UButton
54
+ size="xs"
55
+ icon="i-ph-plus-light"
56
+ @click="add"
57
+ >
58
+ {{ $t('policies.add_tier') }}
59
+ </UButton>
60
+ </div>
61
+ </template>